maglevcms 1.1.6 → 1.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/components/maglev/content/image.rb +2 -0
  3. data/app/controllers/concerns/maglev/fetchers_concern.rb +1 -0
  4. data/app/controllers/concerns/maglev/standalone_sections_concern.rb +6 -0
  5. data/app/controllers/maglev/sitemap_controller.rb +31 -0
  6. data/app/frontend/editor/assets/remixicons/ri-palette-line.svg +1 -0
  7. data/app/frontend/editor/components/dynamic-form/dynamic-input.vue +2 -1
  8. data/app/frontend/editor/components/header-nav/publish-button.vue +1 -1
  9. data/app/frontend/editor/components/kit/collection-item-input.vue +5 -2
  10. data/app/frontend/editor/components/kit/color-input/core-input.vue +85 -0
  11. data/app/frontend/editor/components/kit/color-input/preset-button.vue +57 -0
  12. data/app/frontend/editor/components/kit/color-input/preset-dropdown.vue +46 -0
  13. data/app/frontend/editor/components/kit/color-input.vue +43 -0
  14. data/app/frontend/editor/components/kit/dropdown.vue +1 -0
  15. data/app/frontend/editor/components/kit/index.js +2 -2
  16. data/app/frontend/editor/components/kit/submit-button.vue +3 -3
  17. data/app/frontend/editor/components/kit/tabs.vue +2 -1
  18. data/app/frontend/editor/components/kit/text-input.vue +2 -2
  19. data/app/frontend/editor/misc/utils.js +32 -2
  20. data/app/frontend/editor/services/api.js +2 -2
  21. data/app/frontend/editor/spec/__mocks__/page.js +36 -36
  22. data/app/frontend/editor/spec/__mocks__/section.js +29 -29
  23. data/app/helpers/maglev/editor_helper.rb +1 -1
  24. data/app/helpers/maglev/page_preview_helper.rb +5 -1
  25. data/app/helpers/maglev/sitemap_helper.rb +13 -0
  26. data/app/models/concerns/maglev/sections_concern.rb +10 -0
  27. data/app/models/maglev/page/path_concern.rb +8 -1
  28. data/app/models/maglev/section/setting.rb +5 -0
  29. data/app/models/maglev/site/locales_concern.rb +8 -0
  30. data/app/services/concerns/maglev/get_page_sections/transform_collection_item_concern.rb +8 -1
  31. data/app/services/maglev/app_container.rb +2 -0
  32. data/app/services/maglev/fetch_collection_items.rb +6 -2
  33. data/app/services/maglev/persist_page.rb +23 -4
  34. data/app/services/maglev/setup_pages.rb +41 -5
  35. data/app/views/maglev/api/pages/_show.json.jbuilder +1 -2
  36. data/app/views/maglev/sitemap/index.xml.builder +21 -0
  37. data/config/routes.rb +1 -0
  38. data/db/migrate/20210830085101_create_maglev_page_paths.rb +1 -1
  39. data/db/migrate/20210906102712_add_canonical_to_pages.rb +1 -1
  40. data/db/migrate/20211008064437_add_locales_to_sites.rb +1 -1
  41. data/db/migrate/20211013210954_translate_section_content.rb +1 -1
  42. data/db/migrate/20211101205001_add_lock_version_to_maglev_pages.rb +1 -1
  43. data/db/migrate/20211116161121_better_page_path_canonical_indices.rb +1 -1
  44. data/db/migrate/20211124101005_fix_page_path_indices.rb +1 -1
  45. data/db/migrate/20211203224112_add_open_graph_tags_to_pages.rb +1 -1
  46. data/db/migrate/20220612092235_add_style_to_sites.rb +1 -1
  47. data/lib/generators/maglev/install_generator.rb +3 -2
  48. data/lib/generators/maglev/section_generator.rb +2 -1
  49. data/lib/generators/maglev/templates/section/app/theme/sections/%category%/%file_name%.yml.tt +13 -9
  50. data/lib/maglev/engine.rb +8 -2
  51. data/lib/maglev/version.rb +1 -1
  52. data/package.json +3 -3
  53. data/yarn.lock +799 -565
  54. metadata +14 -7
  55. data/app/frontend/editor/components/kit/color-picker.vue +0 -81
  56. /data/app/frontend/editor/assets/remixicons/{check-line.svg → ri-check-line.svg} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0d746d7653900e39f4c0a7b45ba3503a5688d37cc45f242943f7187729f607f
4
- data.tar.gz: 4d5a6ff5eab5449ada366810c2ffedbde9466600faf1d30c8c4b6c906db24510
3
+ metadata.gz: b23017be4bbbf4d06d99cf0f94ede1136e0e0ea343e0fbbef94edb6dc4d112d4
4
+ data.tar.gz: b6e5d107a2ea7a18091ffdd084a3fed631d0726fe845e0ac0f7100f1fc3e2b60
5
5
  SHA512:
6
- metadata.gz: 6395309dbf50aed228ef2143e48bc4fb34f3184f715d9b70b478487567c56485483253b8e66a951a512e592917732be376815f78591a6fc02fae3e730bede303
7
- data.tar.gz: 7fd1217aae956ca2b19228fe83c815f9905e18020047db71feccb15c20662ff1b720d9af2dd54874e0217e7ed0f2ef90235e49444c130b6bac24d7b27f92c715
6
+ metadata.gz: 1b579c11945ca3c28bb48f59d7efc0ea70a6d1608b7275cd507c3044cf30446bf3a342163ad9c468f0f3cf690386be137401e34f6426bbc016b4306af2d701ff
7
+ data.tar.gz: d4257dfeb8ef1a210fc9147325e4cae5517c4a5f676b7c27637353573ee9c4ba315283edc4b0c97253d06b6327ba48b4fbf25199b4247f21913482b17f53777a
@@ -3,6 +3,8 @@
3
3
  module Maglev
4
4
  module Content
5
5
  class Image < Base
6
+ def_delegators :url, :present?
7
+
6
8
  def url
7
9
  return image[:url] if asset_host.nil? || !hosted_on_platform?
8
10
 
@@ -80,6 +80,7 @@ module Maglev
80
80
  # we drop the path after the "_" segment
81
81
  params[:path].split('/').reduce([]) do |memo, segment|
82
82
  return memo if segment == '_'
83
+
83
84
  memo.push(segment)
84
85
  end
85
86
  end
@@ -15,6 +15,8 @@ module Maglev
15
15
  private
16
16
 
17
17
  def fetch_maglev_site_scoped_sections
18
+ return if within_maglev_engine?
19
+
18
20
  fetch_maglev_site
19
21
  fetch_maglev_theme
20
22
  fetch_maglev_dummy_page
@@ -24,5 +26,9 @@ module Maglev
24
26
  def fetch_maglev_dummy_page
25
27
  @fetch_maglev_page = ::Maglev::Page.new(title: 'DummyPage', sections: fetch_maglev_site.sections)
26
28
  end
29
+
30
+ def within_maglev_engine?
31
+ controller_path.starts_with?('maglev/')
32
+ end
27
33
  end
28
34
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ class SitemapController < ApplicationController
5
+ include Maglev::FetchersConcern
6
+ include Maglev::ServicesConcern
7
+ include Maglev::ContentLocaleConcern
8
+
9
+ before_action :verify_request_format!
10
+ before_action :fetch_maglev_site
11
+
12
+ def index
13
+ @host = request.protocol + fetch_host
14
+ @pages = fetch_pages
15
+ end
16
+
17
+ protected
18
+
19
+ def fetch_host
20
+ request.headers['HTTP_X_MAGLEV_HOST'] || request.host
21
+ end
22
+
23
+ def fetch_pages
24
+ Maglev::Page.all.visible
25
+ end
26
+
27
+ def verify_request_format!
28
+ raise ActionController::UnknownFormat, 'Sitemap is only rendered as XML' if request.format != 'xml'
29
+ end
30
+ end
31
+ end
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C17.5222 2 22 5.97778 22 10.8889C22 13.9556 19.5111 16.4444 16.4444 16.4444H14.4778C13.5556 16.4444 12.8111 17.1889 12.8111 18.1111C12.8111 18.5333 12.9778 18.9222 13.2333 19.2111C13.5 19.5111 13.6667 19.9 13.6667 20.3333C13.6667 21.2556 12.9 22 12 22C6.47778 22 2 17.5222 2 12C2 6.47778 6.47778 2 12 2ZM10.8111 18.1111C10.8111 16.0843 12.451 14.4444 14.4778 14.4444H16.4444C18.4065 14.4444 20 12.851 20 10.8889C20 7.1392 16.4677 4 12 4C7.58235 4 4 7.58235 4 12C4 16.19 7.2226 19.6285 11.324 19.9718C10.9948 19.4168 10.8111 18.7761 10.8111 18.1111ZM7.5 12C6.67157 12 6 11.3284 6 10.5C6 9.67157 6.67157 9 7.5 9C8.32843 9 9 9.67157 9 10.5C9 11.3284 8.32843 12 7.5 12ZM16.5 12C15.6716 12 15 11.3284 15 10.5C15 9.67157 15.6716 9 16.5 9C17.3284 9 18 9.67157 18 10.5C18 11.3284 17.3284 12 16.5 12ZM12 9C11.1716 9 10.5 8.32843 10.5 7.5C10.5 6.67157 11.1716 6 12 6C12.8284 6 13.5 6.67157 13.5 7.5C13.5 8.32843 12.8284 9 12 9Z"></path></svg>
@@ -62,7 +62,7 @@
62
62
  v-model="inputValue"
63
63
  v-if="setting.type == 'link'"
64
64
  />
65
- <color-picker
65
+ <color-input
66
66
  :label="setting.label"
67
67
  :name="setting.id"
68
68
  v-model="inputValue"
@@ -80,6 +80,7 @@
80
80
  :label="setting.label"
81
81
  :name="setting.id"
82
82
  v-model="inputValue"
83
+ :collection-id="options.collectionId"
83
84
  v-if="setting.type == 'collection_item'"
84
85
  />
85
86
  </div>
@@ -28,7 +28,7 @@
28
28
  spin
29
29
  v-if="isInProgress"
30
30
  />
31
- <icon name="check-line" v-if="isSuccess" />
31
+ <icon name="ri-check-line" v-if="isSuccess" />
32
32
  <icon name="ri-alert-line" v-if="isFail" />
33
33
  <span>{{ label }}</span>
34
34
  </span>
@@ -10,7 +10,7 @@
10
10
  :searchEnabled="true"
11
11
  :searchPlaceholder="$t(`collectionItemInput.select.searchPlaceholder`)"
12
12
  :emptyLabel="$t(`collectionItemInput.select.emptyLabel`)"
13
- :fetchList="(q) => services.collectionItem.findAll('products', { q })"
13
+ :fetchList="(q) => services.collectionItem.findAll(collectionId, { q })"
14
14
  :clearEnabled="true"
15
15
  buttonClass="h-10"
16
16
  v-model="selectedCollectionItem"
@@ -42,18 +42,21 @@
42
42
  </template>
43
43
 
44
44
  <script>
45
+ import { camelizeKeys } from '@/misc/utils'
46
+
45
47
  export default {
46
48
  name: 'CollectionItemInput',
47
49
  props: {
48
50
  label: { type: String, default: 'Label' },
49
51
  name: { type: String, default: 'image' },
50
52
  value: { default: () => null },
53
+ collectionId: { type: String },
51
54
  isFocused: { type: Boolean, default: false },
52
55
  },
53
56
  computed: {
54
57
  selectedCollectionItem: {
55
58
  get() {
56
- return this.value
59
+ return this.value === 'any' ? null : camelizeKeys(this.value)
57
60
  },
58
61
  set(collectionItem) {
59
62
  this.$emit('input', collectionItem ? { ...collectionItem } : null)
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <div class="relative">
3
+ <div
4
+ class="absolute left-3 top-2 w-6 h-6 rounded-sm border border-gray-200"
5
+ :class="{ 'bg-checkerboard': isTransparent }"
6
+ :style="{ 'background-color': styleRgbaColor }"></div>
7
+
8
+ <span class="absolute left-11 top-1.5 font-bold text-gray-900 text-lg">#</span>
9
+
10
+ <input
11
+ type="text"
12
+ :value="inputColor"
13
+ @input="updateInput"
14
+ class="py-2 pl-14 rounded bg-gray-100 text-gray-800 focus:outline-none focus:ring placeholder-gray-500 font-normal"
15
+ :class="{
16
+ 'pr-8': hasPresets,
17
+ 'pr-2': !hasPresets
18
+ }"
19
+ autocomplete="off"
20
+ minlength="4"
21
+ maxlength="8"
22
+ size="7"
23
+ />
24
+
25
+ <preset-dropdown
26
+ v-model="updatableValue"
27
+ :presets="presets"
28
+ v-if="hasPresets"
29
+ />
30
+ </div>
31
+ </template>
32
+
33
+ <script>
34
+ import PresetDropdown from './preset-dropdown.vue'
35
+
36
+ import { colorVariableToHex, colorVariableToRgb } from '@/misc/utils'
37
+
38
+ export default {
39
+ name: 'CoreInput',
40
+ components: { PresetDropdown },
41
+ props: {
42
+ value: { type: String },
43
+ presets: {
44
+ type: Array,
45
+ default: () => [],
46
+ },
47
+ },
48
+ computed: {
49
+ inputColor() {
50
+ return this.hexColor?.replaceAll('#', '')
51
+ },
52
+ hexColor() {
53
+ return colorVariableToHex(this.value)
54
+ },
55
+ rgbColor() {
56
+ return colorVariableToRgb(this.value)
57
+ },
58
+ styleRgbaColor() {
59
+ const color = this.isTransparent ? { r: 255, g: 255, b: 255 } : (this.rgbColor || { r: 0, g: 0, b: 0 })
60
+ return `rgba(${color.r}, ${color.g}, ${color.b}, 1)`
61
+ },
62
+ isTransparent() {
63
+ return this.value === ''
64
+ },
65
+ hasPresets() {
66
+ return this.presets && this.presets.length > 0
67
+ },
68
+ updatableValue: {
69
+ get() {
70
+ return this.value
71
+ },
72
+ set(color) {
73
+ this.$emit('input', color)
74
+ },
75
+ },
76
+ },
77
+ methods: {
78
+ updateInput(event) {
79
+ var value = event.target.value
80
+ if (value.length > 0) value = `#${value}`
81
+ this.$emit('input', value)
82
+ }
83
+ }
84
+ }
85
+ </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div
3
+ class="self-center cursor-pointer p-0.5 rounded-sm"
4
+ @click="selectPreset"
5
+ >
6
+ <div
7
+ class="flex items-center justify-center w-6 h-6 rounded-sm transition transform duration-200 ease-in-out hover:scale-110 select-none text-white"
8
+ :class="{
9
+ 'border border-gray-300 text-gray-800': isWhite,
10
+ 'bg-checkerboard': isTransparent,
11
+ }"
12
+ :style="{ 'background-color': hexColor }"
13
+ >
14
+ <icon name="ri-check-line" size="1rem" v-if="selected" />
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script>
20
+ import { hexToRgb, colorVariableToHex, colorVariableToRgb } from '@/misc/utils'
21
+
22
+ export default {
23
+ name: 'PresetButton',
24
+ props: {
25
+ preset: { type: String },
26
+ value: { type: String },
27
+ },
28
+ computed: {
29
+ selected() {
30
+ return !!this.value && this.value.trim().toLowerCase() === this.hexColor
31
+ },
32
+ hexColor() {
33
+ return colorVariableToHex(this.preset)
34
+ },
35
+ rgbColor() {
36
+ return colorVariableToRgb(this.preset)
37
+ },
38
+ isWhite() {
39
+ if (!this.rgbColor) return true
40
+ return this.rgbColor.r === 255 && this.rgbColor.g === 255 && this.rgbColor.b === 255
41
+ },
42
+ isTransparent() {
43
+ return this.hexColor === ''
44
+ },
45
+ borderColor() {
46
+ if (!this.selected) return 'transparent'
47
+ let color = this.isWhite ? { r: 0, g: 0, b: 0 } : this.rgbColor
48
+ return `rgba(${color.r}, ${color.g}, ${color.b}, 0.40)`
49
+ }
50
+ },
51
+ methods: {
52
+ selectPreset() {
53
+ this.$emit('input', this.hexColor)
54
+ }
55
+ }
56
+ }
57
+ </script>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <dropdown v-on="$listeners" class="absolute top-1.5 right-1">
3
+ <template v-slot:button>
4
+ <button
5
+ class="px-1 py-1 hover:text-gray-900 focus:outline-none transition-colors duration-200"
6
+ >
7
+ <icon name="ri-palette-line" size="1.25rem" />
8
+ </button>
9
+ </template>
10
+ <template v-slot:content>
11
+ <div class="flex flex-wrap gap-1">
12
+ <preset-button
13
+ v-for="preset in presets"
14
+ v-model="updatableValue"
15
+ :preset="preset"
16
+ :key="preset" />
17
+ </div>
18
+ </template>
19
+ </dropdown>
20
+ </template>
21
+
22
+ <script>
23
+ import PresetButton from './preset-button.vue'
24
+
25
+ export default {
26
+ name: 'PresetDropdown',
27
+ components: { PresetButton },
28
+ props: {
29
+ value: { type: String },
30
+ presets: {
31
+ type: Array,
32
+ default: () => [],
33
+ },
34
+ },
35
+ computed: {
36
+ updatableValue: {
37
+ get() {
38
+ return this.value
39
+ },
40
+ set(color) {
41
+ this.$emit('input', color)
42
+ },
43
+ },
44
+ }
45
+ }
46
+ </script>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <div class="pt-1">
3
+ <label
4
+ class="flex items-center justify-between font-semibold text-gray-800"
5
+ :for="name"
6
+ v-if="showLabel"
7
+ >
8
+ <span>{{ label }}</span>
9
+ <core-input :presets="presets" v-model="updatableValue" />
10
+ </label>
11
+ </div>
12
+ </template>
13
+
14
+ <script>
15
+ import { hexToRgb } from '@/misc/utils'
16
+
17
+ import CoreInput from '@/components/kit/color-input/core-input.vue'
18
+
19
+ export default {
20
+ name: 'ColorInput',
21
+ components: { CoreInput },
22
+ props: {
23
+ label: { type: String, default: 'Label' },
24
+ name: { type: String, default: 'color' },
25
+ presets: {
26
+ type: Array,
27
+ default: () => [],
28
+ },
29
+ value: { type: String },
30
+ showLabel: { type: Boolean, default: true }
31
+ },
32
+ computed: {
33
+ updatableValue: {
34
+ get() {
35
+ return this.value
36
+ },
37
+ set(color) {
38
+ this.$emit('input', color)
39
+ },
40
+ },
41
+ },
42
+ }
43
+ </script>
@@ -5,6 +5,7 @@
5
5
  :open="open"
6
6
  :placement="placement"
7
7
  class="flex"
8
+ v-on:auto-hide="close"
8
9
  >
9
10
  <div
10
11
  class="z-10 relative flex items-center focus:outline-none select-none cursor-pointer w-full"
@@ -19,7 +19,7 @@ import CheckboxInput from './checkbox-input.vue'
19
19
  import SearchInput from './search-input.vue'
20
20
  import Pagination from './pagination/index.vue'
21
21
  import PageIcon from './page-icon.vue'
22
- import ColorPicker from './color-picker.vue'
22
+ import ColorInput from './color-input.vue'
23
23
  import SimpleSelect from './simple-select.vue'
24
24
  import CollectionItemInput from './collection-item-input.vue'
25
25
  import ListItemButton from './list-item-button.vue'
@@ -44,7 +44,7 @@ Vue.component('checkbox-input', CheckboxInput)
44
44
  Vue.component('search-input', SearchInput)
45
45
  Vue.component('pagination', Pagination)
46
46
  Vue.component('page-icon', PageIcon)
47
- Vue.component('color-picker', ColorPicker)
47
+ Vue.component('color-input', ColorInput)
48
48
  Vue.component('simple-select', SimpleSelect)
49
49
  Vue.component('collection-item-input', CollectionItemInput)
50
50
  Vue.component('list-item-button', ListItemButton)
@@ -8,17 +8,17 @@
8
8
  <span v-if="isDefaultState" data-button-label>{{ defaultLabel }}</span>
9
9
 
10
10
  <span v-if="isInProgressState" class="flex items-center justify-center">
11
- <icon icon="circle-notch" name="ri-loader-4-line" spin color="#fff" />
11
+ <icon name="ri-loader-4-line" spin color="#fff" key="progress" />
12
12
  <span class="ml-2" data-button-label>{{ inProgressLabel }}</span>
13
13
  </span>
14
14
 
15
15
  <span v-if="isSuccessState" class="flex items-center justify-center">
16
- <icon icon="circle-notch" name="check-line" color="#fff" />
16
+ <icon name="ri-check-line" color="#fff" key="success" />
17
17
  <span class="ml-2" data-button-label>{{ successLabel }}</span>
18
18
  </span>
19
19
 
20
20
  <span v-if="isFailState" class="flex items-center justify-center">
21
- <icon icon="circle-notch" name="ri-alert-line" color="#fff" />
21
+ <icon name="ri-alert-line" color="#fff" key="fail" />
22
22
  <span class="ml-2" data-button-label>{{ failLabel }}</span>
23
23
  </span>
24
24
  </button>
@@ -8,8 +8,9 @@
8
8
  v-for="(tab, index) in tabs"
9
9
  :key="`tab-${index}`"
10
10
  type="button"
11
- class="text-gray-500 py-1 pb-0 px-4 block hover:text-editor-primary focus:outline-none border-b-2 border-transparent z-10"
11
+ class=" py-1 pb-0 px-4 block hover:text-editor-primary focus:outline-none border-b-2 z-10"
12
12
  :class="{
13
+ 'text-gray-500 border-transparent': index !== currentIndex,
13
14
  'text-editor-primary font-medium border-editor-primary':
14
15
  index === currentIndex,
15
16
  }"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div>
2
+ <div class="space-y-1">
3
3
  <label
4
4
  class="block font-semibold text-gray-800"
5
5
  :for="name"
@@ -20,7 +20,7 @@
20
20
  :placeholder="placeholder"
21
21
  @blur="blur()"
22
22
  @input="updateInput"
23
- class="block w-full mt-1 py-2 px-3 rounded bg-gray-100 text-gray-800 focus:outline-none focus:ring placeholder-gray-500"
23
+ class="block w-full py-2 px-3 rounded bg-gray-100 text-gray-800 focus:outline-none focus:ring placeholder-gray-500"
24
24
  autocomplete="off"
25
25
  ref="input"
26
26
  />
@@ -8,11 +8,28 @@ export const isBlank = (object) => {
8
8
  }
9
9
 
10
10
  export const camelize = (str) => {
11
+ if (!str.includes('_')) return str
11
12
  return str
12
13
  .toLowerCase()
13
14
  .replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase())
14
15
  }
15
16
 
17
+ export const camelizeKeys = (obj) => {
18
+ if (Array.isArray(obj)) {
19
+ return obj.map(v => camelizeKeys(v));
20
+ } else if (obj != null && obj.constructor === Object) {
21
+ return Object.keys(obj).reduce(
22
+ (result, key) => ({
23
+ ...result,
24
+ [camelize(key)]: camelizeKeys(obj[key]),
25
+ }),
26
+ {},
27
+ );
28
+ }
29
+ return obj;
30
+ }
31
+
32
+
16
33
  export const numberToHumanSize = (size, i18n) => {
17
34
  if (isBlank(size)) return null
18
35
 
@@ -94,16 +111,29 @@ export const omitEmpty = (obj) =>
94
111
  Object.keys(obj).forEach((key) => obj[key] === undefined && delete obj[key])
95
112
 
96
113
  export const hexToRgb = (hex) => {
97
- var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim())
114
+ if (!hex) return null
115
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex.trim())
98
116
  return result
99
117
  ? {
100
118
  r: parseInt(result[1], 16),
101
119
  g: parseInt(result[2], 16),
102
- b: parseInt(result[3], 16),
120
+ b: result[3] ? parseInt(result[3], 16) : 0,
103
121
  }
104
122
  : null
105
123
  }
106
124
 
125
+ export const colorVariableToHex = (variable) => {
126
+ if (!variable) return null
127
+ const color = (variable.startsWith('--')
128
+ ? getComputedStyle(document.body).getPropertyValue(variable)
129
+ : variable).trim().toLowerCase()
130
+ return color === 'transparent' ? '' : color
131
+ }
132
+
133
+ export const colorVariableToRgb = (variable) => {
134
+ return hexToRgb(colorVariableToHex(variable))
135
+ }
136
+
107
137
  // Static pages have absolute path ("/something") but regular pages have no leading slash
108
138
  export const formatPath = (path) => {
109
139
  return path[0] === '/' ? path : `/${path}`
@@ -18,10 +18,10 @@ const api = axios.create({
18
18
  Accept: 'application/json',
19
19
  },
20
20
  transformResponse(data) {
21
- // camelcase in JS
21
+ // for "security" (even if the Rails API already camelcases the JSON),
22
+ // we also camelcase the response in the Editor.
22
23
  // console.log('[DEBUG] API response', data)
23
24
  return data ? camelcaseObjectDeep(JSON.parse(data)) : {}
24
- // return data ? JSON.parse(data) : {} // LEGACY
25
25
  },
26
26
  })
27
27