maglevcms 1.1.6 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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