maglevcms 3.0.0.beta2 → 3.0.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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -53
  3. data/Rakefile +3 -1
  4. data/app/assets/builds/maglev/tailwind.css +681 -11670
  5. data/app/assets/config/maglev_manifest.js +3 -2
  6. data/app/assets/javascripts/maglev/client/dom-operations.js +5 -5
  7. data/app/assets/javascripts/maglev/client/iframe-decorator.js +24 -0
  8. data/app/assets/javascripts/maglev/client/incoming-messages.js +13 -3
  9. data/app/assets/javascripts/maglev/client/index.js +3 -2
  10. data/app/assets/javascripts/maglev/client/utils.js +22 -0
  11. data/app/assets/javascripts/maglev/editor/controllers/app/forms/section_form_controller.js +4 -3
  12. data/app/assets/javascripts/maglev/editor/controllers/app/forms/style_form_controller.js +2 -1
  13. data/app/assets/javascripts/maglev/editor/controllers/app/page_preview_controller.js +96 -5
  14. data/app/assets/javascripts/maglev/editor/controllers/app/preview_notification_center_controller.js +105 -23
  15. data/app/assets/javascripts/maglev/editor/controllers/app/setting_controller.js +3 -2
  16. data/app/assets/javascripts/maglev/editor/controllers/shared/copy_to_clipboard_controller.js +2 -1
  17. data/app/assets/javascripts/maglev/editor/controllers/shared/submit_button_controller.js +29 -5
  18. data/app/assets/javascripts/maglev/editor/controllers/utils.js +38 -0
  19. data/app/assets/javascripts/maglev/editor/index.js +5 -44
  20. data/app/assets/javascripts/maglev/editor/patches/page_renderer_patch.js +47 -0
  21. data/app/assets/javascripts/maglev/editor/patches/turbo_delayed_streams.js +35 -0
  22. data/app/assets/javascripts/maglev/editor/patches/turbo_stream_patch.js +50 -0
  23. data/app/assets/stylesheets/maglev/application.css +0 -2
  24. data/app/assets/stylesheets/maglev/tailwind.css.erb +11 -2
  25. data/app/components/maglev/content/link.rb +1 -1
  26. data/app/components/maglev/editor/settings/link/link_component.rb +7 -1
  27. data/app/components/maglev/section_component.rb +11 -1
  28. data/app/components/maglev/uikit/app_layout/sidebar/link_component.html.erb +1 -1
  29. data/app/components/maglev/uikit/app_layout/sidebar/link_component.rb +35 -5
  30. data/app/components/maglev/uikit/app_layout/sidebar_component.html.erb +3 -4
  31. data/app/components/maglev/uikit/app_layout/topbar/logo_component.html.erb +1 -1
  32. data/app/components/maglev/uikit/app_layout/topbar/page_info_component.html.erb +1 -1
  33. data/app/components/maglev/uikit/app_layout/topbar_component.html.erb +1 -1
  34. data/app/components/maglev/uikit/app_layout/topbar_component.rb +1 -1
  35. data/app/components/maglev/uikit/button_group_component/button_group_component.html.erb +5 -0
  36. data/app/components/maglev/uikit/button_group_component.rb +70 -0
  37. data/app/components/maglev/uikit/device_toggler_component.rb +10 -1
  38. data/app/components/maglev/uikit/dropdown_component/dropdown_component.html.erb +2 -2
  39. data/app/components/maglev/uikit/dropdown_component/dropdown_controller.js +6 -1
  40. data/app/components/maglev/uikit/dropdown_component.rb +6 -1
  41. data/app/components/maglev/uikit/form/color_field_component.html.erb +1 -1
  42. data/app/components/maglev/uikit/form/combobox_component.html.erb +1 -0
  43. data/app/components/maglev/uikit/form/link_component.rb +5 -1
  44. data/app/components/maglev/uikit/form/richtext_controller.js +5 -4
  45. data/app/components/maglev/uikit/form/search_form_component.html.erb +1 -0
  46. data/app/components/maglev/uikit/icon_component.rb +3 -0
  47. data/app/components/maglev/uikit/image_library/uploader_controller.js +3 -2
  48. data/app/components/maglev/uikit/list/list_item_component.html.erb +36 -19
  49. data/app/components/maglev/uikit/list/list_item_component.rb +19 -5
  50. data/app/components/maglev/uikit/locale_switcher_component/locale_switcher_component.html.erb +6 -10
  51. data/app/components/maglev/uikit/menu_dropdown_component/menu_dropdown_component.html.erb +6 -2
  52. data/app/components/maglev/uikit/menu_dropdown_component.rb +244 -7
  53. data/app/components/maglev/uikit/page_actions_dropdown_component/page_actions_dropdown_component.html.erb +39 -46
  54. data/app/components/maglev/uikit/pagination_component/pagination_component.html.erb +9 -12
  55. data/app/components/maglev/uikit/pagination_component.rb +6 -1
  56. data/app/components/maglev/uikit/section_toolbar/bottom_component.html.erb +1 -1
  57. data/app/components/maglev/uikit/tabs_component/tabs_component.html.erb +7 -4
  58. data/app/components/maglev/uikit/tabs_component.rb +23 -4
  59. data/app/components/maglev/uikit/well/simple_well_component.html.erb +15 -0
  60. data/app/components/maglev/uikit/well/simple_well_component.rb +13 -0
  61. data/app/controllers/concerns/maglev/editor/errors_concern.rb +9 -9
  62. data/app/controllers/concerns/maglev/editor/preview_urls_concern.rb +32 -0
  63. data/app/controllers/concerns/maglev/editor/turbo_concern.rb +29 -0
  64. data/app/controllers/concerns/maglev/errors_concern.rb +17 -0
  65. data/app/controllers/concerns/maglev/flash_i18n_concern.rb +1 -0
  66. data/app/controllers/maglev/application_controller.rb +2 -1
  67. data/app/controllers/maglev/assets/active_storage_proxy_controller.rb +2 -1
  68. data/app/controllers/maglev/editor/assets_controller.rb +1 -1
  69. data/app/controllers/maglev/editor/base_controller.rb +6 -32
  70. data/app/controllers/maglev/editor/pages/clone_controller.rb +22 -0
  71. data/app/controllers/maglev/editor/pages/discard_draft_controller.rb +17 -0
  72. data/app/controllers/maglev/editor/pages_controller.rb +26 -7
  73. data/app/controllers/maglev/editor/section_blocks_controller.rb +13 -9
  74. data/app/controllers/maglev/editor/sections_controller.rb +26 -7
  75. data/app/controllers/maglev/published_page_preview_controller.rb +4 -0
  76. data/app/controllers/maglev/site_controller.rb +15 -0
  77. data/app/helpers/maglev/application_helper.rb +26 -8
  78. data/app/helpers/maglev/editor/section_blocks_helper.rb +2 -2
  79. data/app/models/maglev/page/publishable_concern.rb +69 -0
  80. data/app/models/maglev/page.rb +3 -0
  81. data/app/models/maglev/section/block.rb +4 -0
  82. data/app/models/maglev/section/content_concern.rb +3 -1
  83. data/app/models/maglev/section.rb +21 -1
  84. data/app/models/maglev/sections_content_store.rb +1 -3
  85. data/app/models/maglev/site.rb +5 -0
  86. data/app/services/concerns/maglev/content/helpers_concern.rb +5 -8
  87. data/app/services/maglev/app_container.rb +4 -2
  88. data/app/services/maglev/discard_page_draft_service.rb +45 -0
  89. data/app/services/maglev/fetch_section_screenshot_url.rb +12 -1
  90. data/app/services/maglev/fetch_site.rb +3 -1
  91. data/app/services/maglev/get_published_page_sections_service.rb +1 -1
  92. data/app/services/maglev/has_unpublished_changes.rb +21 -0
  93. data/app/services/maglev/publish_service.rb +18 -2
  94. data/app/views/layouts/maglev/editor/_sidebar.html.erb +8 -2
  95. data/app/views/layouts/maglev/editor/_topbar.html.erb +6 -23
  96. data/app/views/layouts/maglev/editor/application.html.erb +6 -0
  97. data/app/views/layouts/maglev/editor/topbar/_page_info.html.erb +11 -0
  98. data/app/views/layouts/maglev/editor/topbar/_publish_button.html.erb +35 -0
  99. data/app/views/maglev/editor/assets/index.html.erb +1 -0
  100. data/app/views/maglev/editor/home/index.html.erb +1 -1
  101. data/app/views/maglev/editor/links/edit/_email.html.erb +1 -1
  102. data/app/views/maglev/editor/links/edit/_url.html.erb +1 -1
  103. data/app/views/maglev/editor/pages/_list.html.erb +25 -21
  104. data/app/views/maglev/editor/pages/_preview.html.erb +6 -6
  105. data/app/views/maglev/editor/pages/_preview_empty_message.html.erb +13 -10
  106. data/app/views/maglev/editor/pages/discard_draft/create.turbo_stream.erb +16 -0
  107. data/app/views/maglev/editor/pages/index.html.erb +8 -1
  108. data/app/views/maglev/editor/publication/create.turbo_stream.erb +3 -1
  109. data/app/views/maglev/editor/section_blocks/_form.html.erb +5 -13
  110. data/app/views/maglev/editor/section_blocks/_form_with_tabs.html.erb +15 -0
  111. data/app/views/maglev/editor/section_blocks/_new.html.erb +1 -1
  112. data/app/views/maglev/editor/section_blocks/edit.html.erb +2 -2
  113. data/app/views/maglev/editor/section_blocks/index/_list.html.erb +2 -2
  114. data/app/views/maglev/editor/section_blocks/index/_tree.html.erb +1 -1
  115. data/app/views/maglev/editor/section_blocks/update.turbo_stream.erb +9 -1
  116. data/app/views/maglev/editor/sections/_form.html.erb +6 -20
  117. data/app/views/maglev/editor/sections/_form_with_tabs.html.erb +21 -0
  118. data/app/views/maglev/editor/sections/_list.html.erb +1 -1
  119. data/app/views/maglev/editor/sections/edit.html.erb +3 -3
  120. data/app/views/maglev/editor/sections/index.html.erb +1 -1
  121. data/app/views/maglev/editor/sections/new.html.erb +18 -1
  122. data/app/views/maglev/editor/sections/theme/_empty_list.html.erb +3 -0
  123. data/app/views/maglev/editor/sections/theme/_list.html.erb +35 -0
  124. data/app/views/maglev/editor/sections/theme/_screenshot_placeholder.html.erb +6 -0
  125. data/app/views/maglev/editor/sections/theme/_search.html.erb +22 -0
  126. data/app/views/maglev/editor/sections/update.turbo_stream.erb +9 -1
  127. data/app/views/maglev/editor/shared/_button_label.html.erb +4 -4
  128. data/app/views/maglev/editor/style/edit.html.erb +1 -0
  129. data/app/views/maglev/errors/site_not_found.html.erb +33 -0
  130. data/config/editor_importmap.rb +16 -13
  131. data/config/locales/editor.ar.yml +12 -4
  132. data/config/locales/editor.en.yml +31 -23
  133. data/config/locales/editor.es.yml +12 -4
  134. data/config/locales/editor.fr.yml +12 -4
  135. data/config/locales/editor.pt-BR.yml +12 -4
  136. data/config/routes/maglev/assets.rb +4 -0
  137. data/config/routes/maglev/editor.rb +38 -0
  138. data/config/routes/maglev/preview.rb +8 -0
  139. data/config/routes/maglev/public_preview.rb +6 -0
  140. data/config/routes.rb +8 -44
  141. data/db/migrate/20211013210954_translate_section_content.rb +1 -0
  142. data/db/migrate/20251116171603_add_published_at_to_sites_and_pages.rb +6 -0
  143. data/db/migrate/20260114112058_add_published_payload_to_pages.rb +14 -0
  144. data/exe/tailwind-cli +1 -1
  145. data/lib/generators/maglev/install_generator.rb +9 -7
  146. data/lib/generators/maglev/templates/install/config/initializers/maglev.rb +10 -3
  147. data/lib/maglev/active_storage/serving_blob.rb +29 -0
  148. data/lib/maglev/active_storage.rb +2 -0
  149. data/lib/maglev/config.rb +22 -3
  150. data/lib/maglev/engine.rb +15 -10
  151. data/lib/maglev/errors.rb +1 -0
  152. data/lib/maglev/version.rb +1 -1
  153. data/lib/maglev.rb +18 -3
  154. data/lib/tasks/db_test_all.rake +290 -0
  155. data/lib/tasks/maglev/tailwindcss.rake +1 -0
  156. metadata +55 -19
  157. data/app/controllers/maglev/editor/page_clone_controller.rb +0 -20
  158. data/app/views/maglev/editor/sections/_theme_list.html.erb +0 -32
  159. /data/vendor/javascript/{@floating-ui--core.js → maglev/@floating-ui--core.js} +0 -0
  160. /data/vendor/javascript/{@floating-ui--dom.js → maglev/@floating-ui--dom.js} +0 -0
  161. /data/vendor/javascript/{@floating-ui--utils--dom.js → maglev/@floating-ui--utils--dom.js} +0 -0
  162. /data/vendor/javascript/{@floating-ui--utils.js → maglev/@floating-ui--utils.js} +0 -0
  163. /data/vendor/javascript/{@hotwired--stimulus.js → maglev/@hotwired--stimulus.js} +0 -0
  164. /data/vendor/javascript/{@hotwired--turbo-rails.js → maglev/@hotwired--turbo-rails.js} +0 -0
  165. /data/vendor/javascript/{@hotwired--turbo.js → maglev/@hotwired--turbo.js} +0 -0
  166. /data/vendor/javascript/{@rails--actioncable--src.js → maglev/@rails--actioncable--src.js} +0 -0
  167. /data/vendor/javascript/{@rails--request.js.js → maglev/@rails--request.js.js} +0 -0
  168. /data/vendor/javascript/{@shopify--draggable.js → maglev/@shopify--draggable.js} +0 -0
  169. /data/vendor/javascript/{el-transition.js → maglev/el-transition.js} +0 -0
  170. /data/vendor/javascript/{stimulus-use.js → maglev/stimulus-use.js} +0 -0
  171. /data/vendor/javascript/{tiptap.bundle.js → maglev/tiptap.bundle.js} +0 -0
@@ -1,25 +1,41 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { sleep } from"maglev-controllers/utils"
3
+ import TurboDelayedStreams from 'maglev-patches/turbo_delayed_streams'
3
4
 
4
5
  export default class extends Controller {
5
6
 
6
7
  initialize() {
7
8
  this._start = this.start.bind(this)
8
9
  this._end = this.end.bind(this)
10
+ this._cancel = this.cancel.bind(this)
9
11
  }
10
12
 
11
13
  connect() {
14
+ this.canceled = false
12
15
  this.formElement = this.element.closest('form')
13
16
  this.formElement.addEventListener('turbo:submit-start', this._start)
14
17
  this.formElement.addEventListener('turbo:submit-end', this._end)
18
+ document.addEventListener("turbo:before-cache", this._cancel)
15
19
  }
16
20
 
17
21
  disconnect() {
22
+ this.canceled = true
23
+ this.requestId = null
18
24
  this.formElement.removeEventListener('turbo:submit-start', this._start)
19
- this.formElement.removeEventListener('turbo:submit-end', this._end)
25
+ this.formElement.removeEventListener('turbo:submit-end', this._end)
20
26
  }
21
27
 
22
- start() {
28
+ cancel() {
29
+ this.canceled = true
30
+ this.element.disabled = false
31
+ this.formElement.classList.remove('is-pending', 'is-success', 'is-error')
32
+ this.formElement.classList.add('is-default')
33
+ }
34
+
35
+ start(event) {
36
+ // the requestId is set by the turbo:before-fetch-request listener. We need it if there are delayed streams to render
37
+ this.requestId = event.detail.formSubmission.fetchRequest.fetchOptions.turboRequestId
38
+
23
39
  this.formElement.classList.add('is-pending')
24
40
  this.startedAt = Date.now()
25
41
  }
@@ -30,16 +46,24 @@ export default class extends Controller {
30
46
 
31
47
  // on an UX standpoint, we want to show the pending state for a short time to avoid flickering
32
48
  // if the submit (call to the server) took less than 800ms, we wait for 800ms to show the pending state
33
- if (Date.now() - this.startedAt < 800) await sleep(800)
49
+ if (Date.now() - this.startedAt < 600) await sleep(600)
50
+
51
+ if (this.canceled) return
34
52
 
35
53
  this.formElement.classList.remove('is-pending')
36
- this.formElement.classList.add(event.detail.success ? 'is-success' : 'is-error')
54
+ this.formElement.classList.add(event.detail.success ? 'is-success' : 'is-error')
37
55
 
38
56
  // wait for 2 seconds and then remove the success or error class and add the default class
39
- await sleep(1600)
57
+ await sleep(1400)
58
+
59
+ if (this.canceled) return
40
60
 
41
61
  this.formElement.classList.remove(event.detail.success ? 'is-success' : 'is-error')
42
62
  this.formElement.classList.add('is-default')
43
63
  this.element.disabled = false
64
+
65
+ // render the delayed turbo stream messages from the request
66
+ // use the requestId to identify the request and render the turbo stream messages
67
+ TurboDelayedStreams.render(this.requestId)
44
68
  }
45
69
  }
@@ -5,6 +5,44 @@ export const isSamePath = (targetPath) => {
5
5
  return current.pathname === target.pathname
6
6
  }
7
7
 
8
+ const getParentEditorConfig = () => {
9
+ try {
10
+ return window.parent?.maglevEditorConfig
11
+ } catch (_) {
12
+ return undefined
13
+ }
14
+ }
15
+
16
+ const getEditorConfig = () => {
17
+ return window.maglevEditorConfig || getParentEditorConfig() || {}
18
+ }
19
+
20
+ export const isEditorJsLogsEnabled = () => {
21
+ return getEditorConfig().jsLogsEnabled === true
22
+ }
23
+
24
+ export const log = (...args) => {
25
+ if (isEditorJsLogsEnabled()) {
26
+ console.log(...args)
27
+ }
28
+ }
29
+
8
30
  export const sleep = (ms) => {
9
31
  return new Promise(resolve => setTimeout(resolve, ms))
32
+ }
33
+
34
+ function uuidv4() {
35
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
36
+ const r = Math.random() * 16 | 0
37
+ const v = c === "x" ? r : (r & 0x3 | 0x8)
38
+ return v.toString(16)
39
+ })
40
+ }
41
+
42
+ export function generateRequestId() {
43
+ // use the native randomUUID function if available (Browsers don't expose it for non-secure domains)
44
+ if (window.crypto?.randomUUID) {
45
+ return window.crypto.randomUUID()
46
+ }
47
+ return uuidv4()
10
48
  }
@@ -1,8 +1,10 @@
1
1
  import "@hotwired/turbo-rails"
2
2
  import "maglev-controllers"
3
- import { PageRenderer, StreamActions } from '@hotwired/turbo'
3
+ import "maglev-patches/page_renderer_patch"
4
+ import "maglev-patches/turbo_stream_patch"
5
+ import { log } from "maglev-controllers/utils"
4
6
 
5
- console.log('Maglev Editor v2 ⚡️')
7
+ console.log('Maglev Editor v3 ⚡️')
6
8
 
7
9
  // We need to set the content locale in the headers for each Turbo request
8
10
  document.addEventListener("turbo:before-fetch-request", (event) => {
@@ -22,45 +24,4 @@ document.addEventListener("click", (event) => {
22
24
  if (current.pathname === target.pathname && current.search === target.search) {
23
25
  link.dataset.turboAction = "replace"
24
26
  }
25
- })
26
-
27
- // iFrames will always be reloaded even if we mark them as turbo-permanent.
28
- // In the context of the Editor, the workaround is to replace the content of another DIV (#root)
29
- // instead of the body and put the preview iframe as a sibling of the root DIV.
30
- PageRenderer.prototype.assignNewBody = function() {
31
- const body = document.querySelector("#root")
32
- const el = this.newElement.querySelector("#root")
33
-
34
- // replace all the data attributes of the BODY tag with the data attributes of the newElement
35
- // This is because the BODY tag carries the current page id in one of the controller values
36
- const bodyDataAttributes = document.body.dataset
37
- const newElementDataAttributes = this.newElement.dataset
38
- Object.keys(newElementDataAttributes).forEach(key => {
39
- bodyDataAttributes[key] = newElementDataAttributes[key]
40
- })
41
-
42
- // now replace the "body" with the newElement
43
- if (body && el) {
44
- body.replaceWith(el)
45
- return
46
- } else if (document.body && this.newElement instanceof HTMLBodyElement) {
47
- document.body.replaceWith(this.newElement)
48
- } else {
49
- document.documentElement.appendChild(this.newElement)
50
- }
51
- }
52
-
53
- // Custom stream actions
54
-
55
- StreamActions.console_log = function() {
56
- const message = this.getAttribute("message")
57
- console.log(message)
58
- }
59
-
60
- StreamActions.dispatch_event = function() {
61
- const type = this.getAttribute("type")
62
- const payload = this.getAttribute("payload")
63
- console.log('dispatchEvent', type, payload, `dispatcher:${type}`)
64
- const event = new CustomEvent(`dispatcher:${type}`, { detail: JSON.parse(payload) })
65
- window.dispatchEvent(event)
66
- }
27
+ })
@@ -0,0 +1,47 @@
1
+ import { PageRenderer } from '@hotwired/turbo'
2
+
3
+ // iFrames will always be reloaded even if we mark them as turbo-permanent.
4
+ // In the context of the Editor, the workaround is to replace the content of another DIV (#root)
5
+ // instead of the body and put the preview iframe as a sibling of the root DIV.
6
+ // assignNewBody is the right function to patch because it's called by both the PageRenderer and the SnapshotRenderer.
7
+
8
+ // Inspirations:
9
+ // - https://github.com/hotwired/turbo/pull/711/files
10
+ // - https://github.com/Challenge-Guy/turbo-cfm1/commit/2a3b0acfe0367f32c9d1635ff7c6fd7d87d2a2cd
11
+ PageRenderer.prototype.assignNewBody = function() {
12
+ const body = document.querySelector("#root")
13
+ const el = this.newElement.querySelector("#root")
14
+
15
+ // Only update sync elements during snapshot preview render
16
+ // those elements are permanent during the snapshot preview render and updated when we've got the new snapshot
17
+ if (this.isPreview) {
18
+ const syncElements = document.querySelectorAll('[data-turbo-sync]')
19
+ syncElements.forEach(element => {
20
+ const currentElement = document.querySelector(`#${element.id}`)
21
+ const newElement = this.newElement.querySelector(`#${element.id}`)
22
+
23
+ if (currentElement && newElement) {
24
+ // sync the innerHTML of the newElement with the currentElement
25
+ newElement.innerHTML = currentElement.innerHTML
26
+ }
27
+ })
28
+ }
29
+
30
+ // replace all the data attributes of the BODY tag with the data attributes of the newElement
31
+ // This is because the BODY tag carries the current page id in one of the controller values
32
+ const bodyDataAttributes = document.body.dataset
33
+ const newElementDataAttributes = this.newElement.dataset
34
+ Object.keys(newElementDataAttributes).forEach(key => {
35
+ bodyDataAttributes[key] = newElementDataAttributes[key]
36
+ })
37
+
38
+ // now replace the "body" with the newElement
39
+ if (body && el) {
40
+ body.replaceWith(el)
41
+ return
42
+ } else if (document.body && this.newElement instanceof HTMLBodyElement) {
43
+ document.body.replaceWith(this.newElement)
44
+ } else {
45
+ document.documentElement.appendChild(this.newElement)
46
+ }
47
+ }
@@ -0,0 +1,35 @@
1
+ // Delayed Turbo streams are streams that are not rendered immediately, but are rendered later manually by the application
2
+ // It's used by the SubmitButton controller for instance
3
+
4
+ class TurboDelayedStream {
5
+ constructor(maxElements = 10) {
6
+ this.maxElements = maxElements
7
+ this.elements = new Map()
8
+ }
9
+
10
+ add(requestId, render) {
11
+ if (this.elements.has(requestId)) {
12
+ this.elements.get(requestId).push(render)
13
+ } else {
14
+ this.elements.set(requestId, [render])
15
+ }
16
+
17
+ if (this.elements.size > this.maxElements) {
18
+ this.elements.delete(this.elements.keys().next().value)
19
+ }
20
+ }
21
+
22
+ render(requestId) {
23
+ if (!this.elements.has(requestId)) return
24
+
25
+ const renderFns = this.elements.get(requestId)
26
+
27
+ // render all the turbo streams for this request
28
+ renderFns.forEach(render => render())
29
+
30
+ // remove the element from the list
31
+ this.elements.delete(requestId)
32
+ }
33
+ }
34
+
35
+ export default new TurboDelayedStream()
@@ -0,0 +1,50 @@
1
+
2
+ import { StreamActions } from '@hotwired/turbo'
3
+ import TurboDelayedStreams from 'maglev-patches/turbo_delayed_streams'
4
+ import { generateRequestId, log } from 'maglev-controllers/utils'
5
+
6
+ // Custom stream actions
7
+ StreamActions.console_log = function() {
8
+ const message = this.getAttribute("message")
9
+ log(message)
10
+ }
11
+
12
+ StreamActions.dispatch_event = function() {
13
+ const type = this.getAttribute("type")
14
+ const payload = this.getAttribute("payload")
15
+ log('dispatchEvent', type, payload, `dispatcher:${type}`)
16
+ const event = new CustomEvent(`dispatcher:${type}`, { detail: JSON.parse(payload) })
17
+ window.dispatchEvent(event)
18
+ }
19
+
20
+ // Handle delayed streams through Turbo events
21
+
22
+ document.addEventListener("turbo:before-stream-render", (event) => {
23
+ const delayedStream = (
24
+ event.detail.newStream.templateContent.querySelector('meta[name="turbo-delayed-stream"]')?.content ?? event.detail.newStream.getAttribute('delayed')
25
+ ) === 'true'
26
+ const requestId = (
27
+ event.detail.newStream.templateContent.querySelector('meta[name="turbo-request-id"]')?.content ?? event.detail.newStream.getAttribute('request-id')
28
+ )?.split(',')?.[0]
29
+
30
+ if (delayedStream) {
31
+ // Keep the stream in the queue to be rendered later
32
+ TurboDelayedStreams.add(requestId, () => {
33
+ // console.log('rendering delayed stream', requestId, event.detail.newStream)
34
+ event.detail.render(event.detail.newStream)
35
+ })
36
+
37
+ // Cancel Turbo's rendering for this stream
38
+ event.preventDefault()
39
+ }
40
+ })
41
+
42
+ document.addEventListener('turbo:before-fetch-request', (event) => {
43
+ const requestId = generateRequestId()
44
+
45
+ // Attach to headers (for server → client stream correlation)
46
+ event.detail.fetchOptions.headers["X-Turbo-Request-ID"] = requestId
47
+
48
+ // Attach to the fetchRequest instance
49
+ event.detail.fetchOptions.turboRequestId = requestId
50
+ })
@@ -10,8 +10,6 @@
10
10
  * files in this directory. Styles in this file should be added after the last require_* statement.
11
11
  * It is generally better to create a new file per style scope.
12
12
  *
13
- *= require_tree .
14
- *= require_self
15
13
  */
16
14
 
17
15
  @keyframes slide-out {
@@ -8,7 +8,13 @@
8
8
  --color-editor-primary: var(--editor-color-primary);
9
9
  }
10
10
 
11
- @source "../../../";
11
+ /* Empty-state overlay in #root: visible when the preview Stimulus shell has .is-empty */
12
+ @custom-variant editor-preview-empty (body:has([data-controller~="editor-page-preview"].is-empty) &);
13
+
14
+ @source "<%= Maglev::Engine.root.join('app') %>";
15
+ @source "<%= Maglev::Engine.root.join('lib') %>";
16
+ @source "<%= Maglev::Engine.root.join('spec/components/previews/maglev/uikit') %>";
17
+
12
18
  <% Maglev.config.tailwindcss_folders.each do |folder| %>
13
19
  @source "<%= folder %>";
14
20
  <% end %>
@@ -26,6 +32,10 @@
26
32
  }
27
33
 
28
34
  @layer base {
35
+ .turbo-progress-bar {
36
+ background-color: var(--editor-color-primary) !important;
37
+ }
38
+
29
39
  /* Tiptap / ProseMirror */
30
40
  .ProseMirror {
31
41
  @apply block py-2 px-3 rounded bg-gray-100 text-gray-800 focus:outline-none focus:ring focus:ring-inset focus:ring-2 focus:ring-editor-primary/50 placeholder-gray-500;
@@ -141,7 +151,6 @@
141
151
  td,
142
152
  th {
143
153
  min-width: 1em;
144
- // border: 2px solid $color-grey;
145
154
  @apply border-2;
146
155
  @apply border-solid;
147
156
  padding: 3px 5px;
@@ -4,7 +4,7 @@ module Maglev
4
4
  module Content
5
5
  class Link < Base
6
6
  def href
7
- link[:href]
7
+ link[:href] || '#'
8
8
  end
9
9
 
10
10
  def text
@@ -6,12 +6,18 @@ module Maglev
6
6
  module Link
7
7
  class LinkComponent < Maglev::Editor::Settings::BaseComponent
8
8
  def edit_link_path
9
- fetch_path(:edit_link_path, { value: value, input_name: input_name })
9
+ fetch_path(:edit_link_path, { value: value || default_value, input_name: input_name })
10
10
  end
11
11
 
12
12
  def after_initialize
13
13
  @value = value&.with_indifferent_access
14
14
  end
15
+
16
+ private
17
+
18
+ def default_value
19
+ { link_type: 'url' }
20
+ end
15
21
  end
16
22
  end
17
23
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # NOTE: this class is too long (but not too complex),
4
+ # we need to move it along with the PageComponent and BlockComponent
5
+ # to a separate folder (content?)
6
+
7
+ # rubocop:disable Metrics/ClassLength
3
8
  module Maglev
4
9
  class SectionComponent < BaseComponent
5
10
  include TagHelper
@@ -31,7 +36,7 @@ module Maglev
31
36
  end
32
37
 
33
38
  def lock_version
34
- @lock_version ||= attributes[:lock_version] || '0'
39
+ @lock_version ||= lock_source&.lock_version || '0'
35
40
  end
36
41
 
37
42
  def dom_data
@@ -63,6 +68,10 @@ module Maglev
63
68
 
64
69
  private
65
70
 
71
+ def lock_source
72
+ definition&.site_scoped? ? site : page
73
+ end
74
+
66
75
  def build_block_list
67
76
  build_blocks(attributes[:blocks])
68
77
  end
@@ -131,3 +140,4 @@ module Maglev
131
140
  end
132
141
  end
133
142
  end
143
+ # rubocop:enable Metrics/ClassLength
@@ -1,5 +1,5 @@
1
1
  <li>
2
- <%= link_to path, class: link_classes(active: active?), data: data do %>
2
+ <%= link_to path, link_html_options do %>
3
3
  <%= render Maglev::Uikit::IconComponent.new(name: icon, size: icon_size) %>
4
4
  <% end %>
5
5
  </li>
@@ -5,12 +5,24 @@ module Maglev
5
5
  module AppLayout
6
6
  module Sidebar
7
7
  class LinkComponent < Maglev::Uikit::BaseComponent
8
- attr_reader :path, :icon, :icon_size, :active, :data
8
+ LINK_BASE_CLASSES = [
9
+ 'relative flex w-full min-h-11 items-center justify-center rounded py-3',
10
+ 'outline-none transition-colors duration-200',
11
+ 'focus-visible:ring-2 focus-visible:ring-editor-primary/50',
12
+ 'focus-visible:ring-offset-2 focus-visible:ring-offset-white'
13
+ ].join(' ')
9
14
 
10
- def initialize(path:, icon:, active: false, options: {})
15
+ LINK_ACTIVE_CLASSES = 'bg-gray-100 text-editor-primary hover:bg-gray-100'
16
+
17
+ LINK_INACTIVE_CLASSES = 'text-black hover:bg-gray-100'
18
+
19
+ attr_reader :path, :icon, :icon_size, :active, :data, :label
20
+
21
+ def initialize(path:, icon:, active: false, label: nil, options: {})
11
22
  @path = path
12
23
  @active = active
13
24
  @icon = icon
25
+ @label = label
14
26
  @icon_size = options[:icon_size] || '1.5rem'
15
27
  @position = options[:position] || :top
16
28
  @data = options[:data]
@@ -24,15 +36,33 @@ module Maglev
24
36
  @position == :top
25
37
  end
26
38
 
39
+ def link_html_options
40
+ {
41
+ class: link_classes(active: active?),
42
+ data: data,
43
+ title: label.presence,
44
+ aria: link_aria_attributes
45
+ }.compact
46
+ end
47
+
27
48
  def link_classes(...)
28
49
  class_variants(
29
- base: 'flex justify-center py-5 -ml-4 -mr-4 hover:bg-gray-100 transition-colors duration-200',
50
+ base: LINK_BASE_CLASSES,
30
51
  variants: {
31
- active: 'bg-gray-100',
32
- '!active': 'bg-white'
52
+ active: LINK_ACTIVE_CLASSES,
53
+ '!active': LINK_INACTIVE_CLASSES
33
54
  }
34
55
  ).render(...)
35
56
  end
57
+
58
+ private
59
+
60
+ def link_aria_attributes
61
+ aria = {}
62
+ aria[:label] = label.presence
63
+ aria[:current] = 'page' if active?
64
+ aria.compact.presence
65
+ end
36
66
  end
37
67
  end
38
68
  end
@@ -2,15 +2,14 @@
2
2
  id="app-layout-sidebar"
3
3
  class="w-16 flex-shrink-0 flex content-center h-full border-r border-gray-200 relative z-20 bg-white"
4
4
  >
5
- <nav class="w-16 flex flex-col justify-between">
6
- <ol class="divide-y divide-gray-300 px-4">
5
+ <nav class="flex h-full w-16 flex-col justify-between py-2">
6
+ <ol class="flex flex-col gap-1 px-2">
7
7
  <% links.each do |link| %>
8
8
  <%= link if link.top? %>
9
9
  <% end %>
10
10
  </ol>
11
11
 
12
- <ol class="divide-y divide-gray-300 px-4">
13
- <li></li>
12
+ <ol class="flex flex-col gap-1 px-2">
14
13
  <% links.each do |link| %>
15
14
  <%= link unless link.top? %>
16
15
  <% end %>
@@ -1,3 +1,3 @@
1
- <%= link_to root_path, class: "w-16 h-full flex justify-center items-center border-r border-gray-200 shrink-0" do %>
1
+ <%= link_to root_path, class: "w-16 h-full flex justify-center items-center border-r border-gray-200 shrink-0", data: { controller: 'prevent-same-path' } do %>
2
2
  <%= image_tag logo_url, class: "w-2/4", id: 'editor-logo-image', data: { turbo_permanent: true } %>
3
3
  <% end %>
@@ -4,7 +4,7 @@
4
4
  <div class="flex flex-row items-center h-full w-full px-4 overflow-hidden">
5
5
  <div class="flex flex-col leading-none overflow-hidden">
6
6
  <div class="flex items-center">
7
- <%= render Maglev::Uikit::IconComponent.new(name: icon_name, size: '1.1rem', class_names: 'shrink-0 text-gray-900') %>
7
+ <%= helpers.maglev_page_icon(page, size: '1.1rem', wrapper_class_names: 'text-gray-900') %>
8
8
  <div class="text-base font-semibold truncate ml-1 mr-3">
9
9
  <%= page.title.presence || page.default_title %>
10
10
  </div>
@@ -9,7 +9,7 @@
9
9
  <%= render Maglev::Uikit::DeviceTogglerComponent.new %>
10
10
  </div>
11
11
 
12
- <div class="col-span-2 flex justify-end h-full">
12
+ <div class="col-span-2 flex justify-end h-full items-center">
13
13
  <%= actions %>
14
14
  </div>
15
15
  </div>
@@ -5,7 +5,7 @@ module Maglev
5
5
  module AppLayout
6
6
  class TopbarComponent < ViewComponent::Base
7
7
  renders_one :logo, 'Maglev::Uikit::AppLayout::Topbar::LogoComponent'
8
- # -> { Maglev::Uikit::AppLayout::Topbar::LogoComponent.new(root_path: root_path, logo_url: logo_url) }
8
+
9
9
  renders_one :page_info
10
10
  renders_one :actions
11
11
 
@@ -0,0 +1,5 @@
1
+ <div class="[&>*:first-child]:rounded-l-sm [&>*:last-child]:rounded-r-sm inline-flex items-center text-white divide-x divide-white/15">
2
+ <% buttons.each do |button| %>
3
+ <%= button %>
4
+ <% end %>
5
+ </div>
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Uikit
5
+ class ButtonGroupComponent < Maglev::Uikit::BaseComponent
6
+ renders_many :buttons
7
+
8
+ attr_reader :color, :size
9
+
10
+ def initialize(color: nil, size: nil)
11
+ @color = color || :primary
12
+ @size = size || :medium
13
+ end
14
+
15
+ # rubocop:disable Metrics/MethodLength
16
+ def button_classes
17
+ class_variants(
18
+ base: %(
19
+ inline-flex items-center justify-center cursor-pointer
20
+ group-[.is-success]/form:bg-green-500/95 group-[.is-success]/form:hover:bg-green-500/100
21
+ group-[.is-success]/form:disabled:bg-green-500/75
22
+ group-[.is-error]/form:bg-red-500/95 group-[.is-error]/form:hover:bg-red-500/100
23
+ group-[.is-error]/form:disabled:bg-red-500/75
24
+ ),
25
+ variants: {
26
+ size: {
27
+ medium: 'inline-flex items-center justify-center px-4 h-10'
28
+ },
29
+ color: {
30
+ primary: 'text-white bg-editor-primary hover:bg-editor-primary/90 disabled:bg-editor-primary/75',
31
+ secondary: 'text-gray-800 hover:bg-gray-100'
32
+ }
33
+ }
34
+ ).render(color: color, size: size)
35
+ end
36
+ # rubocop:enable Metrics/MethodLength
37
+
38
+ # rubocop:disable Metrics/MethodLength
39
+ def wrapper_classes(**args)
40
+ class_variants(
41
+ base: %(
42
+ inline-flex items-center transition-colors duration-200 h-full
43
+ [.is-success]:bg-green-500/95 [.is-success]:hover:bg-green-500/100
44
+ [.is-success]:disabled:bg-green-500/75
45
+ [.is-error]:bg-red-500/95 [.is-error]:hover:bg-red-500/100
46
+ [.is-error]:disabled:bg-red-500/75
47
+ ),
48
+ variants: {
49
+ color: {
50
+ primary: 'text-white bg-editor-primary hover:bg-editor-primary/90 has-[:disabled]:bg-editor-primary/75',
51
+ secondary: 'text-gray-800 hover:bg-gray-100'
52
+ }
53
+ }
54
+ ).render(color: color, **args)
55
+ end
56
+ # rubocop:enable Metrics/MethodLength
57
+
58
+ def wrapped_button_classes(**args)
59
+ class_variants(
60
+ base: 'cursor-pointer',
61
+ variants: {
62
+ size: {
63
+ medium: 'inline-flex items-center justify-center px-4 h-10'
64
+ }
65
+ }
66
+ ).render(size: size, **args)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -5,13 +5,22 @@ module Maglev
5
5
  class DeviceTogglerComponent < Maglev::Uikit::BaseComponent
6
6
  def toggler_classes(...)
7
7
  class_variants(
8
- base: 'cursor-pointer hover:bg-gray-100 h-10 w-10 flex items-center justify-center',
8
+ base: base_classes,
9
9
  variants: {
10
10
  active: active_classes
11
11
  }
12
12
  ).render(...)
13
13
  end
14
14
 
15
+ def base_classes
16
+ [
17
+ 'cursor-pointer hover:bg-gray-100 h-10 w-10 flex items-center justify-center rounded',
18
+ 'outline-none transition-colors duration-200',
19
+ 'focus-visible:ring-2 focus-visible:ring-editor-primary/50',
20
+ 'focus-visible:ring-offset-2 focus-visible:ring-offset-white'
21
+ ].join(' ').freeze
22
+ end
23
+
15
24
  def active_classes
16
25
  'bg-gray-100'
17
26
  end
@@ -1,5 +1,5 @@
1
1
  <%= tag.div(
2
- class: 'relative',
2
+ class: wrapper_classes,
3
3
  data: {
4
4
  controller: 'uikit-dropdown',
5
5
  'uikit-dropdown-placement-value': placement
@@ -8,7 +8,7 @@
8
8
  <%= trigger %>
9
9
 
10
10
  <div
11
- class="hidden z-50 opacity-0 scale-95 shadow-lg rounded p-1 text-sm bg-white w-max fixed"
11
+ class="hidden z-50 opacity-0 scale-95 shadow-lg rounded text-sm bg-white w-max fixed"
12
12
  data-aria-orientation="vertical"
13
13
  data-aria-labelledby="dropdown-button"
14
14
  data-uikit-dropdown-target="content"