@mixd-id/web-scaffold 0.2.240706 → 0.2.250801010

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 (220) hide show
  1. package/docs/components/Dashboard.md +56 -0
  2. package/log.txt +7 -0
  3. package/package.json +27 -19
  4. package/src/components/404.vue +61 -0
  5. package/src/components/AccountIcon.vue +19 -0
  6. package/src/components/Ahref.vue +1 -1
  7. package/src/components/Alert.vue +4 -13
  8. package/src/components/ArrayList.vue +49 -0
  9. package/src/components/Article.vue +24 -30
  10. package/src/components/Button.vue +83 -169
  11. package/src/components/Card.vue +257 -0
  12. package/src/components/Carousel.vue +61 -60
  13. package/src/components/Cart.vue +192 -0
  14. package/src/components/CartIcon.vue +89 -0
  15. package/src/components/ChartBar.vue +2 -3
  16. package/src/components/Checkbox.vue +20 -11
  17. package/src/components/Checkout.vue +373 -0
  18. package/src/components/CheckoutDelivery.vue +267 -0
  19. package/src/components/CodeEditor.vue +5 -16
  20. package/src/components/CollapsiblePanel.vue +70 -0
  21. package/src/components/ColorPicker.vue +12 -5
  22. package/src/components/ColorPicker2.vue +41 -19
  23. package/src/components/ColorPicker3.vue +100 -0
  24. package/src/components/Confirm.vue +9 -7
  25. package/src/components/ContextMenu.vue +122 -206
  26. package/src/components/ContextMenuItem.vue +53 -0
  27. package/src/components/Dashboard.vue +243 -0
  28. package/src/components/Dashboard2.vue +118 -0
  29. package/src/components/DashboardComponentSelector.vue +96 -0
  30. package/src/components/DashboardConfigs.vue +202 -0
  31. package/src/components/Datepicker.vue +102 -41
  32. package/src/components/DayTimeRange.vue +3 -2
  33. package/src/components/Dropdown.vue +7 -4
  34. package/src/components/Flex.vue +14 -40
  35. package/src/components/GHeatMaps.vue +2 -2
  36. package/src/components/Grid.vue +6 -6
  37. package/src/components/HTMLEditor.vue +27 -14
  38. package/src/components/Image.vue +62 -108
  39. package/src/components/ImagePreview.vue +14 -4
  40. package/src/components/ImageUploader.vue +114 -0
  41. package/src/components/ImportModal.vue +3 -3
  42. package/src/components/Link.vue +62 -6
  43. package/src/components/List.vue +528 -403
  44. package/src/components/ListContextMenu.vue +88 -0
  45. package/src/components/ListItem.vue +6 -4
  46. package/src/components/ListPage1.vue +14 -15
  47. package/src/components/ListView.vue +5 -6
  48. package/src/components/ListViewSettings.vue +2 -2
  49. package/src/components/LogViewerItem.vue +1 -1
  50. package/src/components/MarkdownEdit.vue +128 -0
  51. package/src/components/MarkdownPreview.vue +102 -0
  52. package/src/components/MenuItem1.vue +36 -0
  53. package/src/components/Modal.vue +95 -43
  54. package/src/components/MultiDropdown.vue +124 -0
  55. package/src/components/MultilineText.vue +1 -4
  56. package/src/components/OTPField.vue +40 -26
  57. package/src/components/ObjectTree.vue +1 -1
  58. package/src/components/PageBuilder.vue +3 -3
  59. package/src/components/Paragraph.vue +1 -2
  60. package/src/components/PresetSelectorFilterItem.vue +107 -95
  61. package/src/components/Radio.vue +1 -1
  62. package/src/components/SearchModal.vue +153 -0
  63. package/src/components/Slider.vue +1 -1
  64. package/src/components/Svg.vue +1 -1
  65. package/src/components/SvgEditor.vue +173 -0
  66. package/src/components/Switch.vue +4 -5
  67. package/src/components/Table.vue +2 -2
  68. package/src/components/TableView.vue +2 -3
  69. package/src/components/TableViewHead.vue +2 -2
  70. package/src/components/Tabs.vue +1 -1
  71. package/src/components/Testimonial.vue +2 -2
  72. package/src/components/Text.vue +7 -22
  73. package/src/components/TextEditor.vue +3 -3
  74. package/src/components/TextWithTag.vue +61 -30
  75. package/src/components/Textarea.vue +16 -22
  76. package/src/components/Textbox.vue +9 -19
  77. package/src/components/Timepicker.vue +25 -15
  78. package/src/components/Toast.vue +5 -3
  79. package/src/components/TreeMenu.vue +122 -0
  80. package/src/components/TreeView.vue +15 -10
  81. package/src/components/TreeView2.vue +38 -0
  82. package/src/components/TreeViewItem.vue +58 -29
  83. package/src/components/TreeViewItem2.vue +55 -0
  84. package/src/components/Uploader.vue +45 -0
  85. package/src/components/Video.vue +119 -0
  86. package/src/components/VirtualGrid.vue +24 -7
  87. package/src/components/VirtualTable.vue +363 -128
  88. package/src/configs/dashboard/data-table.js +9 -0
  89. package/src/configs/web-page-builder.js +118 -0
  90. package/src/directives/intersect.js +26 -0
  91. package/src/hooks/device.js +14 -0
  92. package/src/index.js +62 -107
  93. package/src/mixin/component.js +151 -67
  94. package/src/themes/default/index.js +118 -159
  95. package/src/utils/dashboard.js +22 -962
  96. package/src/utils/helpers.cjs +635 -0
  97. package/src/utils/helpers.js +91 -60
  98. package/src/utils/helpers.mjs +245 -12
  99. package/src/utils/importer.js +22 -3
  100. package/src/utils/list.mjs +1509 -0
  101. package/src/utils/preset-selector.cjs +1455 -0
  102. package/src/utils/preset-selector.js +489 -95
  103. package/src/utils/preset-selector.mjs +59 -20
  104. package/src/utils/queue.js +63 -0
  105. package/src/utils/web.mjs +120 -0
  106. package/src/utils/wss.js +38 -36
  107. package/src/utils/wss.mjs +24 -19
  108. package/src/widgets/AhrefSetting.vue +16 -13
  109. package/src/widgets/ArticleSetting.vue +15 -27
  110. package/src/widgets/BackgroundColorSetting.vue +153 -0
  111. package/src/widgets/BorderColorSetting.vue +57 -0
  112. package/src/widgets/BotEditor/BotEditorActions.vue +3 -2
  113. package/src/widgets/BotEditor/BotEditorSettings.vue +21 -0
  114. package/src/widgets/BotEditor.vue +35 -15
  115. package/src/widgets/ButtonSetting.vue +12 -13
  116. package/src/widgets/CarouselSetting.vue +33 -45
  117. package/src/widgets/CartSetting.vue +46 -0
  118. package/src/widgets/CheckoutSetting.vue +46 -0
  119. package/src/widgets/CollapsiblePanelSetting.vue +46 -0
  120. package/src/widgets/ColumnSelector.vue +29 -5
  121. package/src/widgets/ComponentSetting.vue +1 -1
  122. package/src/widgets/ComponentSetting2.vue +145 -236
  123. package/src/widgets/ComponentSetting3.vue +1 -1
  124. package/src/widgets/ContactForm.vue +3 -3
  125. package/src/widgets/ContactFormSetting.vue +41 -30
  126. package/src/widgets/Dashboard/BarChart.vue +47 -11
  127. package/src/widgets/Dashboard/BarChartSetting.vue +1 -1
  128. package/src/widgets/Dashboard/DataTable.vue +125 -0
  129. package/src/widgets/Dashboard/DataTableSetting.vue +243 -0
  130. package/src/widgets/Dashboard/DatasourceSelector.vue +1 -1
  131. package/src/widgets/Dashboard/Doughnut.vue +49 -7
  132. package/src/widgets/Dashboard/DoughnutSetting.vue +2 -2
  133. package/src/widgets/Dashboard/Metric.vue +78 -19
  134. package/src/widgets/Dashboard/MetricSetting.vue +81 -28
  135. package/src/widgets/Dashboard/Pie.vue +55 -6
  136. package/src/widgets/Dashboard/PieSetting.vue +1 -1
  137. package/src/widgets/Dashboard/PolarArea.vue +49 -7
  138. package/src/widgets/Dashboard/PolarAreaSetting.vue +1 -1
  139. package/src/widgets/Dashboard/SharingModal.vue +4 -5
  140. package/src/widgets/Dashboard/ViewSelector.vue +2 -2
  141. package/src/widgets/Dashboard/VirtualTableSetting.vue +121 -184
  142. package/src/widgets/{Dashboard.vue → Dashboard0.vue} +426 -343
  143. package/src/widgets/EmbeddedVideoSetting.vue +7 -5
  144. package/src/widgets/FAQ.vue +16 -3
  145. package/src/widgets/FAQSetting.vue +53 -47
  146. package/src/widgets/FeatureList.vue +3 -0
  147. package/src/widgets/FeatureListSetting.vue +112 -102
  148. package/src/widgets/FlexSetting.vue +83 -106
  149. package/src/widgets/GridSetting.vue +71 -196
  150. package/src/widgets/Header2.vue +34 -71
  151. package/src/widgets/Header2Setting.vue +95 -179
  152. package/src/widgets/HeaderSetting.vue +16 -18
  153. package/src/widgets/IconListSetting.vue +69 -65
  154. package/src/widgets/ImageSetting.vue +33 -60
  155. package/src/widgets/LinkSetting.vue +60 -37
  156. package/src/widgets/LinkSettingModal.vue +173 -0
  157. package/src/widgets/LogViewer.vue +1 -1
  158. package/src/widgets/MarginSetting.vue +2 -2
  159. package/src/widgets/MenuEditor.vue +1 -1
  160. package/src/widgets/MenuItem1Setting.vue +78 -0
  161. package/src/widgets/ModalSetting.vue +42 -44
  162. package/src/widgets/MultiValueSetting.vue +2 -2
  163. package/src/widgets/MultiValueSetting2.vue +78 -45
  164. package/src/widgets/OGSettingModal.vue +103 -0
  165. package/src/widgets/PaddingSetting.vue +2 -2
  166. package/src/widgets/ParagraphSetting.vue +16 -13
  167. package/src/widgets/PositionSetting.vue +209 -0
  168. package/src/widgets/PresetBar.vue +359 -210
  169. package/src/widgets/PresetBarPivot.vue +31 -19
  170. package/src/widgets/PresetSelector.vue +29 -17
  171. package/src/widgets/SearchModalSetting.vue +70 -0
  172. package/src/widgets/Share.vue +1 -2
  173. package/src/widgets/ShareSetting.vue +67 -60
  174. package/src/widgets/StyleSetting.vue +365 -150
  175. package/src/widgets/TestimonialSetting.vue +97 -88
  176. package/src/widgets/TextBlockSetting.vue +16 -13
  177. package/src/widgets/UserActionBuilder/UserActionConsole.vue +30 -10
  178. package/src/widgets/UserActionBuilder/UserActionOutput.vue +2 -2
  179. package/src/widgets/UserActionBuilder/UserActionOutputReply.vue +64 -87
  180. package/src/widgets/UserActionBuilder/UserActionProps.vue +3 -3
  181. package/src/widgets/UserActionBuilder.vue +4 -16
  182. package/src/widgets/WebComponentSelector.vue +15 -11
  183. package/src/widgets/WebLayoutSelector.vue +41 -270
  184. package/src/widgets/WebPageBuilder.vue +1019 -707
  185. package/src/widgets/WebPageBuilder2.vue +7 -7
  186. package/src/widgets/WebPageBuilder4/ButtonSetting.vue +0 -8
  187. package/src/widgets/WebPageBuilder4/CarouselSetting.vue +63 -7
  188. package/src/widgets/WebPageBuilder4/FlexAlignSetting.vue +3 -3
  189. package/src/widgets/WebPageBuilder4/FlexSetting.vue +1 -10
  190. package/src/widgets/WebPageBuilder4/MultiValueSetting.vue +2 -2
  191. package/src/widgets/WebPageBuilder4/PropertySetting.vue +0 -7
  192. package/src/widgets/WebPageBuilder4/WebPageComponentSelector.vue +1 -7
  193. package/src/widgets/WebPageBuilder4.vue +289 -575
  194. package/src/widgets/WebPageSelector.vue +1 -1
  195. package/src/widgets/YoutubeVideoSetting.vue +16 -13
  196. package/tailwind.config.js +3 -35
  197. package/docs/schema/user-action.json +0 -266
  198. package/src/App.vue +0 -25
  199. package/src/components/SearchButton.vue +0 -57
  200. package/src/entry-client.js +0 -27
  201. package/src/entry-server.js +0 -73
  202. package/src/events/event.js +0 -2
  203. package/src/main.js +0 -29
  204. package/src/mixin/website.js +0 -121
  205. package/src/router.js +0 -57
  206. package/src/widgets/MobileMenu.vue +0 -182
  207. package/src/widgets/WebPageBuilder4/ActionSetting.vue +0 -158
  208. package/src/widgets/WebPageBuilder4/ColorSetting.vue +0 -63
  209. package/src/widgets/WebPageBuilder4/DataSetting.vue +0 -92
  210. package/src/widgets/WebPageBuilder4/FontSizeSetting.vue +0 -76
  211. package/src/widgets/WebPageBuilder4/LinkSetting.vue +0 -68
  212. package/src/widgets/WebPageBuilder4/MobileMenuSetting.vue +0 -106
  213. package/src/widgets/WebPageBuilder4/Setting.vue +0 -73
  214. package/src/widgets/WebPageBuilder4/StyleSetting.vue +0 -77
  215. package/src/widgets/WebPageBuilder4/SvgSetting.vue +0 -207
  216. package/src/widgets/WebPageBuilder4/TextTransformSetting.vue +0 -70
  217. package/src/widgets/WebPageBuilder4/WebPageDataEdit.vue +0 -121
  218. package/test.json +0 -22
  219. /package/src/widgets/{Header1.vue → Header0.vue} +0 -0
  220. /package/src/widgets/{Header1Setting.vue → Header0Setting.vue} +0 -0
@@ -1,13 +1,15 @@
1
1
  <template>
2
- <div class="flex-1 flex items-center justify-center" v-if="state === 2">
2
+ <div class="flex-1 flex items-center justify-center" v-if="readyState === 2">
3
3
  <svg class="animate-spin aspect-square w-[48px]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
4
4
  </div>
5
5
  <div :class="$style.comp" v-else-if="page">
6
6
 
7
7
  <div class="flex-1 flex flex-col bg-base-400">
8
8
 
9
- <div class="p-3 pr-5 sticky top-0 flex justify-center gap-4 bg-base-400 dark:bg-base-300 border-b-[1px] border-text-50">
10
- <div class="flex-1 flex flex-row gap-4 items-center">
9
+ <div class="p-3 pr-5 flex justify-center gap-4 border-b-[1px] border-border-50">
10
+ <div class="flex-1 flex flex-row gap-4 items-center"
11
+ @click.meta="log(page)"
12
+ @click.alt="log(layout)">
11
13
 
12
14
  <button ref="close" class=" p-1"
13
15
  @click="close">
@@ -23,27 +25,18 @@
23
25
  <template #start>
24
26
  <div class="flex flex-row items-center mr-2">
25
27
  <button @click="reloadIframe" class="p-3">
26
- <svg width="14" height="14" viewBox="0 0 24 24" class="fill-text" xmlns="http://www.w3.org/2000/svg">
28
+ <svg width="14" height="14" viewBox="0 0 24 24" class="fill-primary" xmlns="http://www.w3.org/2000/svg">
27
29
  <path d="M3.75 12C3.75 7.44365 7.44365 3.75 12 3.75C14.7802 3.75 17.1982 5.12612 18.6816 7.24467L16.5022 7.23828C16.088 7.23707 15.7512 7.57187 15.75 7.98608C15.7488 8.4003 16.0836 8.73706 16.4978 8.73828L19.9491 8.74839C19.9817 8.75065 20.0147 8.75076 20.0477 8.74868L20.4978 8.75C20.6971 8.75058 20.8884 8.67182 21.0296 8.53111C21.1707 8.39039 21.25 8.19929 21.25 8L21.25 4C21.25 3.58579 20.9142 3.25 20.5 3.25C20.0858 3.25 19.75 3.58579 19.75 4L19.75 6.16237C17.9894 3.79113 15.2004 2.25 12 2.25C6.61522 2.25 2.25 6.61522 2.25 12C2.25 17.3848 6.61522 21.75 12 21.75C15.8354 21.75 19.0799 19.5367 20.6716 16.3338C20.856 15.9628 20.7047 15.5127 20.3338 15.3284C19.9628 15.144 19.5127 15.2953 19.3284 15.6662C17.9747 18.3902 15.2321 20.25 12 20.25C7.44365 20.25 3.75 16.5563 3.75 12Z"/>
28
30
  </svg>
29
31
  </button>
30
- <select v-model="store.zoomLevel" :class="$style.zoomLevel" @change="resize()">
31
- <optgroup label="Zoom Level">
32
- <option value="fit">Fit</option>
33
- <option value="125%">125%</option>
34
- <option value="100%">100%</option>
35
- <option value="75%">75%</option>
36
- <option value="50%">50%</option>
37
- </optgroup>
38
- </select>
39
32
  </div>
40
33
  </template>
41
34
  <template #end>
42
- <div class="flex flex-row gap-4 ml-3">
43
- <CopyToClipboard :value="computedIframeSrc" @copied="toast($t('Copied'))">
35
+ <div class="flex flex-row gap-5 ml-3">
36
+ <CopyToClipboard :value="computedIframeSrc" @copied="toast($t('URL Copied'))">
44
37
  <svg width="14" height="14" class="fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM352 32.491a15.88 15.88 0 0 1 7.431 4.195l51.882 51.883A15.885 15.885 0 0 1 415.508 96H352V32.491zM288 464c0 8.822-7.178 16-16 16H48c-8.822 0-16-7.178-16-16V144c0-8.822 7.178-16 16-16h80v240c0 26.51 21.49 48 48 48h112v48zm128-96c0 8.822-7.178 16-16 16H176c-8.822 0-16-7.178-16-16V48c0-8.822 7.178-16 16-16h144v72c0 13.2 10.8 24 24 24h72v240z"/></svg>
45
38
  </CopyToClipboard>
46
- <a type="button" class="w-[21px]" :href="computedIframeSrc" target="_blank">
39
+ <a type="button" class="w-[24px]" :href="computedIframeSrc" target="_blank">
47
40
  <svg width="12" height="12" class="fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M497.6,0,334.4.17A14.4,14.4,0,0,0,320,14.57V47.88a14.4,14.4,0,0,0,14.69,14.4l73.63-2.72,2.06,2.06L131.52,340.49a12,12,0,0,0,0,17l23,23a12,12,0,0,0,17,0L450.38,101.62l2.06,2.06-2.72,73.63A14.4,14.4,0,0,0,464.12,192h33.31a14.4,14.4,0,0,0,14.4-14.4L512,14.4A14.4,14.4,0,0,0,497.6,0ZM432,288H416a16,16,0,0,0-16,16V458a6,6,0,0,1-6,6H54a6,6,0,0,1-6-6V118a6,6,0,0,1,6-6H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V304A16,16,0,0,0,432,288Z"/></svg>
48
41
  </a>
49
42
  </div>
@@ -51,9 +44,9 @@
51
44
  </Textbox>
52
45
  </div>
53
46
 
54
- <Tabs :items="previewModes" v-model="store.previewMode" variant="button" @change="updateIframe" />
47
+ <Tabs :items="previewModes" v-model="store.previewMode" variant="button" @change="updateIframe" class="rounded-full" />
55
48
 
56
- <div v-if="pageHistory" class="flex flex-row border-[1px] border-text-200 bg-text-50 rounded-full overflow-hidden">
49
+ <div v-if="pageHistory" class="flex flex-row border-[1px] border-border-200 bg-text-50 rounded-full overflow-hidden">
57
50
  <button type="button" class="p-2 px-3 group"
58
51
  :class="pageHistory.canUndo.value && pageHistory.history.value.length > 2 ? 'hover:bg-primary' : 'opacity-50 cursor-not-allowed'"
59
52
  :disabled="!(pageHistory.canUndo.value && pageHistory.history.value.length > 2)"
@@ -76,27 +69,27 @@
76
69
  class="w-[70px] rounded-full p-1"
77
70
  :state="canSave ? 1 : -1"
78
71
  @click="save">{{ $t('Save') }}</Button>
72
+
79
73
  </div>
80
74
  </div>
81
75
 
82
76
  <div class="flex-1 flex flex-row">
83
77
 
84
- <div class="flex flex-row bg-base-400 dark:bg-base-300 border-r-[1px] border-text-50"
78
+ <div ref="leftPanel" class="flex flex-row"
85
79
  :style="section1Style">
86
80
 
87
81
  <div class="flex-1 flex flex-col overflow-y-auto">
88
82
 
89
- <div class="flex flex-col items-center border-b-[1px] border-text-100" @click="currentArea = 'header'">
83
+ <div class="flex flex-col px-3 border-b-[1px] border-border-100 overflow-x-auto no-scrollbar pt-1">
90
84
  <Tabs :items="tabItems"
91
- class="pt-1"
92
85
  v-model="store.tabIndex" />
93
86
  </div>
94
87
 
95
- <div v-if="store.tabIndex === 1" class="flex-1 overflow-y-auto flex flex-col gap-4 p-6" @click="currentArea = 'pageinfo'">
88
+ <div v-if="store.tabIndex === 1" class="flex-1 overflow-y-auto flex flex-col gap-4 p-6">
96
89
 
97
90
  <div class="flex flex-row gap-4" v-if="!page.isSystem">
98
91
  <div class="flex flex-col gap-1">
99
- <label class="text-text-400 w-[40px]">{{ $t('Status') }}</label>
92
+ <small class="text-text-400 w-[40px]">{{ $t('Status') }}</small>
100
93
  <Dropdown v-model="page.status">
101
94
  <option :value="-1">{{ $t('Disabled') }}</option>
102
95
  <option :value="0">{{ $t('Draft') }}</option>
@@ -105,52 +98,55 @@
105
98
  </div>
106
99
 
107
100
  <div class="flex-1">
108
- <label class="text-text-400">{{ $t('Path')}}</label>
101
+ <small class="text-text-400">{{ $t('Path')}}</small>
109
102
  <Textbox v-model="page.path" />
110
103
  </div>
111
104
  </div>
112
105
 
113
106
  <div class="flex flex-col gap-1">
114
- <label class="text-text-400">{{ $t('Title')}}</label>
107
+ <small class="text-text-400">{{ $t('Title')}}</small>
115
108
  <Textbox v-model="page.title" />
116
109
  </div>
117
110
 
118
111
  <div class="flex flex-col gap-1">
119
- <label class="text-text-400">{{ $t('Description')}}</label>
112
+ <small class="text-text-400">{{ $t('Description')}}</small>
120
113
  <Textarea v-model="page.description" />
121
114
  </div>
122
115
 
123
116
  <div class="flex flex-col gap-1">
124
- <label class="text-text-400">{{ $t('Keywords')}}</label>
117
+ <small class="text-text-400">{{ $t('Keywords')}}</small>
125
118
  <Textbox v-model="page.keywords" />
126
119
  </div>
127
120
 
128
121
  <div>
129
122
  <div class="mb-1">
130
- <label class="text-text-400">{{ $t('OG')}}</label>
123
+ <small class="text-text-400">{{ $t('OG')}}</small>
131
124
  </div>
132
- <div class="flex flex-row gap-4 bg-base-500 rounded-lg p-2 cursor-pointer"
133
- @click="$refs.ogModal.open({ ...this.page.og })"
125
+ <div class="flex flex-row gap-2 bg-base-500 border-[1px] border-border-200 rounded-lg p-1 cursor-pointer"
126
+ @click="$refs.ogModal.open({ ...this.page.og }, og => page.og = og)"
134
127
  v-if="page.og && Object.keys(page.og).length > 0">
135
128
  <div>
136
- <Image :src="imageUrl(page.og)" class="w-[40px] aspect-square bg-text-50" />
129
+ <Image :src="page.og.image" class="w-[40px] aspect-square bg-text-50" />
137
130
  </div>
138
131
  <div class="flex-1 flex flex-col">
139
- <small class="text-text-400 pointer-events-none">{{ page.og.type }}</small>
140
- <label class="pointer-events-none line-clamp-2">{{ page.og.title }}</label>
141
- <small class="text-text-400 pointer-events-none line-clamp-1">{{ page.og.url }}</small>
132
+ <label class="text-sm text-ellipsis-nowrap">
133
+ {{ page.og.title }}
134
+ </label>
135
+ <small class="text-sm text-ellipsis-nowrap">
136
+ {{ page.og.description }}
137
+ </small>
142
138
  </div>
143
139
  </div>
144
- <div v-else class="bg-base-500 p-2 text-center border-[1px] border-text-100 rounded-lg cursor-pointer"
145
- @click="$refs.ogModal.open({ ...this.page.og })">
140
+ <div v-else class="bg-base-500 p-2 text-center border-[1px] border-border-100 rounded-lg cursor-pointer"
141
+ @click="$refs.ogModal.open(page.og, og => page.og = og)">
146
142
  Set OG
147
143
  </div>
148
144
  </div>
149
145
 
150
146
  <div>
151
147
  <div class="flex flex-row gap-2 mb-1">
152
- <label class="flex-1 text-text-400">{{ $t('JSON-LD')}}</label>
153
- <button type="button" class="text-primary" @click="$refs.ldjsonModal.open({})">
148
+ <small class="flex-1 text-text-400">{{ $t('JSON-LD')}}</small>
149
+ <button type="button" class="text-primary text-sm" @click="$refs.ldjsonModal.open({})">
154
150
  {{ $t('Add') }}
155
151
  </button>
156
152
  </div>
@@ -166,46 +162,97 @@
166
162
  </div>
167
163
  </template>
168
164
  </ListItem>
169
- <div v-else class="bg-base-500 p-2 text-center border-[1px] border-text-100 rounded-lg cursor-pointer text-text-300">
165
+ <div v-else class="bg-base-500 p-2 text-center border-[1px] border-border-100 rounded-lg cursor-pointer text-text-300">
170
166
  No JSON-LD set
171
167
  </div>
172
168
  </div>
173
169
 
174
170
  <div>
175
- <label class="flex-1 text-text-400">{{ $t('No Index')}}</label>
176
- <Dropdown v-model="page.noIndex" class="max-w-[150px]">
171
+ <small class="flex-1 text-text-400">{{ $t('No Index')}}</small>
172
+ <Dropdown v-model="page.noIndex" class="w-full max-w-[300px]">
177
173
  <option value="none">None</option>
178
174
  <option value="robots">All</option>
179
175
  <option value="googlebot">Google Bot</option>
180
176
  </Dropdown>
181
177
  </div>
182
178
 
183
- <div class="h-[1px] bg-text-100 my-3"></div>
179
+ </div>
180
+
181
+ <div v-else-if="store.tabIndex === 2" class="flex-1 overflow-y-auto p-6 flex flex-col gap-6">
184
182
 
185
183
  <div>
186
- <label class="flex-1 text-text-400">{{ $t('Require Login')}}</label>
187
- <Dropdown v-model="page.requireLogin" class="max-w-[150px]">
188
- <option value="none">None</option>
189
- <option value="popup">Popup</option>
190
- <option value="page">Page</option>
191
- </Dropdown>
184
+ <div class="flex flex-row gap-1 items-end cursor-pointer">
185
+ <small class="flex-1 text-text-400 text-overflow-ellipsis">{{ $t('Components')}}</small>
186
+ <button type="button" class="text-primary text-sm flex flex-row items-center gap-1"
187
+ @click="openComponentSelector({ callback:(component) => page.components.push(component) })">
188
+ {{ $t('Add')}}
189
+ </button>
190
+ </div>
191
+
192
+ <div class="flex flex-col gap-10">
193
+ <TreeView class="mt-2"
194
+ v-model="page.components"
195
+ :selected-item="currentItem"
196
+ @add="(items) => openComponentSelector({ callback:(component) => items.push(component) })"
197
+ @duplicate="duplicate"
198
+ @change="pageHistory.commit()">
199
+ <template #default="{ item }">
200
+ <div class="flex-1 text-ellipsis whitespace-nowrap overflow-hidden"
201
+ :class="!item.props.enabled ? 'line-through' : ''"
202
+ @click="select(item.uid)">
203
+ {{ item.props.name ?? item.type }}
204
+ </div>
205
+ </template>
206
+ </TreeView>
207
+ </div>
192
208
  </div>
193
209
 
194
- <div class="h-[1px] my-3"></div>
210
+ </div>
211
+
212
+ <div v-else-if="store.tabIndex === 3" class="flex-1 overflow-y-auto p-6">
213
+
214
+ <div class="p-6 flex flex-col gap-2" v-if="Array.isArray(page.datasource) && page.datasource.length > 0">
215
+ <div v-for="(ds, index) in page.datasource"
216
+ class="p-3 border-[1px] border-border-50 rounded-lg flex flex-row items-start gap-2">
217
+ <div class="flex-1 flex flex-col" @click="$refs.webDatasourceSelector.open({ _index:index, ...ds })">
218
+ <label>{{ ds.name }}</label>
219
+ </div>
220
+ <button type="button"
221
+ @click="removeDatasource(index)">
222
+ <svg width="14" height="14" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>
223
+ </button>
224
+ </div>
225
+ </div>
226
+ <div v-else>
227
+ <div class="p-6 text-center">
228
+ <small class="text-text-400">{{ $t('No Datasource')}}</small>
229
+ </div>
230
+ </div>
231
+
232
+ <div class="text-center" v-if="useDatasource">
233
+ <button type="button" class="text-primary"
234
+ @click="$refs.webDatasourceSelector.create()">
235
+ Add Datasource
236
+ </button>
237
+ </div>
238
+
239
+ <WebDatasourceSelector ref="webDatasourceSelector"
240
+ @apply="addDatasource"
241
+ :use-datasource="useDatasource" />
195
242
 
196
243
  </div>
197
244
 
198
- <div v-else-if="store.tabIndex === 2" class="flex-1 overflow-y-auto p-6 flex flex-col gap-6">
245
+ <div v-else-if="store.tabIndex === 4" class="flex-1 overflow-y-auto flex flex-col">
199
246
 
200
- <div>
201
- <div class="flex flex-row gap-2" @click="currentArea = 'layout'">
202
- <label class="flex-1 text-text-400">{{ $t('Layout')}}</label>
203
- <button type="button" class="text-primary" @click="store.layoutMode = !store.layoutMode">
204
- {{ store.layoutMode ? 'Hide Layout' : 'Show Layout' }}
247
+ <div class="p-6">
248
+ <div class="flex flex-row items-end gap-2">
249
+ <small class="flex-1 text-text-400">{{ $t('Layout')}}</small>
250
+ <button type="button" class="text-primary text-sm" @click="store.layoutMode = !store.layoutMode">
251
+ {{ store.layoutMode ? 'Hide Layout' : 'Edit Layout' }}
205
252
  </button>
206
253
  </div>
207
- <div class="mt-2">
208
- <div class="cursor-pointer bg-text-50 p-2 border-[1px] border-text-200 flex flex-row items-center rounded-lg"
254
+ <div class="mt-1">
255
+ <div class="cursor-pointer bg-text-50 p-2 border-[1px] border-border-200 flex flex-row items-center rounded-lg"
209
256
  @click="(e) => $refs.layoutSelector.open(e.target)">
210
257
  <div class="flex-1 pointer-events-none">
211
258
  {{ layout ? (layout.name ?? layout.title) : 'None' }}
@@ -218,19 +265,21 @@
218
265
  </div>
219
266
 
220
267
  <ContextMenu ref="layoutSelector">
221
- <div class="flex flex-col min-w-[260px] divide-y divide-text-50">
268
+ <div class="flex flex-col min-w-[260px] divide-y divide-border-50">
222
269
 
223
- <div class="p-3" @click="page.layoutId = null;">None</div>
270
+ <div @click="page.layoutId = null;" :class="appStyle.menuItem">None</div>
224
271
 
225
272
  <div v-for="layout in layouts"
226
- class="p-3"
273
+ :class="appStyle.menuItem"
227
274
  @click="page.layoutId = layout.id;pageHistory.commit()">
228
275
  {{ layout.name ?? layout.title }}
229
276
  </div>
230
277
 
231
- <div class="flex flex-row" v-if="canManageLayout">
232
- <div class="p-3 flex-1 cursor-pointer text-primary" @click="$refs.webLayoutSelector.create()">Create New</div>
233
- <div class="p-3 cursor-pointer text-primary" @click="$refs.webLayoutSelector.open(layout.id)">Manage</div>
278
+ <div class="flex flex-row">
279
+ <button type="button" class="p-3 flex-1 cursor-pointer text-primary"
280
+ @click="$refs.webLayoutSelector.create({})">
281
+ Create New...
282
+ </button>
234
283
  </div>
235
284
 
236
285
  </div>
@@ -238,57 +287,26 @@
238
287
  </div>
239
288
  </div>
240
289
 
241
- <div v-if="store.layoutMode && layout" class="flex flex-row gap-2"
242
- @click="currentArea = 'style'">
243
- <label class="flex-1 text-text-400">{{ $t('Style')}}</label>
244
- <button type="button" class="text-primary"
245
- @click="store.selectedComponent = [ 'style' ]">Edit Style</button>
246
- </div>
247
-
248
- <div v-if="store.layoutMode && layout" @click="currentArea = 'headers'">
249
- <div class="flex flex-row gap-1 items-center cursor-pointer">
250
- <svg v-if="!expanded['headers']" width="12" height="12" @click="expanded['headers'] = !expanded['headers']" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 512"><path d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"/></svg>
251
- <svg v-else width="12" height="12" @click="expanded['headers'] = !expanded['headers']" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"/></svg>
252
- <strong class="flex-1 text-text-400 line-clamp-1" @click="expanded['headers'] = !expanded['headers']">{{ $t('Header')}}</strong>
253
- <button type="button" class="text-primary flex flex-row items-center gap-1"
254
- @click="openComponentSelector({ items:layout.headers, isLayout:true })">
255
- <svg width="16" height="16" class="fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M376 232H216V72c0-4.42-3.58-8-8-8h-32c-4.42 0-8 3.58-8 8v160H8c-4.42 0-8 3.58-8 8v32c0 4.42 3.58 8 8 8h160v160c0 4.42 3.58 8 8 8h32c4.42 0 8-3.58 8-8V280h160c4.42 0 8-3.58 8-8v-32c0-4.42-3.58-8-8-8z"/></svg>
256
- {{ $t('Add')}}
257
- </button>
258
- </div>
259
- <div v-if="expanded['headers']" class="flex flex-col gap-10">
260
- <TreeView class="mt-2"
261
- v-model="layoutHeaders"
262
- :selected-item="currentItem"
263
- @add="(items) => openComponentSelector({ items })">
264
- <template #default="{ item }">
265
- <div class="flex-1 text-ellipsis whitespace-nowrap overflow-hidden"
266
- :class="!item.props.enabled ? 'line-through' : ''"
267
- @click="select(item.uid)">
268
- {{ item.name ?? item.type }}
269
- </div>
270
- </template>
271
- </TreeView>
290
+ <div v-if="layout" class="flex flex-col gap-6 p-6 border-t-[1px] border-border-50">
291
+ <div>
292
+ <small class="text-text-400">Name</small>
293
+ <Textbox v-model="layout.title"/>
272
294
  </div>
273
- </div>
274
295
 
275
- <div @click="currentArea = 'components'">
276
- <div class="flex flex-row gap-1 items-center cursor-pointer">
277
- <svg v-if="!expanded['components']" width="12" height="12" @click="expanded['components'] = !expanded['components']" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 512"><path d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"/></svg>
278
- <svg v-else width="12" height="12" @click="expanded['components'] = !expanded['components']" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"/></svg>
279
- <strong class="flex-1 text-text-400 line-clamp-1" @click="expanded['components'] = !expanded['components']">{{ $t('Components')}}</strong>
280
- <button type="button" class="text-primary flex flex-row items-center gap-1"
281
- @click="openComponentSelector({ items:page.components }); currentArea = ''">
282
- <svg width="16" height="16" class="fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M376 232H216V72c0-4.42-3.58-8-8-8h-32c-4.42 0-8 3.58-8 8v160H8c-4.42 0-8 3.58-8 8v32c0 4.42 3.58 8 8 8h160v160c0 4.42 3.58 8 8 8h32c4.42 0 8-3.58 8-8V280h160c4.42 0 8-3.58 8-8v-32c0-4.42-3.58-8-8-8z"/></svg>
283
- {{ $t('Add')}}
284
- </button>
285
- </div>
296
+ <div>
297
+ <div class="flex flex-row gap-1 items-end cursor-pointer">
298
+ <small class="flex-1 text-text-400 text-overflow-ellipsis">{{ $t('Headers')}}</small>
299
+ <button type="button" class="text-primary text-sm flex flex-row items-center gap-1"
300
+ @click="openComponentSelector({ callback:(component) => layout.headers.push(component) })">
301
+ {{ $t('Add')}}
302
+ </button>
303
+ </div>
286
304
 
287
- <div v-if="expanded['components']" class="flex flex-col gap-10">
288
305
  <TreeView class="mt-2"
289
- v-model="page.components"
306
+ v-model="layout.headers"
290
307
  :selected-item="currentItem"
291
- @add="(items) => openComponentSelector({ items })"
308
+ @add="(items) => openComponentSelector({ callback:(component) => items.push(component) })"
309
+ @duplicate="duplicate"
292
310
  @change="pageHistory.commit()">
293
311
  <template #default="{ item }">
294
312
  <div class="flex-1 text-ellipsis whitespace-nowrap overflow-hidden"
@@ -300,25 +318,21 @@
300
318
  </TreeView>
301
319
  </div>
302
320
 
303
- </div>
304
-
305
- <div v-if="store.layoutMode && layout" @click="currentArea = 'footers'">
306
- <div class="flex flex-row gap-1 items-center cursor-pointer">
307
- <svg v-if="!expanded['footers']" width="12" height="12" @click="expanded['footers'] = !expanded['footers']" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 512"><path d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"/></svg>
308
- <svg v-else width="12" height="12" @click="expanded['footers'] = !expanded['footers']" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"/></svg>
309
- <strong class="flex-1 text-text-400 line-clamp-1" @click="expanded['footers'] = !expanded['footers']">{{ $t('Footer')}}</strong>
310
- <button type="button" class="text-primary flex flex-row items-center gap-1"
311
- @click="openComponentSelector({ items:layoutFooters, isLayout:true })">
312
- <svg width="16" height="16" class="fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M376 232H216V72c0-4.42-3.58-8-8-8h-32c-4.42 0-8 3.58-8 8v160H8c-4.42 0-8 3.58-8 8v32c0 4.42 3.58 8 8 8h160v160c0 4.42 3.58 8 8 8h32c4.42 0 8-3.58 8-8V280h160c4.42 0 8-3.58 8-8v-32c0-4.42-3.58-8-8-8z"/></svg>
313
- {{ $t('Add')}}
314
- </button>
315
- </div>
321
+ <div>
322
+ <div class="flex flex-row gap-1 items-end cursor-pointer">
323
+ <small class="flex-1 text-text-400 text-overflow-ellipsis">{{ $t('Footers')}}</small>
324
+ <button type="button" class="text-primary text-sm flex flex-row items-center gap-1"
325
+ @click="openComponentSelector({ callback:(component) => layout.footers.push(component) })">
326
+ {{ $t('Add')}}
327
+ </button>
328
+ </div>
316
329
 
317
- <div v-if="expanded['footers']" class="flex flex-col gap-10">
318
330
  <TreeView class="mt-2"
319
- v-model="layoutFooters"
331
+ v-model="layout.footers"
320
332
  :selected-item="currentItem"
321
- @add="(items) => openComponentSelector({ items })">
333
+ @add="(items) => openComponentSelector({ callback:(component) => items.push(component) })"
334
+ @duplicate="duplicate"
335
+ @change="pageHistory.commit()">
322
336
  <template #default="{ item }">
323
337
  <div class="flex-1 text-ellipsis whitespace-nowrap overflow-hidden"
324
338
  :class="!item.props.enabled ? 'line-through' : ''"
@@ -328,160 +342,136 @@
328
342
  </template>
329
343
  </TreeView>
330
344
  </div>
331
- </div>
332
345
 
333
- <div v-if="store.layoutMode && layout">
334
- <small class="text-text-400">{{ $t('Updating layout will affect all pages using this layout, proceed with caution.') }}</small>
335
- </div>
336
-
337
- <WebComponentSelector ref="webPageComponentSelector"
338
- component-src="template.load"
339
- :components="availableComponents"
340
- @apply="addComponent"
341
- dismissable="true"
342
- @dismiss="$refs.webPageComponentSelector.close()" />
343
-
344
- </div>
345
-
346
- <div v-else-if="store.tabIndex === 3" class="flex-1 overflow-y-auto p-6" @click="currentArea = 'datasource'">
347
-
348
- <div class="p-6 flex flex-col gap-2" v-if="Array.isArray(page.datasource) && page.datasource.length > 0">
349
- <div v-for="(ds, index) in page.datasource"
350
- class="p-3 border-[1px] border-text-50 rounded-lg flex flex-row items-start gap-2">
351
- <div class="flex-1 flex flex-col" @click="$refs.webDatasourceSelector.open({ _index:index, ...ds })">
352
- <label>{{ ds.name }}</label>
346
+ <div>
347
+ <div class="flex flex-row gap-1 items-end cursor-pointer">
348
+ <small class="flex-1 text-text-400 text-overflow-ellipsis">{{ $t('Styles')}}</small>
349
+ <button type="button" class="text-primary text-sm flex flex-row items-center gap-1"
350
+ @click="select('style')">
351
+ {{ $t('Edit Style')}}
352
+ </button>
353
353
  </div>
354
- <button type="button"
355
- @click="removeDatasource(index)">
356
- <svg width="14" height="14" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>
357
- </button>
358
- </div>
359
- </div>
360
- <div v-else>
361
- <div class="p-6 text-center">
362
- <label class="text-text-400">{{ $t('No Datasource')}}</label>
363
354
  </div>
364
355
  </div>
365
356
 
366
- <div class="text-center" v-if="useDatasource">
367
- <button type="button" class="text-primary"
368
- @click="$refs.webDatasourceSelector.create()">
369
- Add Datasource
357
+ </div>
358
+
359
+ <div v-else-if="store.tabIndex === 7" class="flex-1 flex flex-col">
360
+ <div class="flex flex-row justify-end p-4">
361
+ <button type="button" @click="newChat()">
362
+ <svg width="19" height="19" class="fill-primary hover:fill-primary-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256C397.4 512 512 397.4 512 256S397.4 0 256 0zM352 280H280V352c0 13.2-10.8 24-23.1 24C242.8 376 232 365.2 232 352V280H160C146.8 280 136 269.2 136 256c0-13.2 10.8-24 24-24H232V160c0-13.2 10.8-24 24-24C269.2 136 280 146.8 280 160v72h72C365.2 232 376 242.8 376 256C376 269.2 365.2 280 352 280z"/></svg>
370
363
  </button>
371
364
  </div>
365
+ <div class="flex-1 overflow-y-auto">
372
366
 
373
- <WebDatasourceSelector ref="webDatasourceSelector"
374
- @apply="addDatasource"
375
- :use-datasource="useDatasource" />
376
-
377
- </div>
367
+ <div class="flex flex-col gap-4 p-4">
378
368
 
379
- </div>
369
+ <div v-for="message in chat.messages"
370
+ :class="$style.chat + ' ' + (message.direction === 1 ? $style.chatIn : $style.chatOut)">
380
371
 
381
- <div :class="$style.resize1"
382
- @mousedown="(e) => $util.dragResize(e, resize1)"></div>
372
+ <p>{{ message.body }}</p>
383
373
 
384
- <Modal ref="ogModal" width="480" height="520">
385
- <template v-slot:head>
386
- <div class="relative p-5">
387
- <h3>{{ $t('Edit OG')}}</h3>
388
- <div class="absolute top-0 right-0 p-2">
389
- <button type="button" class="p-2" @click="$refs.ogModal.close()">
390
- <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
391
- <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
392
- </svg>
393
- </button>
394
- </div>
395
- </div>
396
- </template>
397
- <template v-slot:foot="{ context }">
398
- <div class="p-5">
399
- <Button type="button" class="w-[100px]"
400
- @click="Object.assign(this.page.og, context);$refs.ogModal.close()">
401
- OK
402
- </Button>
403
- </div>
404
- </template>
405
- <template #default="{ context }">
406
- <div class="flex-1 p-5 flex flex-col gap-4">
407
- <div class="flex flex-row gap-4">
408
- <div>
409
- <label class="text-text-400">Type</label>
410
- <Dropdown v-model="context.type" class="mt-1 w-[120px]">
411
- <option value="website">Website</option>
412
- <option value="article">Article</option>
413
- </Dropdown>
414
- </div>
415
- <div class="flex-1">
416
- <label class="text-text-400">Title</label>
417
- <Textbox v-model="context.title" class="mt-1" />
374
+ <div v-if="message.direction !== 1" class="flex text-text-300 flex-row items-center gap-2 self-stretch pl-3">
375
+ <small class="text-xs text-text-300">{{ message.llmOutputTokenCount }} token</small>
376
+ &bull;
377
+ <small class="text-xs text-text-300">{{ message.llmEllapsedMs }} ms</small>
378
+ <div class="flex-1"></div>
379
+ <button type="button" class="text-xs group hover:text-primary" v-if="(message.attachments ?? [])[0]" @click="chatSet(message.attachments[0])">
380
+ <svg width="9" height="9" class="inline fill-text-300 group-hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M176 480C148.6 480 128 457.6 128 432v-352c0-25.38 20.4-47.98 48.01-47.98c8.686 0 17.35 2.352 25.02 7.031l288 176C503.3 223.8 512 239.3 512 256s-8.703 32.23-22.97 40.95l-288 176C193.4 477.6 184.7 480 176 480z"/></svg>
381
+ Apply
382
+ </button>
383
+ </div>
418
384
  </div>
419
- </div>
420
385
 
421
- <div>
422
- <label class="text-text-400">Description</label>
423
- <Textarea v-model="context.description" class="mt-1" rows="3" />
424
386
  </div>
425
387
 
426
- <div>
427
- <label class="text-text-400">Url</label>
428
- <Textbox v-model="context.url" class="mt-1" />
429
- </div>
388
+ <pre class="text-xs break-all whitespace-pre-wrap">
430
389
 
431
- <div>
432
- <div class="flex flex-row">
433
- <label class="text-text-400 flex-1">Image</label>
434
- <button type="button" class="text-primary"
435
- @click="$refs.ogImage.edit()">Change Image</button>
390
+ </pre>
391
+
392
+ </div>
393
+ <div class="p-2">
394
+ <div class="flex flex-col gap-2">
395
+ <div class="flex flex-row gap-2 items-start">
396
+ <div class="w-[22px]"></div>
397
+ <div class="flex-1 flex flex-row flex-wrap gap-2">
398
+ <div v-for="(attachment, idx) in chat.attachments">
399
+ <Image :src="attachment.image"
400
+ @click="chat.attachments.splice(idx, 1)"
401
+ class="w-[48px] h-[48px] border-[1px] rounded-lg border-border-200" />
402
+ </div>
403
+ </div>
404
+ </div>
405
+
406
+ <div class="flex flex-row items-start gap-2">
407
+ <div class="pt-1">
408
+ <Button variant="minimal" class="p-0 mt-1" type="button" @click="$refs.chatFile.click()">
409
+ <svg width="19" height="19" class="fill-primary hover:fill-primary-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256C397.4 512 512 397.4 512 256S397.4 0 256 0zM352 280H280V352c0 13.2-10.8 24-23.1 24C242.8 376 232 365.2 232 352V280H160C146.8 280 136 269.2 136 256c0-13.2 10.8-24 24-24H232V160c0-13.2 10.8-24 24-24C269.2 136 280 146.8 280 160v72h72C365.2 232 376 242.8 376 256C376 269.2 365.2 280 352 280z"/></svg>
410
+ </Button>
411
+ <input type="file" accept="image/*" multiple ref="chatFile" class="hidden" @change="onChatUpload" />
412
+ </div>
413
+ <div class="flex-1">
414
+ <Textarea v-model="chat.input" rows="5" class="max-h-[60vh]">
415
+ <template #end>
416
+ <div class="p-2">
417
+ <Button ref="chatBtn" variant="minimal" class="p-0" type="button" @click="chatApply">
418
+ <svg width="19" height="19" class="fill-primary hover:fill-primary-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM372.5 276.5l-144 88C224.7 366.8 220.3 368 216 368c-13.69 0-24-11.2-24-24V168C192 155.3 202.2 144 216 144c4.344 0 8.678 1.176 12.51 3.516l144 88C379.6 239.9 384 247.6 384 256C384 264.4 379.6 272.1 372.5 276.5z"/></svg>
419
+ </Button>
420
+ </div>
421
+ </template>
422
+ </Textarea>
423
+ </div>
436
424
  </div>
437
- <div class="mt-1">
438
- <Image ref="ogImage" :src="imageUrl(context)" class="w-[80px] aspect-square"
439
- :editable="true"
440
- @change="(base64, file) => { uploadImage(file).then((res) => context.imageUrl = res.name); }" />
425
+
426
+ <div class="flex flex-row justify-end px-3 gap-2 text-text-300 text-xs">
427
+ <div class="text-text-300">{{ totalChatTokens }} tokens</div>
428
+ &bull;
429
+ <div class="text-text-300">{{ chat.model }}</div>
441
430
  </div>
442
431
  </div>
443
432
  </div>
444
- </template>
445
- </Modal>
446
-
447
- <Modal ref="ldjsonModal" width="600" height="480">
448
- <template v-slot:head>
449
- <div class="relative p-5">
450
- <h3>JSON-LD</h3>
451
- <div class="absolute top-0 right-0 p-2">
452
- <button type="button" class="p-2" @click="$refs.ldjsonModal.close()">
453
- <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
454
- <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
455
- </svg>
456
- </button>
457
- </div>
458
- </div>
459
- </template>
460
- <template #foot="{ context }">
461
- <div class="p-5">
462
- <Button type="button" class="w-[100px]"
463
- @click="saveLdjson(context)">
464
- Save
465
- </Button>
466
- </div>
467
- </template>
468
- <template #default="{ context }">
469
- <div class="flex-1 p-5">
470
- <Textarea v-model="context.code" rows="10" ></Textarea>
471
- </div>
472
- </template>
473
- </Modal>
433
+ </div>
434
+
435
+ </div>
436
+
437
+ <div :class="$style.resize1"
438
+ @mousedown="(e) => $util.dragResize(e, resize1)"></div>
474
439
 
475
440
  </div>
476
441
 
477
- <div class="flex-1 bg-base-300 dark:bg-base-400 relative" :class="previewClass" ref="preview"
478
- @click="currentArea = 'preview'">
479
- <div class="flex flex-row p-2">
480
- <select class="p-1 text-sm cursor-pointer bg-text-50 outline-none"
481
- v-model="store.previewViewType"
482
- @change="resize">
483
- <option v-for="_type in previewViewTypes" :value="_type.value">{{ _type.text }}</option>
442
+ <div ref="preview" class="flex-1 relative" :class="previewClass">
443
+
444
+ <div class="flex flex-row gap-4 items-center p-2">
445
+ <select v-model="store.zoomLevel" :class="$style.zoomLevel">
446
+ <optgroup label="Zoom Level">
447
+ <option value="fit">Fit</option>
448
+ <option value="125%">125%</option>
449
+ <option value="100%">100%</option>
450
+ <option value="75%">75%</option>
451
+ <option value="50%">50%</option>
452
+ </optgroup>
484
453
  </select>
454
+
455
+ <div class="flex-1"></div>
456
+
457
+ <div class="flex flex-row border-[1px] border-border-200 divide-x divide-border-50" :class="$style.zoomLevel">
458
+ <Radio v-for="_type in viewTypes" :value="_type.value" v-model="store.viewType" :custom="true">
459
+ <template #default="">
460
+ <button v-if="_type.value === ''" type="button" class="w-[30px] flex items-center justify-center">
461
+ <svg width="14" height="14" :class="_type.value === store.viewType ? 'fill-primary' : 'fill-text'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M320 0H64C37.5 0 16 21.5 16 48v416C16 490.5 37.5 512 64 512h256c26.5 0 48-21.5 48-48v-416C368 21.5 346.5 0 320 0zM240 447.1C240 456.8 232.8 464 224 464H159.1C151.2 464 144 456.8 144 448S151.2 432 160 432h64C232.8 432 240 439.2 240 447.1zM304 384h-224V64h224V384z"/></svg>
462
+ </button>
463
+ <button v-if="_type.value === 'md:'" type="button" class="w-[30px] flex items-center justify-center">
464
+ <svg width="14" height="14" :class="_type.value === store.viewType ? 'fill-primary' : 'fill-text'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M384 .0001H64c-35.35 0-64 28.65-64 64v384c0 35.35 28.65 63.1 64 63.1h320c35.35 0 64-28.65 64-63.1v-384C448 28.65 419.3 .0001 384 .0001zM288 448c0 8.837-7.163 16-15.1 16H175.1c-8.837 0-15.1-7.163-15.1-16s7.163-16 15.1-16h96C280.8 432 288 439.2 288 448zM384 384H64v-320h320V384z"/></svg>
465
+ </button>
466
+ <button v-if="_type.value === 'xl:'" type="button" class="w-[30px] flex items-center justify-center">
467
+ <svg width="14" height="14" :class="_type.value === store.viewType ? 'fill-primary' : 'fill-text'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M528 0h-480C21.5 0 0 21.5 0 48v320C0 394.5 21.5 416 48 416h192L224 464H152C138.8 464 128 474.8 128 488S138.8 512 152 512h272c13.25 0 24-10.75 24-24s-10.75-24-24-24H352L336 416h192c26.5 0 48-21.5 48-48v-320C576 21.5 554.5 0 528 0zM512 288H64V64h448V288z"/></svg>
468
+ </button>
469
+ <button v-if="_type.value === '2xl:'" type="button" class="w-[30px] flex items-center justify-center">
470
+ <svg width="14" height="14" :class="_type.value === store.viewType ? 'fill-primary' : 'fill-text'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M512 448H127.1C110.3 448 96 462.3 96 479.1S110.3 512 127.1 512h384C529.7 512 544 497.7 544 480S529.7 448 512 448zM592 0h-544C21.5 0 0 21.5 0 48v320C0 394.5 21.5 416 48 416h544c26.5 0 48-21.5 48-48v-320C640 21.5 618.5 0 592 0zM576 352H64v-288h512V352z"/></svg>
471
+ </button>
472
+ </template>
473
+ </Radio>
474
+ </div>
485
475
  </div>
486
476
 
487
477
  <div class="p-6">
@@ -490,74 +480,97 @@
490
480
  </div>
491
481
  </div>
492
482
 
493
- <div v-if="currentItem" class="flex flex-col bg-base-400 dark:bg-base-300 border-l-[1px] border-text-50"
494
- @click="currentArea = 'properties'"
495
- :style="section3Style">
496
-
497
- <div class="px-6 py-4 flex flex-row items-center gap-2">
498
- <Textbox v-if="currentItem.type !== 'Style'"
499
- v-model="currentItem.props.name"
500
- :placeholder="currentItem.type"
501
- class="bg-base-500 rounded-full flex-1"
502
- item-class="p-0 px-2"
503
- variant="minimal">
504
- <template #start>
505
- <div class="pl-3">
506
- <Switch v-model="currentItem.props.enabled"/>
507
- </div>
508
- </template>
509
- </Textbox>
483
+ <div v-if="currentItem" ref="rightPanel" class="flex flex-row"
484
+ :style="section3Style" @click.meta="log(currentItem)">
510
485
 
511
- <div v-else class="px-3">
512
- <h4>Style</h4>
513
- </div>
514
- </div>
486
+ <div :class="$style.resize3"
487
+ @mousedown="(e) => $util.dragResize(e, resize3)"></div>
515
488
 
516
- <div v-if="false" class="px-4 bg-base-300 pt-2 relative">
517
- <Tabs v-model="store.viewType" :items="viewTypes" variant="minimal">
518
- <template #tab="{ item }">
519
- <div v-if="item.value === ''" class="px-6 p-2 border-[1px] border-b-0 relative top-[1px] rounded-t-md overflow-hidden"
520
- :class="store.viewType === item.value ? 'bg-base-400 border-text-50' : 'border-transparent'">
521
- <div v-if="store.viewType === item.value" class="absolute top-0 left-0 right-0 h-[2px] bg-primary"></div>
522
- Mobile
523
- </div>
524
- <div v-else-if="item.value === 'md:'" class="px-6 p-2 border-[1px] border-b-0 relative top-[1px] rounded-t-md overflow-hidden"
525
- :class="store.viewType === item.value ? 'bg-base-400 border-text-50' : 'border-transparent'">
526
- <div v-if="store.viewType === item.value" class="absolute top-0 left-0 right-0 h-[2px] bg-primary"></div>
527
- Tablet
528
- </div>
529
- </template>
530
- </Tabs>
531
- </div>
489
+ <div class="flex-1 flex flex-col relative"
490
+ ref="rightPane">
532
491
 
533
- <div class="flex-1 flex flex-row border-t-[1px] border-text-50">
492
+ <TransitionGroup name="openltr" tag="div" class="flex-1 flex flex-col">
534
493
 
535
- <div :class="$style.resize3"
536
- @mousedown="(e) => $util.dragResize(e, resize3)"></div>
494
+ <div v-if="extRightPane === null" class="flex-1 flex flex-col divide-y divide-border-50 overflow-y-auto">
537
495
 
538
- <div class="flex-1 flex flex-col gap-6 overflow-y-auto p-6">
496
+ <div v-if="currentItem.type !== 'Style'" class="px-6 py-4 flex flex-row gap-5 items-start">
497
+ <div>
498
+ <small class="text-text-400">Enabled</small>
499
+ <Switch v-model="currentItem.props.enabled"/>
500
+ </div>
501
+ <div v-if="currentItem.type !== 'Style'" class="flex-1">
502
+ <small class="text-text-400">Name</small>
503
+ <Textbox v-model="currentItem.props.name"
504
+ :placeholder="currentItem.type"
505
+ class="bg-base-500 flex-1"
506
+ variant="minimal" />
507
+ </div>
508
+ <div v-else class="px-3">
509
+ <h4>Style</h4>
510
+ </div>
511
+ </div>
539
512
 
540
- <div>
541
513
  <component :is="`${currentItem.type}Setting`"
542
514
  :item="currentItem"
543
515
  :view-type="store.viewType"
516
+ :view-index="viewIndex"
544
517
  :view-types="viewTypes"
545
518
  ref="settingComponent"
546
519
  @change="pageHistory.commit()"
547
520
  @postMessageToIframe="onPostMessageToIframe"/>
521
+
522
+ <div class="p-6 py-4" v-if="debugMode">
523
+ <small class="text-text-400">Debug ID</small>
524
+ <Textbox v-model="currentItem.props.debugId" maxlength="5" />
525
+ </div>
526
+
527
+ <br />
528
+ <br />
529
+ <br />
530
+
548
531
  </div>
549
532
 
550
- <div class="p-5 text-center" v-if="useTemplateCreator && currentItem.type !== 'Style'">
551
- <button type="button" class="text-primary"
552
- @click="openTemplateCreator">
553
- Save as Template
554
- </button>
533
+ <div v-else-if="extRightPane?.type" class="flex-1 flex flex-col divide-y divide-border-50 overflow-y-auto">
534
+
535
+ <div class="px-6 py-4 flex flex-row gap-5 items-center">
536
+ <div class="flex flex-col">
537
+ <small class="text-text-400">&nbsp;</small>
538
+ <button type="button" @click="extRightPane = null">
539
+ <svg width="14" height="14" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M447.1 256C447.1 273.7 433.7 288 416 288H109.3l105.4 105.4c12.5 12.5 12.5 32.75 0 45.25C208.4 444.9 200.2 448 192 448s-16.38-3.125-22.62-9.375l-160-160c-12.5-12.5-12.5-32.75 0-45.25l160-160c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L109.3 224H416C433.7 224 447.1 238.3 447.1 256z"/></svg>
540
+ </button>
541
+ </div>
542
+ <div>
543
+ <small class="text-text-400">Enabled</small>
544
+ <Switch v-model="extRightPane.props.enabled"/>
545
+ </div>
546
+ <div v-if="extRightPane.type !== 'Style'" class="flex-1">
547
+ <small class="text-text-400">Name</small>
548
+ <Textbox v-model="currentItem.props.name"
549
+ :placeholder="extRightPane.type"
550
+ class="bg-base-500 flex-1"
551
+ variant="minimal" />
552
+ </div>
553
+ <div v-else class="px-3">
554
+ <h4>Style</h4>
555
+ </div>
556
+ </div>
557
+
558
+ <component :is="`${extRightPane.type}Setting`"
559
+ :item="extRightPane"
560
+ :view-type="store.viewType"
561
+ :view-index="viewIndex"
562
+ :view-types="viewTypes"
563
+ ref="settingComponent"
564
+ @change="pageHistory.commit()"
565
+ @postMessageToIframe="onPostMessageToIframe"/>
566
+
567
+ <br />
568
+ <br />
569
+ <br />
555
570
 
556
- <WebTemplateCreator :="useTemplateCreator"
557
- ref="templateCreator"
558
- @save="templateCreatorAfterSave" />
559
571
  </div>
560
- </div>
572
+
573
+ </TransitionGroup>
561
574
 
562
575
  </div>
563
576
 
@@ -568,7 +581,44 @@
568
581
  </div>
569
582
 
570
583
  <div class="absolute">
571
- <WebLayoutSelector ref="webLayoutSelector" :layouts="layouts" @apply="createLayout"/>
584
+ <WebLayoutSelector ref="webLayoutSelector" @apply="createLayout"/>
585
+
586
+ <WebComponentSelector ref="webPageComponentSelector"
587
+ :component-src="`${this.controller}.load-templates`"
588
+ :components="availableComponents"
589
+ @apply="addComponent"
590
+ dismissable="true"
591
+ @dismiss="$refs.webPageComponentSelector.close()" />
592
+
593
+ <OGSettingModal ref="ogModal" />
594
+
595
+ <Modal ref="ldjsonModal" width="600" height="480">
596
+ <template v-slot:head>
597
+ <div class="relative p-5">
598
+ <h3>JSON-LD</h3>
599
+ <div class="absolute top-0 right-0 p-2">
600
+ <button type="button" class="p-2" @click="$refs.ldjsonModal.close()">
601
+ <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
602
+ <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
603
+ </svg>
604
+ </button>
605
+ </div>
606
+ </div>
607
+ </template>
608
+ <template #foot="{ context }">
609
+ <div class="p-5">
610
+ <Button type="button" class="w-[100px]"
611
+ @click="saveLdjson(context)">
612
+ Save
613
+ </Button>
614
+ </div>
615
+ </template>
616
+ <template #default="{ context }">
617
+ <div class="flex-1 p-5">
618
+ <Textarea v-model="context.code" rows="10" ></Textarea>
619
+ </div>
620
+ </template>
621
+ </Modal>
572
622
  </div>
573
623
 
574
624
  </div>
@@ -578,14 +628,123 @@
578
628
 
579
629
  import throttle from "lodash/throttle";
580
630
  import md5 from "md5";
581
- import {copyToClipboard, createFormData, getClipboardData} from "../utils/helpers.mjs";
631
+ import {createFormData, invokeAfterIdle} from "../utils/helpers.mjs";
582
632
  import {ref} from 'vue'
583
633
  import {useManualRefHistory} from "@vueuse/core";
584
634
  import axios from "axios";
585
635
  import {useRouter} from "vue-router";
636
+ import WebLayoutSelector from "./WebLayoutSelector.vue";
637
+ import WebComponentSelector from "./WebComponentSelector.vue";
638
+ import defaultConfig from "../configs/web-page-builder.js"
639
+ import OGSettingModal from "./OGSettingModal.vue";
640
+
641
+ const patchPageCache = {}
642
+
643
+ const fontFamilies = {
644
+ '"Anton", sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Anton&display=swap');`,
645
+ 'Dosis, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Dosis:wght@400;700&display=swap');`,
646
+ 'Lato, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap');`,
647
+ 'Merriweather, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&display=swap');`,
648
+ 'Montserrat, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');`,
649
+ '"Noto Sans", sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;800&display=swap');`,
650
+ 'Oswald, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;700&display=swap');`,
651
+ 'Oxygen, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@400;700&display=swap');`,
652
+ 'Poppins, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');`,
653
+ 'Quantico, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Quantico:wght@400;700&display=swap"');`,
654
+ 'Raleway, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@400;700&display=swap');`,
655
+ '"Reddit Sans", sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Reddit+Sans:wght@400;700&display=swap');`,
656
+ '"Roboto", sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');`,
657
+ '"Roboto Slab", serif': `@import url('https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;800&display=swap');`,
658
+ 'Volkhov, serif': `@import url('https://fonts.googleapis.com/css2?family=Volkhov:wght@400;700&display=swap');`,
659
+ '"Work Sans", serif': `@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;700&display=swap');`,
660
+ }
661
+
662
+ const defaultMedia = {
663
+ ":root": {
664
+ "--spacing-1": [],
665
+ "--spacing-2": [],
666
+ "--spacing-3": [],
667
+ "--spacing-4": [],
668
+ "--spacing-5": [],
669
+ "--spacing-6": [],
670
+ "--spacing-7": [],
671
+ "--spacing-8": [],
672
+ "--spacing-9": [],
673
+ "--spacing-10": [],
674
+ "--spacing-11": [],
675
+ "--spacing-12": []
676
+ },
677
+ "*": {
678
+ "font-family": [ "Poppins, sans-serif" ]
679
+ },
680
+ "html, .html": {
681
+ "font-size": [],
682
+ "--base-300": [ "rgb(235, 235, 235)" ],
683
+ "--base-400": [ "rgb(245, 245, 245)" ],
684
+ "--base-500": [ "rgb(255, 255, 255)" ],
685
+ "--primary-100": [],
686
+ "--primary-200": [],
687
+ "--primary-300": [],
688
+ "--primary-400": [],
689
+ "--primary-500": [ "rgb(0, 0, 0)" ],
690
+ "--primary-600": [ "rgb(15, 15, 15)" ],
691
+ "--primary-700": [],
692
+ "--primary-800": [],
693
+ "--primary-900": [],
694
+ "--secondary-100": [],
695
+ "--secondary-200": [],
696
+ "--secondary-300": [],
697
+ "--secondary-400": [],
698
+ "--secondary-500": [],
699
+ "--secondary-600": [],
700
+ "--secondary-700": [],
701
+ "--secondary-800": [],
702
+ "--secondary-900": [],
703
+ "--text-100": [],
704
+ "--text-200": [],
705
+ "--text-300": [],
706
+ "--text-400": [],
707
+ "--text-500": [ "rgb(33, 33, 33)" ],
708
+ "--text-600": [],
709
+ "--text-700": [],
710
+ "--text-800": [],
711
+ "--text-900": []
712
+ }
713
+ }
714
+
715
+ function collectFontImports(styles) {
716
+ const usedFonts = new Set()
717
+ const media = styles?.media || {}
718
+
719
+ for (const selector in media) {
720
+ const rules = media[selector]
721
+ if (!rules || !rules['font-family']) continue
722
+
723
+ const values = rules['font-family']
724
+ if (!Array.isArray(values)) continue
725
+
726
+ for (const val of values) {
727
+ if (!val) continue
728
+ if (fontFamilies[val]) {
729
+ usedFonts.add(val)
730
+ }
731
+ }
732
+ }
733
+
734
+ return Array.from(usedFonts)
735
+ .map(font => fontFamilies[font])
736
+ .join('\n')
737
+ }
738
+
739
+ function indent(str, spaces = 2) {
740
+ const pad = ' '.repeat(spaces)
741
+ return str.split('\n').map(line => pad + line).join('\n')
742
+ }
586
743
 
587
744
  export default{
588
745
 
746
+ components: {OGSettingModal, WebComponentSelector, WebLayoutSelector},
747
+
589
748
  setup(){
590
749
  const page = ref({})
591
750
  const pageHistory = useManualRefHistory(page, { clone:true })
@@ -598,78 +757,34 @@ export default{
598
757
 
599
758
  props: {
600
759
 
601
- /**
602
- * Can add/remove layout
603
- */
604
760
  canManageLayout: {
605
761
  type: Boolean,
606
762
  default: false
607
763
  },
608
764
 
609
- /**
610
- * Exclude components
611
- * @param {Array} components
612
- * @param {String} components[]
613
- */
765
+ controller: String,
766
+
614
767
  excludeComponents: {
615
768
  type: Array,
616
769
  default: []
617
770
  },
618
771
 
619
- /**
620
- * Add more components
621
- * @param {Array} components
622
- * @param {String} components[]
623
- */
624
772
  moreComponents: {
625
773
  type: Array,
626
774
  default: []
627
775
  },
628
776
 
629
- /**
630
- * Persist web page builder state
631
- */
632
- store: {
633
- type: Object,
634
- default: {
635
- version: '0.0.999',
636
- layoutMode: false,
637
- selectedComponent: null, // [ 'uid|style', 'style|headers|components|footers' ]
638
- tabIndex: 2,
639
- viewType: '',
640
- zoomLevel: 'fit',
641
- width: [ 320, 320 ]
642
- }
643
- },
644
-
645
- src:{
646
- type: [ String, Array ],
647
- default: 'page.open'
648
- },
649
-
650
- saveSrc:{
651
- type: [ String, Array ],
652
- default: 'page.save'
653
- },
654
-
655
- /**
656
- * @param {Object} config
657
- * @param {String} config.method
658
- * @param {String} config.url
659
- */
660
777
  uploadConfig: {
661
- type: Object,
662
- required: true
778
+ type: Object
663
779
  },
664
780
 
665
- /**
666
- * Enable dynamic datasource for page
667
- * @param {Object} config
668
- * @param {String} config.url
669
- */
781
+ uploadImageFn: Function,
782
+
670
783
  useDatasource: undefined,
671
784
 
672
- useTemplateCreator: undefined
785
+ useTemplateCreator: undefined,
786
+
787
+ presetKey: String
673
788
 
674
789
  },
675
790
 
@@ -708,27 +823,6 @@ export default{
708
823
  this.loadDatasource()
709
824
  },
710
825
 
711
- cleanItem(item){
712
-
713
- delete item.uid
714
- delete item.enabled
715
-
716
- if(item.props){
717
- for(let key in item.props){
718
- if(Array.isArray(item.props[key]) &&
719
- (item.props[key].length === 0 || [ '[{},{}]', '[]', '["",""]', '[""]' ].includes(JSON.stringify(item.props[key])))){
720
- delete item.props[key]
721
- }
722
- }
723
- }
724
-
725
- if(Array.isArray(item.items)){
726
- for(let i in item.items){
727
- this.cleanItem(item.items[i])
728
- }
729
- }
730
- },
731
-
732
826
  close(){
733
827
  this.$emit('close')
734
828
  },
@@ -760,7 +854,13 @@ export default{
760
854
  return Array.isArray(component.props[key]) ? component.props[key].join(' ') : ''
761
855
  })
762
856
  .filter(_ => _)
763
- .join(' ')
857
+ .join(' '),
858
+
859
+ style: this.styleClasses.reduce((res, cur) => {
860
+ if(Array.isArray(component.props[cur]))
861
+ res[cur] = component.props[cur]
862
+ return res
863
+ }, {})
764
864
  }
765
865
 
766
866
  for(let key in component.props){
@@ -779,44 +879,136 @@ export default{
779
879
  }
780
880
  })
781
881
 
782
- if(Array.isArray(component.items)){
783
- instance.items = component.items.map((_) => this.createComponentInstance(_)).filter(_=>_)
882
+ if(Array.isArray(component.items)){
883
+ instance.items = component.items.map((_) => this.createComponentInstance(_)).filter(_=>_)
884
+ }
885
+
886
+ if(Array.isArray(component.items2)){
887
+ instance.items2 = component.items2.map((_) => this.createComponentInstance(_)).filter(_=>_)
888
+ }
889
+
890
+ if(Array.isArray(component.items3)){
891
+ instance.items3 = component.items3.map((_) => this.createComponentInstance(_)).filter(_=>_)
892
+ }
893
+
894
+ if(component.props && Array.isArray(component.props.items)){
895
+ instance.items = component.props.items
896
+ }
897
+
898
+ if(component.slots){
899
+ const slots = {}
900
+ for(let key in component.slots){
901
+ slots[key] = component.slots[key].map((_) => this.createComponentInstance(_)).filter(_=>_)
902
+ }
903
+ instance.slots = slots
904
+ }
905
+
906
+ return instance
907
+ },
908
+
909
+ createLayout(layout){
910
+ this.useSocket().send(`${this.controller}.create-layout`, layout)
911
+ .then(layout => {
912
+ this.loadLayouts()
913
+ this.page.layoutId = layout.id
914
+ this.$refs.webLayoutSelector.close()
915
+ })
916
+ .catch(err => this.alert(err))
917
+ },
918
+
919
+ createStyleSheet(styles, breakpoints = {
920
+ md: '768px',
921
+ lg: '1024px',
922
+ xl: '1280px'
923
+ }){
924
+ const mediaBuckets = {
925
+ base: [],
926
+ md: [],
927
+ lg: [],
928
+ xl: []
929
+ }
930
+
931
+ const media = styles?.media || {}
932
+
933
+ for (const selector in media) {
934
+ const rules = media[selector]
935
+ if (!rules || Object.keys(rules).length === 0) continue
936
+
937
+ const ruleSets = {
938
+ base: [],
939
+ md: [],
940
+ lg: [],
941
+ xl: []
942
+ }
943
+
944
+ for (const prop in rules) {
945
+ const values = rules[prop]
946
+ if (!Array.isArray(values)) continue
947
+
948
+ if (values[0] !== undefined) ruleSets.base.push(`${prop}: ${values[0]};`)
949
+ if (values[1] !== undefined) ruleSets.md.push(`${prop}: ${values[1]};`)
950
+ if (values[2] !== undefined) ruleSets.lg.push(`${prop}: ${values[2]};`)
951
+ if (values[3] !== undefined) ruleSets.xl.push(`${prop}: ${values[3]};`)
952
+ }
953
+
954
+ for (const size in ruleSets) {
955
+ if (ruleSets[size].length === 0) continue
956
+ mediaBuckets[size].push(
957
+ `${selector} {\n ${ruleSets[size].join('\n ')}\n}`
958
+ )
959
+ }
960
+ }
961
+
962
+ let css = ''
963
+
964
+ const fontImports = collectFontImports(styles, fontFamilies)
965
+ if (fontImports) {
966
+ css += fontImports + '\n\n'
784
967
  }
785
968
 
786
- if(component.props && Array.isArray(component.props.items)){
787
- instance.items = component.props.items
969
+ const mediaImports = styles?.imports || []
970
+ if (Array.isArray(mediaImports)) {
971
+ css += mediaImports.join('\n') + '\n\n'
788
972
  }
789
973
 
790
- return instance
791
- },
974
+ if (mediaBuckets.base.length) {
975
+ css += mediaBuckets.base.join('\n\n') + '\n\n'
976
+ }
792
977
 
793
- createLayout(layout){
794
- this.page.layoutId = layout.id
795
- },
978
+ if (mediaBuckets.md.length) {
979
+ css += `@media (min-width: ${breakpoints.md}) {\n${indent(mediaBuckets.md.join('\n\n'))}\n}\n\n`
980
+ }
796
981
 
797
- copy(){
798
- if(!this.currentItem) return
799
- if(![ 'components', 'headers', 'footers' ].includes(this.currentArea)) return
982
+ if (mediaBuckets.lg.length) {
983
+ css += `@media (min-width: ${breakpoints.lg}) {\n${indent(mediaBuckets.lg.join('\n\n'))}\n}\n\n`
984
+ }
800
985
 
801
- const copyItem = JSON.parse(JSON.stringify(this.currentItem))
802
- this.cleanItem(copyItem)
986
+ if (mediaBuckets.xl.length) {
987
+ css += `@media (min-width: ${breakpoints.xl}) {\n${indent(mediaBuckets.xl.join('\n\n'))}\n}\n\n`
988
+ }
803
989
 
804
- copyToClipboard(JSON.stringify(copyItem))
805
- .then(() => this.toast('Copied to clipboard'))
990
+ return css.trim()
806
991
  },
807
992
 
808
- openTemplateCreator(){
809
- const copyItem = JSON.parse(JSON.stringify(this.currentItem))
810
- this.cleanItem(copyItem)
993
+ createInstances(){
994
+ if(!this.page.instances || typeof this.page.instances !== 'object' || Array.isArray(this.page.instances))
995
+ this.page.instances = {}
996
+ this.page.instances.components = (this.page.components ?? []).map((_) => this.createComponentInstance(_)).filter(_=>_)
997
+
998
+ if(this.layout){
999
+ if(!this.layout.instances || typeof this.layout.instances !== 'object' || Array.isArray(this.layout.instances))
1000
+ this.layout.instances = {}
1001
+ this.layout.instances.headers = (this.layout.headers ?? []).map((_) => this.createComponentInstance(_)).filter(_=>_)
1002
+ this.layout.instances.footers = (this.layout.footers ?? []).map((_) => this.createComponentInstance(_)).filter(_=>_)
811
1003
 
812
- this.$refs.templateCreator.open({
813
- data: copyItem
814
- })
1004
+ this.layout.instances.stylesheet = this.createStyleSheet(this.layout.styles)
1005
+ }
815
1006
  },
816
1007
 
817
- templateCreatorAfterSave(){
818
- this.toast('Template created')
819
- this.$refs.webPageComponentSelector.load()
1008
+ duplicate(parent, item){
1009
+ const newItem = JSON.parse(JSON.stringify(item))
1010
+ this.setUid(newItem)
1011
+ parent.push(newItem)
820
1012
  },
821
1013
 
822
1014
  findCompByUid(uid, components){
@@ -893,7 +1085,7 @@ export default{
893
1085
  },
894
1086
 
895
1087
  listen(){
896
- this.socketEmit2('page.subscribe', { name:'page' })
1088
+ this.useSocket().send(`${this.controller}.subscribe`, { name:'page' })
897
1089
  .then(() => {
898
1090
 
899
1091
  })
@@ -901,11 +1093,16 @@ export default{
901
1093
  },
902
1094
 
903
1095
  async load(){
904
- return this.socketEmit2(this.src, { uid:this.$route.params.uid })
905
- .then(({ page, layouts, host }) => {
906
- if(page) this.page = page
907
- if(layouts) this.layouts = layouts
908
- if(host) this.host = host
1096
+ return this.useSocket().send(`${this.controller}.open`, { uid:this.$route.params.uid })
1097
+ .then(({ page, previewHost, debugMode, useChat }) => {
1098
+
1099
+ this.patchPage(page)
1100
+
1101
+ if(page) Object.assign(this.page, page)
1102
+ this.debugMode = debugMode ?? this.debugMode
1103
+ if(previewHost) this.previewHost = previewHost
1104
+
1105
+ this.useChat = useChat ?? this.useChat
909
1106
 
910
1107
  this.prevData = {
911
1108
  page: JSON.stringify(this.page),
@@ -916,25 +1113,117 @@ export default{
916
1113
 
917
1114
  this.$nextTick(() => {
918
1115
  this.resize()
919
- this.iframeSrc = (this.host ?? import.meta.env.VITE_WEB_HOST) + '/' + (this.page.path ?? '') +
920
- `?edit-mode=${this.store.previewMode}`
921
- window.setTimeout(() => {
922
- this.updateIframe()
923
- }, 500)
924
- })
925
1116
 
926
- this.state = 1
1117
+ this.iframeSrc = this.previewHost + `?edit-mode=${this.store.previewMode}`
1118
+ })
927
1119
 
1120
+ this.readyState = 1
928
1121
  })
929
1122
  .catch((err) => {
930
1123
  this.toast(err)
931
- this.$emit('close')
1124
+ this.readyState = -1
1125
+ console.error(err)
1126
+ })
1127
+ },
1128
+
1129
+ patchBgColors(bgColors){
1130
+ if (!Array.isArray(bgColors)) return
1131
+
1132
+ for(let i = 0 ; i < bgColors.length ; i++){
1133
+ let value = bgColors[i]
1134
+ if(`${bgColors[i]}`.startsWith('bg-')){
1135
+ if(patchPageCache[bgColors[i]]){
1136
+ value = patchPageCache[bgColors[i]]
1137
+ }
1138
+ else{
1139
+ const el = document.createElement('div')
1140
+ el.classList.add(bgColors[i])
1141
+ el.style.position = 'fixed'
1142
+ el.style.top = '-100000px'
1143
+ document.body.appendChild(el)
1144
+
1145
+ const rgbText = window.getComputedStyle(el)['background-color']
1146
+
1147
+ const rgb = rgbText.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*\d+)?\)/)
1148
+ if(rgb){
1149
+ value = `#${((1 << 24) + (parseInt(rgb[1]) << 16) +
1150
+ (parseInt(rgb[2]) << 8) +
1151
+ parseInt(rgb[3])).toString(16).slice(1)}`
1152
+ }
1153
+ document.body.removeChild(el)
1154
+
1155
+ patchPageCache[bgColors[i]] = value
1156
+ }
1157
+
1158
+ bgColors[i] = value
1159
+ }
1160
+ }
1161
+ },
1162
+
1163
+ patchBdColor(bdColor){
1164
+ if (!Array.isArray(bdColor)) return
1165
+
1166
+ for(let i = 0 ; i < bdColor.length ; i++){
1167
+ let value = bdColor[i]
1168
+ if(`${bdColor[i]}`.startsWith('border-')){
1169
+ if(patchPageCache[bdColor[i]]){
1170
+ value = patchPageCache[bdColor[i]]
1171
+ }
1172
+ else{
1173
+ const el = document.createElement('div')
1174
+ el.classList.add(bdColor[i])
1175
+ el.style.position = 'fixed'
1176
+ el.style.borderStyle = "solid"
1177
+ el.style.borderWidth = "5px"
1178
+ el.style.width = '100px'
1179
+ el.style.height = '100px'
1180
+ el.style.top = '10px'
1181
+ el.style.left = '10px'
1182
+ document.body.appendChild(el)
1183
+
1184
+ const rgbText = window.getComputedStyle(el)['border-color']
1185
+ const rgb = rgbText.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*\d+)?\)/)
1186
+ if(rgb){
1187
+ value = `#${((1 << 24) + (parseInt(rgb[1]) << 16) +
1188
+ (parseInt(rgb[2]) << 8) +
1189
+ parseInt(rgb[3])).toString(16).slice(1)}`
1190
+ }
1191
+ document.body.removeChild(el)
1192
+
1193
+ patchPageCache[bdColor[i]] = value
1194
+ }
1195
+
1196
+ bdColor[i] = value
1197
+ }
1198
+ }
1199
+ },
1200
+
1201
+ patchPage(page){
1202
+
1203
+ const recursePatch = (components) => {
1204
+ for(let component of components){
1205
+ this.patchBgColors(component.props.bgColors)
1206
+ this.patchBdColor(component.props.bdColor)
1207
+
1208
+ if(Array.isArray(component.items)){
1209
+ recursePatch(component.items)
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ recursePatch(page.components)
1215
+ },
1216
+
1217
+ async loadLayouts(){
1218
+ this.useSocket().send(`${this.controller}.load-layouts`, {})
1219
+ .then(_ => {
1220
+ this.layouts = _
932
1221
  })
933
1222
  },
934
1223
 
935
1224
  loadDatasource(){
936
1225
  if(this.useDatasource && this.useDatasource[1]){
937
- this.socketEmit2(this.useDatasource[1], this.page.datasource)
1226
+ this.useSocket().send(this.useDatasource[1], this.page.datasource)
938
1227
  .then((data) => {
939
1228
  Object.assign(this.page, { data })
940
1229
  this.updateIframe()
@@ -942,32 +1231,40 @@ export default{
942
1231
  }
943
1232
  },
944
1233
 
945
- openComponentSelector(params){
946
- this.$refs.webPageComponentSelector.open(params)
947
- },
948
-
949
- paste(){
950
- if(!this.currentItem) return
951
- if(![ 'components', 'headers', 'footers' ].includes(this.currentArea)) return
1234
+ loadPreset(){
1235
+ if(!Object.keys(this.$route.query).map(_ => _.toLowerCase()).includes('reset')){
1236
+ if(this.presetKey){
1237
+ return this.useSocket().send(this.presetSrc, { key:this.presetKey })
1238
+ .then(config => {
1239
+ if(config){
1240
+ Object.assign(this.config.params, config.params)
952
1241
 
953
- getClipboardData().then(text => {
954
- try{
955
- const item = JSON.parse(text)
956
- Object.assign(item.props, {
957
- enabled: true
1242
+ if(this.$route.query?.search)
1243
+ this.preset.search = this.$route.query.search
1244
+ }
1245
+ })
1246
+ }
1247
+ return new Promise(resolve => resolve())
1248
+ }
1249
+ else{
1250
+ return new Promise((resolve) => {
1251
+ const query = {}
1252
+ for(let key in this.$route.query){
1253
+ if(key.toLowerCase() !== 'reset')
1254
+ query[key] = this.$route.query[key]
1255
+ }
1256
+ this.$router.replace({
1257
+ ...this.$route,
1258
+ query
958
1259
  })
959
- this.setUid(item)
960
1260
 
961
- const comp = this.findItemByUid(this.currentItem.uid)
962
- comp.items.splice(comp.items.indexOf(comp.item) + 1, 0, item)
963
- this.select(item.uid)
1261
+ resolve()
1262
+ })
1263
+ }
1264
+ },
964
1265
 
965
- this.pageHistory.commit()
966
- }
967
- catch(e){
968
- console.error(e)
969
- }
970
- })
1266
+ openComponentSelector(params, callback){
1267
+ this.$refs.webPageComponentSelector.open(params, callback)
971
1268
  },
972
1269
 
973
1270
  onHooks(model, event, items){
@@ -983,25 +1280,19 @@ export default{
983
1280
 
984
1281
  onKeyDown(e){
985
1282
 
986
- if(e.keyCode === 67 && (e.metaKey || e.ctrlKey)){
987
- this.copy()
988
- }
989
- else if(e.keyCode === 86 && (e.metaKey || e.ctrlKey)){
990
- this.paste(e)
991
- }
992
- else if(e.altKey){
1283
+ if(e.altKey){
993
1284
  if([ 49, 50, 51, 52 ].includes(e.keyCode)){
994
1285
  if(e.keyCode === 49){
995
- this.store.previewViewType = ''
1286
+ this.store.viewType = ''
996
1287
  }
997
1288
  else if(e.keyCode === 50){
998
- this.store.previewViewType = 'md:'
1289
+ this.store.viewType = 'md:'
999
1290
  }
1000
1291
  else if(e.keyCode === 51){
1001
- this.store.previewViewType = 'xl:'
1292
+ this.store.viewType = 'xl:'
1002
1293
  }
1003
1294
  else if(e.keyCode === 52){
1004
- this.store.previewViewType = '2xl:'
1295
+ this.store.viewType = '2xl:'
1005
1296
  }
1006
1297
  this.resize()
1007
1298
  }
@@ -1049,11 +1340,26 @@ export default{
1049
1340
  break
1050
1341
 
1051
1342
  case 'component-click':
1052
- const pageComp = this.findCompByUid(uid, this.page.components)
1053
- if((pageComp && !this.store.layoutMode) || (!pageComp && this.store.layoutMode)){
1343
+ let component = this.findCompByUid(uid, this.page.components)
1344
+ if(component){
1345
+ this.store.tabIndex = 2
1346
+ this.store.selectedComponent = [ uid ]
1347
+ return
1348
+ }
1349
+
1350
+ component = this.findCompByUid(uid, this.layout.headers)
1351
+ if(!component)
1352
+ component = this.findCompByUid(uid, this.layout.footers)
1353
+ if(component){
1354
+ this.store.tabIndex = 4
1054
1355
  this.store.selectedComponent = [ uid ]
1356
+ return
1055
1357
  }
1056
1358
  break
1359
+
1360
+ case 'mounted':
1361
+ this.updateIframe()
1362
+ break
1057
1363
  }
1058
1364
  },
1059
1365
 
@@ -1072,6 +1378,10 @@ export default{
1072
1378
  }
1073
1379
  },
1074
1380
 
1381
+ openRightPane2(component){
1382
+ this.extRightPane = component
1383
+ },
1384
+
1075
1385
  postIframe(data){
1076
1386
  return new Promise((resolve, reject) => {
1077
1387
  const handleResponse = (e) => {
@@ -1115,7 +1425,7 @@ export default{
1115
1425
 
1116
1426
  resize(){
1117
1427
 
1118
- const transformOrigin = this.computedPreviewViewType === '' ? 'center top' : '0 0'
1428
+ const transformOrigin = this.store.viewType === '' ? 'center top' : '0 0'
1119
1429
 
1120
1430
  switch(this.store.zoomLevel){
1121
1431
 
@@ -1125,7 +1435,7 @@ export default{
1125
1435
  const previewHeight = this.$refs.preview.clientHeight - 70
1126
1436
 
1127
1437
  let scale = 1
1128
- switch(this.computedPreviewViewType){
1438
+ switch(this.store.viewType){
1129
1439
 
1130
1440
  case 'md:':
1131
1441
  scale = (previewWidth / 1024).toFixed(2)
@@ -1209,80 +1519,16 @@ export default{
1209
1519
  },
1210
1520
 
1211
1521
  resize3(w){
1212
- if(this.store.width[1] - w >= 270){
1522
+ if(this.store.width[1] - w >= 270 && this.store.width[1] - w <= 480){
1213
1523
  this.store.width[1] -= w
1214
1524
  }
1215
1525
  },
1216
1526
 
1217
- createStyleSheet(styles){
1218
-
1219
- const mediaQueries = {
1220
- '': '@media screen',
1221
- 'md:': '@media screen and (min-width: 640px)',
1222
- }
1223
-
1224
- const fontFamilies = {
1225
- '"Anton", sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Anton&display=swap');`,
1226
- 'Dosis, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Dosis:wght@400;700&display=swap');`,
1227
- 'Lato, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap');`,
1228
- 'Merriweather, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&display=swap');`,
1229
- 'Montserrat, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');`,
1230
- '"Noto Sans", sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;800&display=swap');`,
1231
- 'Oswald, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;700&display=swap');`,
1232
- 'Poppins, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');`,
1233
- 'Raleway, sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@400;700&display=swap');`,
1234
- '"Roboto", sans-serif': `@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');`,
1235
- '"Roboto Slab", serif': `@import url('https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;800&display=swap');`,
1236
- '"Work Sans", serif': `@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;700&display=swap');`,
1237
- }
1238
-
1239
- let text = ''
1240
- const usedFonts = {}
1241
- for(let mediaKey in styles.media){
1242
- const css = styles.media[mediaKey]
1243
-
1244
- text += mediaQueries[mediaKey] + '{ '
1245
- for(let key in css){
1246
- text += key + '{ '
1247
- for(let selector in css[key]){
1248
- text += selector + ':' + css[key][selector] + ';'
1249
-
1250
- if(selector === 'font-family' && fontFamilies[css[key][selector]]){
1251
- usedFonts[css[key][selector]] = fontFamilies[css[key][selector]]
1252
- }
1253
- }
1254
- text += '}'
1255
- }
1256
- text += '}\n'
1257
- }
1258
-
1259
- return [
1260
- ...Object.values(usedFonts),
1261
- text
1262
- ]
1263
- .join("\n")
1264
- },
1265
-
1266
- createInstances(){
1267
- if(!this.page.instances || typeof this.page.instances !== 'object' || Array.isArray(this.page.instances))
1268
- this.page.instances = {}
1269
- this.page.instances.components = (this.page.components ?? []).map((_) => this.createComponentInstance(_)).filter(_=>_)
1270
-
1271
- if(this.layout){
1272
- if(!this.layout.instances || typeof this.layout.instances !== 'object' || Array.isArray(this.layout.instances))
1273
- this.layout.instances = {}
1274
- this.layout.instances.headers = (this.layout.headers ?? []).map((_) => this.createComponentInstance(_)).filter(_=>_)
1275
- this.layout.instances.footers = (this.layout.footers ?? []).map((_) => this.createComponentInstance(_)).filter(_=>_)
1276
-
1277
- this.layout.instances.stylesheet = this.createStyleSheet(this.layout.styles)
1278
- }
1279
- },
1280
-
1281
1527
  save(){
1282
1528
  this.createInstances()
1283
1529
 
1284
1530
  this.$refs.saveBtn.setState(2)
1285
- this.socketEmit2(this.saveSrc, { ...this.page, layout:this.layout })
1531
+ this.useSocket().send(`${this.controller}.save`, { ...this.page, layout:this.layout })
1286
1532
  .then((_) => {
1287
1533
  this.prevData = {
1288
1534
  page: JSON.stringify(this.page),
@@ -1290,8 +1536,8 @@ export default{
1290
1536
  }
1291
1537
  })
1292
1538
  .catch((err) => {
1539
+ console.log('ERR', err)
1293
1540
  this.toast(err)
1294
- this.load()
1295
1541
  })
1296
1542
  .finally(_ => this.$refs.saveBtn.resetState())
1297
1543
  },
@@ -1312,8 +1558,16 @@ export default{
1312
1558
  this.$refs.ldjsonModal.close()
1313
1559
  },
1314
1560
 
1561
+ savePreset: invokeAfterIdle(function() {
1562
+ if(this.presetKey) {
1563
+ this.useSocket().send(this.presetSrc,
1564
+ {key: this.presetKey, config: this.config})
1565
+ }
1566
+ }),
1567
+
1315
1568
  select(uid){
1316
1569
  this.store.selectedComponent = [ uid ]
1570
+ this.extRightPane = null
1317
1571
 
1318
1572
  this.$refs.iframe.contentWindow.postMessage({
1319
1573
  action: 'select',
@@ -1332,10 +1586,7 @@ export default{
1332
1586
  },
1333
1587
 
1334
1588
  stopListen(){
1335
- this.socketEmit2('page.unsubscribe', { name:'page' })
1336
- .then(() => {
1337
-
1338
- })
1589
+ this.useSocket().send(`${this.controller}.unsubscribe`, { name:'page' })
1339
1590
  .catch((err) => this.toast(err))
1340
1591
  },
1341
1592
 
@@ -1375,37 +1626,136 @@ export default{
1375
1626
 
1376
1627
  async uploadImage(image, extra = {}, opt = {}){
1377
1628
 
1378
- return axios({
1379
- method: this.uploadConfig.method,
1380
- url: this.uploadConfig.url,
1381
- data: createFormData({ image, ...extra }),
1382
- onUploadProgress: function (progressEvent) {
1383
- if(opt.onUploadProgress)
1384
- opt.onUploadProgress(progressEvent)
1385
- },
1386
- })
1387
- .then((res) => {
1388
- return res.data
1629
+ if(typeof this.uploadImageFn === 'function'){
1630
+ return this.uploadImageFn(image, extra, opt)
1631
+ }
1632
+ else if(this.uploadConfig?.method){
1633
+ return axios({
1634
+ method: this.uploadConfig.method,
1635
+ url: this.uploadConfig.url,
1636
+ data: createFormData({ image, ...extra, ...(this.uploadConfig.data ?? {}) }),
1637
+ onUploadProgress: function (progressEvent) {
1638
+ if(opt.onUploadProgress)
1639
+ opt.onUploadProgress(progressEvent)
1640
+ },
1641
+ })
1642
+ .then((res) => {
1643
+ return res.data
1644
+ })
1645
+ .catch((err) => {
1646
+ this.$refs.image.value = ''
1647
+ this.$refs.imageModal.close()
1648
+ this.toast(err)
1649
+ })
1650
+ }
1651
+ else{
1652
+ throw 'Unable to upload image'
1653
+ }
1654
+ },
1655
+
1656
+ chatApply(){
1657
+
1658
+ this.$refs.chatBtn.setState(2)
1659
+ return this.useSocket()
1660
+ .send(`${this.controller}.chat`, {
1661
+ key: this.chat.key,
1662
+ prompt: this.chat.input,
1663
+ attachments: this.chat.attachments,
1664
+ pageId: this.page.id,
1665
+ input: {
1666
+ components: this.page.components,
1667
+ styles: this.layout?.styles ?? {}
1668
+ }
1669
+ })
1670
+ .catch(err => this.alert(err))
1671
+ .then(res => {
1672
+ for(let message of res.messages){
1673
+ try{
1674
+ for(let attachment of (message.attachments ?? [])){
1675
+ if(attachment.type === 'page'){
1676
+ this.chatSet(attachment)
1677
+ }
1678
+ }
1679
+
1680
+ if(message.model) this.chat.model = message.model
1681
+
1682
+ this.chat.messages.push(message)
1683
+ }
1684
+ catch(e){
1685
+ console.error(e)
1686
+ }
1687
+ }
1688
+
1689
+ this.chat.input = ''
1690
+ this.chat.attachments = []
1691
+ })
1692
+ .finally(_ => this.$refs.chatBtn?.resetState())
1693
+ },
1694
+
1695
+ onChatUpload(e){
1696
+ for(let file of e.target.files){
1697
+ this.chat.attachments.push({
1698
+ type: "image",
1699
+ image: file
1700
+ })
1701
+ }
1702
+ },
1703
+
1704
+ chatSet(obj){
1705
+
1706
+ if(obj?.styles?.media && this.layout){
1707
+ Object.assign(this.layout, {
1708
+ styles: {
1709
+ media: this.createMedia(obj.styles.media),
1710
+ imports: obj.styles.imports
1711
+ }
1389
1712
  })
1390
- .catch((err) => {
1391
- this.$refs.image.value = ''
1392
- this.$refs.imageModal.close()
1393
- this.toast(err)
1713
+ }
1714
+
1715
+ if((obj?.components ?? []).length > 0){
1716
+ Object.assign(this.page, {
1717
+ components: obj.components
1394
1718
  })
1719
+ }
1720
+ },
1721
+
1722
+ createMedia(obj){
1723
+
1724
+ const newMedia = JSON.parse(JSON.stringify(defaultMedia))
1725
+
1726
+ for(let selector in obj){
1727
+ if(!newMedia[selector]){
1728
+ newMedia[selector] = {}
1729
+ }
1730
+ for(let prop in obj[selector]){
1731
+ newMedia[selector][prop] = obj[selector][prop]
1732
+ }
1733
+ }
1734
+
1735
+ return newMedia
1395
1736
  },
1396
1737
 
1738
+ newChat(){
1739
+ this.config.params.chat = {
1740
+ key: md5('web-page-chat-' + new Date().getTime()),
1741
+ input: '',
1742
+ attachments: [],
1743
+ messages: []
1744
+ }
1745
+ }
1746
+
1397
1747
  },
1398
1748
 
1399
1749
  computed: {
1400
1750
 
1401
1751
  componentStore(){
1402
- if(this.store && this.store.components){
1403
- if(!this.store.components.compsetting)
1404
- this.store.components.compsetting = {}
1752
+ if(!this.store.components)
1753
+ this.store.components = {}
1405
1754
 
1406
- return this.store.components.compsetting
1407
- }
1408
- return {}
1755
+ if(!this.store.components.compsetting)
1756
+ this.store.components.compsetting = {}
1757
+
1758
+ return this.store.components.compsetting
1409
1759
  },
1410
1760
 
1411
1761
  expanded(){
@@ -1430,14 +1780,8 @@ export default{
1430
1780
  this.prevData.layout !== JSON.stringify(this.layout))
1431
1781
  },
1432
1782
 
1433
- computedPreviewViewType(){
1434
- if(this.store.previewViewType === 'auto')
1435
- return this.store.viewType
1436
- return this.store.previewViewType
1437
- },
1438
-
1439
1783
  computedIframeSrc(){
1440
- return this.iframeSrc.substring(0, this.iframeSrc.indexOf('?'))
1784
+ return this.page.app.hosts[0].name + this.page.path
1441
1785
  },
1442
1786
 
1443
1787
  currentItem(){
@@ -1483,7 +1827,7 @@ export default{
1483
1827
  iframeSize(){
1484
1828
 
1485
1829
  let width, height
1486
- switch(this.computedPreviewViewType){
1830
+ switch(this.store.viewType){
1487
1831
  case '':
1488
1832
  width = 390
1489
1833
  height = 844
@@ -1517,24 +1861,6 @@ export default{
1517
1861
  }
1518
1862
  },
1519
1863
 
1520
- layoutHeaders(){
1521
- if(!this.layout) return []
1522
-
1523
- if(!Array.isArray(this.layout.headers))
1524
- this.layout.headers = []
1525
-
1526
- return this.layout.headers
1527
- },
1528
-
1529
- layoutFooters(){
1530
- if(!this.layout) return []
1531
-
1532
- if(!Array.isArray(this.layout.footers))
1533
- this.layout.footers = []
1534
-
1535
- return this.layout.footers
1536
- },
1537
-
1538
1864
  previewClass(){
1539
1865
  return {
1540
1866
  'overflow-auto': this.store.zoomLevel !== 'fit',
@@ -1554,125 +1880,71 @@ export default{
1554
1880
  }
1555
1881
  },
1556
1882
 
1557
- },
1558
-
1559
- data(){
1560
- return {
1561
- components: [
1562
-
1563
- {"type":"Flex","name":"3 Column Layout","group":"Layouts","thumbnailUrl":"/images/templates/3-column-layout1.png", "items":[{"type":"Flex","name":"Flex","group":"Components","items":[{"type":"Article","name":"Article","group":"Components","props":{"htmlText":"Left","padding":["p-6",""],"enabled":true,"direction":["flex-column"],"gap":["gap-2"],"name":"Dummy Text"}}],"props":{"direction":["flex-col"],"enabled":true,"width":["","md:w-2/12"],"name":"Left"}},{"type":"Flex","name":"Flex","group":"Components","items":[{"type":"Article","name":"Article","group":"Components","props":{"htmlText":"Middle","padding":["p-6",""],"enabled":true,"direction":["flex-column"],"gap":["gap-2"],"name":"Dummy Text"}}],"props":{"direction":["flex-col"],"enabled":true,"width":[null,"md:flex-1"],"name":"Middle"}},{"type":"Flex","name":"Flex","group":"Components","items":[{"type":"Article","name":"Article","group":"Components","props":{"htmlText":"Right","padding":["p-6",""],"enabled":true,"direction":["flex-column"],"gap":["gap-2"],"name":"Dummy Text"}}],"props":{"direction":["flex-col"],"enabled":true,"width":[null,"md:w-3/12"],"name":"RIght"}}],"props":{"direction":["flex-col","md:flex-row"],"gap":[null,"md:gap-4"],"enabled":true,"name":"3 Column Layout"}},
1564
-
1565
-
1566
- {"type":"Flex","name":"Thumbnails","group":"Sections","thumbnailUrl":"/images/templates/thumbnails.gif","items":[{"type":"Flex","name":"Flex","group":"Components","items":[{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Thumbnails","tagName":"h5","flex":["flex-1"]}},{"type":"Link","name":"Link","group":"Components","props":{"enabled":true,"textColor":["text-primary-500"],"direction":["flex-column"],"gap":["gap-2"],"text":"More Thumbnails"},"items":[]}],"props":{"direction":["flex-row"],"enabled":true,"flexAlign":["items-center"]}},{"type":"Flex","name":"Flex","group":"Components","items":[{"type":"Thumbnail","name":"Thumbnail","group":"Components","props":{"enabled":true,"width":["w-5/12"],"direction":["flex-column"],"gap":["gap-2"],"flex":["flex-0"]},"items":[]},{"type":"Thumbnail","name":"Thumbnail","group":"Components","props":{"enabled":true,"width":["w-5/12"],"direction":["flex-column"],"gap":["gap-2"],"flex":["flex-0"]},"items":[]},{"type":"Thumbnail","name":"Thumbnail","group":"Components","props":{"enabled":true,"width":["w-5/12"],"direction":["flex-column"],"gap":["gap-2"],"flex":["flex-0"]},"items":[]}],"props":{"direction":["flex-row"],"gap":["gap-4"],"enabled":true,"overflow":["overflow-x-scroll"],"width":["w-full"],"minWidth":["min-w-0"]}}],"props":{"direction":["flex-col"],"gap":["gap-2"],"enabled":true,"padding":["p-5",""],"name":"Thumbnails"}},
1567
-
1568
- {"type":"Carousel","name":"Image Carousel","group":"Sections","thumbnailUrl":"/images/templates/carousel1.png","items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["/assets/web/banner1.jpg"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"],"bdRadius":["rounded-xl"],"aspectRatio":["aspect-[2/1]"],"bgColors":["bg-amber-300"]}},{"type":"Image","name":"Image","group":"Components","props":{"src":["/assets/web/banner1.jpg"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"],"bdRadius":["rounded-xl"],"aspectRatio":["aspect-[2/1]"],"bgColors":["bg-lime-300"]}},{"type":"Image","name":"Image","group":"Components","props":{"src":["/assets/web/banner1.jpg"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"],"bdRadius":["rounded-xl"],"aspectRatio":["aspect-[2/1]"],"bgColors":["bg-blue-300"]}}],"props":{"enabled":true,"containerGap":["gap-2"],"padding":["p-4",""],"direction":["flex-column"],"gap":["gap-2"],"useLegend":true}},
1569
-
1570
- {"type":"Link","name":"Icon Link","group":"Sections","thumbnailUrl":"/images/templates/icon-link.png","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["059bae0eadf1b650f3acff7dd1e7433a.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","fontWeight":["font-normal"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Icon Link 1"}}]},
1571
-
1572
- {"type":"Carousel","name":"Icon Link Carousel","group":"Sections","thumbnailUrl":"/images/templates/icon-link-carousel.gif", "items":[{"type":"Grid","name":"Grid","group":"Components","thumbnailUrl":"/images/templates/grid1.png","items":[{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["059bae0eadf1b650f3acff7dd1e7433a.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 1","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["c7e8d12c0dcda3b9d3b2862559a8bc2e.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 2","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["47bcb782edfc426c8506664cc3b320e7.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 3","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["01e5a3e8f0e89562a7f6a5449be44614.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 4","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["7d7cc53e346aa5abf1f1ada463ed0e5c.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 5","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["4a1d1808e9f1be18f0ba18322647a11e.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 6","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["1b7f9673aeee9ebdf305658a54c63dc8.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 7","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["71828d52f682527cd500c1ba6c2a9ef0.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 8","fontSize":["text-sm"]}}]}],"gap":[],"props":{"columns":["grid-cols-4"],"gap":["gap-5"],"enabled":true,"direction":["flex-column"],"padding":["p-5",""]}},{"type":"Grid","name":"Grid","group":"Components","thumbnailUrl":"/images/templates/grid1.png","items":[{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["71828d52f682527cd500c1ba6c2a9ef0.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 9","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["059bae0eadf1b650f3acff7dd1e7433a.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 10","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["4a1d1808e9f1be18f0ba18322647a11e.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 11","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["7d7cc53e346aa5abf1f1ada463ed0e5c.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 12","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["47bcb782edfc426c8506664cc3b320e7.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 13","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["c7e8d12c0dcda3b9d3b2862559a8bc2e.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 14","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["4a1d1808e9f1be18f0ba18322647a11e.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 15","fontSize":["text-sm"]}}]},{"type":"Link","name":"Icon Link","group":"Sections","props":{"enabled":true,"display":["flex"],"direction":["flex-col"],"gap":["gap-2"],"flexAlign":["items-center"],"name":"Icon Link"},"items":[{"type":"Image","name":"Image","group":"Components","props":{"src":["059bae0eadf1b650f3acff7dd1e7433a.png"],"enabled":true,"direction":["flex-column"],"gap":["gap-2"]}},{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","enabled":true,"direction":["flex-column"],"gap":["gap-2"],"text":"Link 16","fontSize":["text-sm"]}}]}],"gap":[],"props":{"columns":["grid-cols-4"],"gap":["gap-5"],"enabled":true,"direction":["flex-column"],"padding":["p-5",""]}}],"props":{"enabled":true,"containerGap":["gap-2"],"padding":["p-4",""],"direction":["flex-column"],"gap":["gap-2"],"useLegend":true,"bgColors":["bg-gray-200"],"name":"Icon Link Carousel"}},
1573
-
1574
- { type:'ContactForm', name:'Contact Form', group:'Widgets', thumbnailUrl:"/images/templates/contact-form1.png", props:{
1575
- fields:[
1576
- { type:'name', label:'Nama', required:true },
1577
- { type:'mobileNumber', label:'Nomor HP', required:true },
1578
- { type:'remark', label:'Pertanyaan' },
1579
- ],
1580
- title: 'Contact Us',
1581
- description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent gravida erat eget nisi',
1582
- submitMethod: 'post',
1583
- submitUrl: '/inquiry',
1584
- onSubmit:[],
1585
- }},
1586
-
1587
- { type:'IconList', name:'Icon List', group:'Widgets', props:{
1588
- icons:[], columns:[ 'grid-cols-4' ],
1589
- }},
1590
-
1591
- { type:'DataList', name:'Data List', group:'Widgets', props:{}, items:[] },
1592
-
1593
- { type:'FAQ', name:'FAQ', group:'Widgets', thumbnailUrl:"/images/templates/faq.gif", props:{ items:[] }},
1594
- { type:'FAQ', name:'FAQ', group:'Widgets', thumbnailUrl:"/images/templates/faq.gif", props:{ items:[] }},
1595
-
1596
- { type:'FeatureList', name:'Feature List', group:'Widgets', props:{ items:[], columns:[], variant:['variant1'] }},
1597
-
1598
- { type:'Review', name:'Review', group:'Widgets', props:{} },
1599
-
1600
- { type:'Share', name:'Share To', group:'Widgets', props:{ channels:[] }},
1601
-
1602
- { type:'Testimonial', name:'Testimonial', group:'Widgets', props:{} },
1603
-
1604
- { type:'Header', name:'Header', group:'Widgets', props:{}, items:[] },
1605
-
1606
- { type:'ThumbnailList', name:'Thumbnail List', group:'Widgets', props:{ items:[] } },
1607
-
1608
- { type:'ProductDetail', name:'Product Detail', props:{}, group:'Widgets' },
1609
-
1610
-
1611
- {"type":"Flex","name":"Section 1","group":"Components","items":[{"type":"TextBlock","name":"TextBlock","group":"Components","props":{"htmlText":"","fontSize":["text-4xl"],"enabled":true,"text":"Drive more revenue at lower costs\n","fontWeight":["font-semibold"]}},{"type":"Paragraph","name":"Paragraph","group":"Components","props":{"name":"Paragraph","enabled":true,"text":"Reach every customer on their own preferred channel with dynamic, omnichannel campaigns, using channel-responsive templates.","html":"Reach every customer on their own preferred channel with dynamic, omnichannel campaigns, using channel-responsive templates.","maxWidth":["max-w-lg"],"fontSize":["text-xl"]}},{"type":"Flex","name":"Flex","group":"Components","items":[{"type":"Button","name":"Button","group":"Components","props":{"name":"Button","text":"Start Now","enabled":true,"padding":["px-3 py-0"],"fontSize":["text-xl"]}},{"type":"Button","name":"Button","group":"Components","props":{"name":"Button","text":"Contact Sales","enabled":true,"variant":"outline","padding":["p-3"],"fontSize":["text-xl"]}}],"props":{"direction":["flex-row"],"gap":["gap-3"],"enabled":true}}],"props":{"direction":["flex-col"],"gap":["gap-4"],"enabled":true,"padding":["p-8"],"margin":["mx-auto"],"width":["w-full"],"maxWidth":["max-w-screen-xl"]}},
1612
-
1613
- {"type":"Carousel","name":"Carousel","group":"Components","items":[],"props":{"enabled":true,"containerGap":["gap-2"],"padding":["p-4",""],"direction":["flex-column"],"gap":["gap-2"],"useLegend":true}},
1614
-
1615
- { type:'Link', name:'Link', group:'Components', props:{}, items:[] },
1616
-
1617
- { type:'Modal', name:'Modal', group:'Components', props:{}, items:[] },
1618
-
1619
- { type:'Thumbnail', name:'Thumbnail', group:'Components', props:{}, items:[] },
1620
-
1621
- { type:'Ahref', name:'Ahref', group:'Components', props:{ name:'Ahref', text:'Ahref' } },
1622
-
1623
- { type:'Paragraph', name:'Paragraph', group:'Components', thumbnailUrl:"/images/templates/paragraph1.png", props:{ name:'Paragraph' } },
1624
-
1625
- { type:'Button', name:'Button', group:'Components', props:{
1626
- name:'Button', text:'Button'
1627
- }},
1628
-
1629
- { type:'Flex', name:'Flex', group:'Components', items:[], props:{ direction: [ 'flex-row' ] }},
1630
-
1631
- { type:'Grid', name:'Grid', group:'Components', thumbnailUrl:"/images/templates/grid1.png", items:[], gap:[], props:{
1632
- columns: [], gap: [],
1633
- }},
1634
-
1635
- { type:'Image', name:'Image', group:'Components', props:{
1636
- src:[],
1637
- }},
1638
-
1639
- { type:'EmbeddedVideo', name:'Video', group:'Components', props:{
1640
- src:[],
1641
- }},
1642
-
1643
- { type:'Article', name:'Article', group:'Components', props:{
1644
- htmlText:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed convallis odio at odio dapibus, " +
1645
- "eget molestie risus semper. Aenean magna tellus, aliquet nec tristique eget, lacinia nec urna.",
1646
- }},
1883
+ store(){
1884
+ return this.config.params
1885
+ },
1647
1886
 
1648
- { type:'Table', name:'Table', group:'Components', props:{
1649
- items: []
1650
- }},
1887
+ tabItems(){
1888
+ return [
1889
+ { text:"Page Info", value:1 },
1890
+ { text:"Layout", value:4 },
1891
+ { text:"Components", value:2 },
1892
+ this.useChat ? { text:"Chat", value:7 } : null,
1893
+ /*{ text:"Datasource", value:3 },*/
1894
+ ]
1895
+ .filter(_ => _)
1896
+ },
1651
1897
 
1652
- { type:'TextBlock', name:'TextBlock', group:'Components', props:{
1653
- htmlText:'',
1654
- fontFamily:[], fontSize:[], fontWeight:[], textColor:[]
1655
- }},
1898
+ presetSrc(){
1899
+ return this.controller ?
1900
+ `${this.controller}.preset` :
1901
+ 'user.preset'
1902
+ },
1656
1903
 
1657
- { type:'Block', name:'Block', group:'Components', items:[], props:{}},
1904
+ viewIndex(){
1905
+ return this.viewTypes.findIndex(_ => _.value === this.store.viewType)
1906
+ },
1658
1907
 
1659
- { type:'Countdown', name:'Countdown', group:'Components', props:{}},
1908
+ chat(){
1909
+ if(!this.config.params.chat)
1910
+ this.newChat()
1911
+ return this.config.params.chat
1912
+ },
1660
1913
 
1661
- { type:'YoutubeVideo', name:'Youtube Video', group:'Components', props:{ name:"Youtube Video", delay:"0" }},
1914
+ totalChatTokens(){
1915
+ let total = 0
1916
+ for(let message of this.chat.messages){
1917
+ total += message.llmOutputTokenCount ?? 0
1918
+ }
1919
+ return total
1920
+ }
1662
1921
 
1663
- { type:'Svg', name:'Svg', group:'Components', props:{}},
1922
+ },
1664
1923
 
1665
- ],
1924
+ data(){
1925
+ return {
1926
+ components: defaultConfig.components,
1927
+ config: {
1928
+ params: {
1929
+ version: '0.0.999',
1930
+ layoutMode: false,
1931
+ selectedComponent: null, // [ 'uid|style', 'style|headers|components|footers' ]
1932
+ tabIndex: 2,
1933
+ viewType: '',
1934
+ zoomLevel: 'fit',
1935
+ width: [ 320, 320 ],
1936
+ previewMode: 1,
1937
+ }
1938
+ },
1666
1939
 
1667
- currentArea: null,
1668
1940
  currentComponentItems: null,
1669
1941
  compClasses: [
1670
1942
  'aspectRatio', 'position', 'left', 'top', 'right', 'bottom',
1671
1943
 
1672
- 'bdSize', 'bdColor', 'bdRadius', 'bdStyle',
1944
+ 'bdSize', 'bdRadius', 'bdStyle',
1673
1945
  'divideSize', 'divideColor', 'divideStyle',
1674
1946
  'outlineWidth', 'outlineColor', 'outlineStyle',
1675
- 'bgColors', 'bgSize', 'bgPosition', 'bgRepeat',
1947
+ 'bgSize', 'bgPosition', 'bgRepeat',
1676
1948
  'textAlign', 'verticalAlign',
1677
1949
  'gap',
1678
1950
  'padding', 'margin',
@@ -1687,6 +1959,8 @@ export default{
1687
1959
  'flexDirection',
1688
1960
  'zIndex',
1689
1961
 
1962
+ 'backdropBlur',
1963
+
1690
1964
  'autoFlow', 'alignItems', 'justifyContent',
1691
1965
  'letterSpacing', 'lineClamp',
1692
1966
 
@@ -1698,9 +1972,12 @@ export default{
1698
1972
  'containerVariant', 'containerGridColumn', 'containerGap'
1699
1973
  ],
1700
1974
  itemClasses: [
1701
- 'itemMinWidth', 'itemRatio', 'itemVariant'
1975
+ 'itemMinWidth', 'itemRatio', 'itemVariant', 'objectFit'
1976
+ ],
1977
+ styleClasses: [
1978
+ 'bgColors', 'bgImages', 'bdColor'
1702
1979
  ],
1703
- host: null,
1980
+ previewHost: null,
1704
1981
  iframeStyle: {},
1705
1982
  iframeSrc: '',
1706
1983
  layouts: [],
@@ -1709,38 +1986,35 @@ export default{
1709
1986
  { text:"Design", value:1 },
1710
1987
  { text:"Preview", value:2 },
1711
1988
  ],
1712
- previewViewTypes: [
1713
- { text:'Mobile', value:'' },
1714
- { text:'Tablet', value:'md:' },
1715
- { text:'Desktop', value:'xl:' },
1716
- { text:'TV', value:'2xl:' },
1717
- ],
1718
- routerBeforeEach: null,
1719
- state: 2,
1720
- tabItems: [
1721
- { text:"Page Info", value:1 },
1722
- { text:"Components", value:2 },
1723
- { text:"Datasource", value:3 },
1724
- ],
1725
1989
  viewTypes: [
1726
1990
  { text:'Mobile', value:'' },
1727
1991
  { text:'Tablet', value:'md:' },
1728
- /*{ text:'Desktop', value:'xl:' },
1729
- { text:'TV', value:'2xl:' },*/
1992
+ { text:'Desktop', value:'xl:' }
1730
1993
  ],
1994
+ routerBeforeEach: null,
1995
+ readyState: 2,
1731
1996
  stylesheets: [
1732
1997
  'font-family'
1733
1998
  ],
1734
1999
  updating: true,
2000
+
2001
+ extRightPane: null,
2002
+ debugMode: false,
2003
+
2004
+ useChat: false
1735
2005
  }
1736
2006
  },
1737
2007
 
1738
2008
  emits: [ 'close', 'unmount' ],
1739
2009
 
1740
- inject: [ 'alert', 'confirm', 'socket', 'socketEmit2', 'toast' ],
2010
+ inject: [ 'alert', 'appStyle', 'confirm', 'useSocket', 'toast' ],
1741
2011
 
1742
2012
  mounted() {
1743
- this.load()
2013
+ this.loadPreset()
2014
+ .then(_ => {
2015
+ this.loadLayouts()
2016
+ this.load()
2017
+ })
1744
2018
 
1745
2019
  window.addEventListener('message', this.onMessage)
1746
2020
  window.addEventListener('resize', this.onResize)
@@ -1757,7 +2031,7 @@ export default{
1757
2031
  })
1758
2032
 
1759
2033
  this.listen()
1760
- this.socket.onAny(this.onHooks)
2034
+ this.useSocket().onAny(this.onHooks)
1761
2035
  },
1762
2036
 
1763
2037
  unmounted() {
@@ -1767,7 +2041,7 @@ export default{
1767
2041
  window.removeEventListener('beforeunload', this.onUnload)
1768
2042
  this.routerBeforeEach()
1769
2043
  this.stopListen()
1770
- this.socket.offAny(this.onHooks)
2044
+ this.useSocket().offAny(this.onHooks)
1771
2045
  this.$emit('unmount', { uid:this.page.uid })
1772
2046
  },
1773
2047
 
@@ -1780,11 +2054,23 @@ export default{
1780
2054
  getPage: this.getPage,
1781
2055
  uploadImage: this.uploadImage,
1782
2056
  openComponentSelector: this.openComponentSelector,
2057
+ setUid: this.setUid,
2058
+ openRightPane2: this.openRightPane2,
2059
+ getRightPane: () => this.$refs.rightPane,
2060
+ pageHistory: this.pageHistory,
2061
+ duplicate: this.duplicate
1783
2062
  }
1784
2063
  },
1785
2064
 
1786
2065
  watch: {
1787
2066
 
2067
+ config: {
2068
+ deep: true,
2069
+ handler(){
2070
+ this.savePreset()
2071
+ }
2072
+ },
2073
+
1788
2074
  currentItem(){
1789
2075
  this.$nextTick(() => this.resize())
1790
2076
  },
@@ -1793,6 +2079,11 @@ export default{
1793
2079
  if(to === 'fit'){
1794
2080
  this.$refs.preview.scrollTop = 0
1795
2081
  }
2082
+ this.resize()
2083
+ },
2084
+
2085
+ "store.viewType"(to){
2086
+ this.resize()
1796
2087
  },
1797
2088
 
1798
2089
  page: {
@@ -1820,20 +2111,41 @@ export default{
1820
2111
  .comp{
1821
2112
  @apply flex-1;
1822
2113
  @apply hidden md:flex flex-row;
1823
- @apply divide-x divide-text-50;
2114
+ @apply divide-x divide-border-50;
1824
2115
  }
1825
2116
 
1826
2117
  .resize1{
1827
- @apply w-[3px] cursor-ew-resize;
2118
+ @apply w-[3px] cursor-ew-resize border-l-[1px] border-border-50;
1828
2119
  }
1829
2120
 
1830
2121
  .resize3{
1831
- @apply w-[3px] cursor-ew-resize;
2122
+ @apply w-[3px] cursor-ew-resize border-r-[1px] border-border-50;
1832
2123
  }
1833
2124
 
1834
2125
  .zoomLevel{
1835
2126
  @apply appearance-none bg-base-400 text-center outline-none h-[24px] rounded-md;
1836
- @apply border-[1px] border-text-100;
2127
+ @apply border-[1px] border-border-200;
2128
+ }
2129
+
2130
+ .chat{
2131
+ @apply max-w-[80%] flex flex-col;
2132
+ }
2133
+
2134
+ .chatIn{
2135
+ @apply self-start;
2136
+ }
2137
+ .chatIn p{
2138
+ @apply bg-border-100 p-3 rounded-lg;
2139
+ }
2140
+
2141
+ .chatOut{
2142
+ @apply self-end flex flex-col items-end;
2143
+ }
2144
+ .chatOut p{
2145
+ @apply bg-primary-100 p-3 rounded-lg;
2146
+ }
2147
+ .chatOut button{
2148
+ @apply p-0 px-2 text-sm mt-1 rounded-sm;
1837
2149
  }
1838
2150
 
1839
2151
  </style>