@mixd-id/web-scaffold 0.2.250801014 → 0.2.260801002

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 (113) hide show
  1. package/README.md +25 -1
  2. package/docs/components/AIDashboard.md +117 -32
  3. package/docs/components/Alert.md +3 -1
  4. package/docs/components/Card.md +5 -0
  5. package/docs/components/PivotList.md +207 -0
  6. package/docs/components/Textbox.md +20 -1
  7. package/docs/components/Tooltip.md +148 -0
  8. package/package.json +17 -61
  9. package/src/components/AIChat.vue +14 -5
  10. package/src/components/AIChatItem.vue +34 -3
  11. package/src/components/AIDashboard.vue +144 -98
  12. package/src/components/Alert.vue +58 -35
  13. package/src/components/Card.vue +55 -6
  14. package/src/components/Carousel.vue +1 -1
  15. package/src/{widgets → components}/CartSetting.vue +1 -0
  16. package/src/components/Chart.vue +2 -2
  17. package/src/components/Checkbox.vue +22 -24
  18. package/src/components/CodeEditor.vue +2 -0
  19. package/src/{widgets → components}/CollapsiblePanelSetting.vue +1 -0
  20. package/src/components/ColorPicker.vue +2 -2
  21. package/src/{widgets → components}/ComponentSetting2.vue +1 -1
  22. package/src/components/Confirm.vue +2 -0
  23. package/src/{widgets → components}/ContactForm.vue +1 -1
  24. package/src/components/CopyToClipboard.vue +0 -1
  25. package/src/components/Datepicker.vue +2 -0
  26. package/src/components/Datetimepicker.vue +1 -1
  27. package/src/components/Dropdown.vue +1 -1
  28. package/src/{widgets → components}/FeatureList.vue +1 -1
  29. package/src/components/Gmaps.vue +1 -1
  30. package/src/{widgets → components}/IconList.vue +1 -1
  31. package/src/components/Image.vue +1 -1
  32. package/src/components/ImagePreview.vue +32 -51
  33. package/src/components/List.vue +45 -8
  34. package/src/components/MarkdownEdit.vue +1 -1
  35. package/src/components/MarkdownPreview.vue +4 -1
  36. package/src/{widgets → components}/MenuItem1Setting.vue +1 -0
  37. package/src/components/Metric.vue +30 -7
  38. package/src/components/MultiDropdown.vue +3 -1
  39. package/src/{widgets → components}/MultiValueSetting.vue +2 -2
  40. package/src/{widgets → components}/MultiValueSetting2.vue +2 -2
  41. package/src/components/OTPField.vue +3 -1
  42. package/src/components/PivotList.vue +1239 -0
  43. package/src/{widgets → components}/PresetBar.vue +1 -1
  44. package/src/components/PresetSelectorFilterItem.vue +1 -1
  45. package/src/components/Radio.vue +1 -1
  46. package/src/{widgets → components}/Share.vue +1 -1
  47. package/src/components/SvgEditor.vue +1 -1
  48. package/src/components/Switch.vue +1 -1
  49. package/src/components/TextWithTag.vue +2 -2
  50. package/src/components/Textbox.vue +63 -5
  51. package/src/components/Toast.vue +2 -1
  52. package/src/components/Tooltip.vue +626 -0
  53. package/src/components/VirtualGrid.vue +2 -2
  54. package/src/components/VirtualTable.vue +4 -3
  55. package/src/{widgets → components}/WebDatasourceSelector.vue +2 -0
  56. package/src/{widgets → components}/WebLayoutSelector.vue +1 -1
  57. package/src/directives/tooltip.js +131 -0
  58. package/src/index.js +11 -235
  59. package/src/themes/default/index.js +16 -0
  60. package/src/utils/ai-dashboard.js +163 -38
  61. package/src/utils/helpers.mjs +52 -5
  62. package/src/utils/list.mjs +4 -1
  63. package/src/utils/pivot-formula.mjs +358 -0
  64. package/src/utils/pivot-list.mjs +557 -0
  65. package/src/utils/tooltip.mjs +243 -0
  66. package/src/utils/wss.js +26 -6
  67. package/src/utils/wss.mjs +29 -2
  68. /package/src/{widgets → components}/AhrefSetting.vue +0 -0
  69. /package/src/{widgets → components}/ArticleSetting.vue +0 -0
  70. /package/src/{widgets → components}/BackgroundColorSetting.vue +0 -0
  71. /package/src/{widgets → components}/BlockSetting.vue +0 -0
  72. /package/src/{widgets → components}/BorderColorSetting.vue +0 -0
  73. /package/src/{widgets → components}/ButtonSetting.vue +0 -0
  74. /package/src/{widgets → components}/CarouselSetting.vue +0 -0
  75. /package/src/{widgets → components}/ColumnSelector.vue +0 -0
  76. /package/src/{widgets → components}/ComponentSetting.vue +0 -0
  77. /package/src/{widgets → components}/ContactFormSetting.vue +0 -0
  78. /package/src/{widgets → components}/CountdownSetting.vue +0 -0
  79. /package/src/{widgets → components}/DataListSetting.vue +0 -0
  80. /package/src/{widgets → components}/EmbeddedVideo.vue +0 -0
  81. /package/src/{widgets → components}/EmbeddedVideoSetting.vue +0 -0
  82. /package/src/{widgets → components}/FAQ.vue +0 -0
  83. /package/src/{widgets → components}/FAQSetting.vue +0 -0
  84. /package/src/{widgets → components}/FeatureListSetting.vue +0 -0
  85. /package/src/{widgets → components}/FlexSetting.vue +0 -0
  86. /package/src/{widgets → components}/GridSetting.vue +0 -0
  87. /package/src/{widgets → components}/Header2.vue +0 -0
  88. /package/src/{widgets → components}/Header2Setting.vue +0 -0
  89. /package/src/{widgets → components}/IconListSetting.vue +0 -0
  90. /package/src/{widgets → components}/ImageSetting.vue +0 -0
  91. /package/src/{widgets → components}/LLMChat.vue +0 -0
  92. /package/src/{widgets → components}/LinkSetting.vue +0 -0
  93. /package/src/{widgets → components}/LinkSettingModal.vue +0 -0
  94. /package/src/{widgets → components}/ModalSetting.vue +0 -0
  95. /package/src/{widgets → components}/OGSettingModal.vue +0 -0
  96. /package/src/{widgets → components}/ParagraphSetting.vue +0 -0
  97. /package/src/{widgets → components}/PositionSetting.vue +0 -0
  98. /package/src/{widgets → components}/PresetBarPivot.vue +0 -0
  99. /package/src/{widgets → components}/PresetBarPivotColumnEdit.vue +0 -0
  100. /package/src/{widgets → components}/PresetBarPivotValueEdit.vue +0 -0
  101. /package/src/{widgets → components}/PresetSelector.vue +0 -0
  102. /package/src/{widgets → components}/Review.vue +0 -0
  103. /package/src/{widgets → components}/ReviewSetting.vue +0 -0
  104. /package/src/{widgets → components}/SearchModalSetting.vue +0 -0
  105. /package/src/{widgets → components}/ShareSetting.vue +0 -0
  106. /package/src/{widgets → components}/SvgSetting.vue +0 -0
  107. /package/src/{widgets → components}/TableSetting.vue +0 -0
  108. /package/src/{widgets → components}/TestimonialSetting.vue +0 -0
  109. /package/src/{widgets → components}/TextBlockSetting.vue +0 -0
  110. /package/src/{widgets → components}/ThumbnailSetting.vue +0 -0
  111. /package/src/{widgets → components}/WebComponentSelector.vue +0 -0
  112. /package/src/{widgets → components}/WebPageBuilder.vue +0 -0
  113. /package/src/{widgets → components}/YoutubeVideoSetting.vue +0 -0
@@ -0,0 +1,1239 @@
1
+ <template>
2
+ <div class="flex flex-col bg-base-400 md:bg-transparent"
3
+ v-intersect="onVisibleChange">
4
+
5
+ <div v-if="readyState >= 1"
6
+ class="flex-1 flex flex-row">
7
+
8
+ <div v-if="computedPresetMode === 'sidebar' && sidebar.open"
9
+ class="relative flex flex-col border-r-[1px] border-border-50 bg-base-400"
10
+ :style="sidebarStyle">
11
+
12
+ <div class="flex-1 flex flex-col relative">
13
+ <PresetBar :config="cConfig"
14
+ class="flex-1"
15
+ :enumCache="enumCache"
16
+ :controller="controller"
17
+ :preset-key="presetKey"
18
+ @apply="load">
19
+
20
+ <template #toolbar>
21
+ <button type="button" class="p-1" @click="togglePreset">
22
+ <svg width="16" height="16" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"/></svg>
23
+ </button>
24
+ </template>
25
+
26
+ </PresetBar>
27
+ </div>
28
+
29
+ <div v-if="sidebar.open"
30
+ :class="$style.resize1"
31
+ @mousedown="(e) => $util.dragResize(e, resize1)"></div>
32
+ </div>
33
+
34
+ <div v-if="preset" class="flex-1 flex flex-col relative">
35
+
36
+ <div class="flex-1 flex flex-col" :class="containerClass">
37
+ <slot name="head"
38
+ :preset="preset"
39
+ :presetSelector="$refs.presetSelector"
40
+ :compPrefix="compPrefix">
41
+ <div class="flex flex-col md:flex-row gap-3 md:items-end overflow-hidden p-3 pb-1 md:p-0"
42
+ :class="headerClass">
43
+ <div class="flex md:flex-1 flex-row items-end md:gap-3 py-1 md:py-0" @click="log(cConfig)">
44
+ <slot name="left"></slot>
45
+ <div class="flex flex-row gap-2 md:gap-3 items-center px-3" ref="title">
46
+ <div class="flex flex-col whitespace-nowrap text-ellipsis overflow-hidden">
47
+ <div class="cursor-pointer group leading-4" @click="$refs.contextMenu.open($refs.title)">
48
+ <small class="text-text-400 text-xs">{{ title ?? 'Untitled' }}</small>
49
+ <div class="flex flex-row items-baseline gap-1">
50
+ <h3 class="whitespace-nowrap relative text-ellipsis overflow-hidden group-hover:text-primary">
51
+ {{ preset.name ?? 'Preset Name' }}
52
+ </h3>
53
+ <svg width="13" height="13" class="fill-text group-hover:fill-primary relative top-[-1px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"/></svg>
54
+ </div>
55
+ </div>
56
+ <ContextMenu ref="contextMenu" class="bg-base-300 mt-4">
57
+ <div class="flex flex-col min-w-[280px] max-w-[400px] p-1">
58
+
59
+ <div class="flex flex-col gap-1 bg-base-300">
60
+ <button v-for="(preset, idx) in configPresets"
61
+ class="p-2 px-5 text-left flex flex-row items-center rounded-md"
62
+ :class="configParams.presetIdx === preset.uid ? 'bg-primary-200 text-white' : 'hover:bg-primary-100'"
63
+ type="button"
64
+ @click="selectPreset(idx)">
65
+ <label class="flex-1 pr-12 text-ellipsis-nowrap">
66
+ {{ preset.name }}
67
+ </label>
68
+ <div class="p-1">
69
+ <div v-if="idx < 10"
70
+ class="border-[1px] border-text-300 px-2 text-mono text-xs text-text-400 rounded-lg">
71
+ Alt {{ idx + 1 }}
72
+ </div>
73
+ </div>
74
+ </button>
75
+ </div>
76
+
77
+ <div class="my-2 h-[1px] bg-border-50"></div>
78
+
79
+ <div class="px-3">
80
+ <button type="button" class="text-primary p-3 text-left w-full" @click="openPreset">Edit</button>
81
+ </div>
82
+
83
+ </div>
84
+ </ContextMenu>
85
+ </div>
86
+ </div>
87
+
88
+ <slot name="left-toolbar"></slot>
89
+
90
+ <div class="flex-1"></div>
91
+
92
+ <slot name="toolbar">
93
+ </slot>
94
+
95
+ <slot name="right-toolbar">
96
+ <div class="hidden md:flex flex-row gap-2" v-if="Boolean(toolbar)">
97
+ <Textbox v-if="canSearch"
98
+ class="flex-1 md:w-[240px]"
99
+ placeholder="Search..."
100
+ clearable="1"
101
+ maxlength="100"
102
+ v-model="preset.search"
103
+ @clear="delete preset.search; load()"
104
+ @keyup.enter="load">
105
+ <template #start>
106
+ <div class="p-2 pr-0">
107
+ <svg width="14" height="14" class="fill-text-300" 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="M504.1 471l-134-134C399.1 301.5 415.1 256.8 415.1 208c0-114.9-93.13-208-208-208S-.0002 93.13-.0002 208S93.12 416 207.1 416c48.79 0 93.55-16.91 129-45.04l134 134C475.7 509.7 481.9 512 488 512s12.28-2.344 16.97-7.031C514.3 495.6 514.3 480.4 504.1 471zM48 208c0-88.22 71.78-160 160-160s160 71.78 160 160s-71.78 160-160 160S48 296.2 48 208z"/></svg>
108
+ </div>
109
+ </template>
110
+ </Textbox>
111
+ </div>
112
+ </slot>
113
+ </div>
114
+
115
+ </div>
116
+ </slot>
117
+
118
+ <div class="md:hidden">
119
+ <slot name="mobile-toolbar">
120
+ <div class="p-3">
121
+ <Textbox v-if="canSearch"
122
+ placeholder="Search..."
123
+ class="bg-base-300"
124
+ clearable="1"
125
+ maxlength="100"
126
+ v-model="preset.search"
127
+ @clear="delete preset.search; load()"
128
+ @keyup.enter="load">
129
+ <template #start>
130
+ <div class="p-2 pr-0">
131
+ <svg width="14" height="14" class="fill-text-300" 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="M504.1 471l-134-134C399.1 301.5 415.1 256.8 415.1 208c0-114.9-93.13-208-208-208S-.0002 93.13-.0002 208S93.12 416 207.1 416c48.79 0 93.55-16.91 129-45.04l134 134C475.7 509.7 481.9 512 488 512s12.28-2.344 16.97-7.031C514.3 495.6 514.3 480.4 504.1 471zM48 208c0-88.22 71.78-160 160-160s160 71.78 160 160s-71.78 160-160 160S48 296.2 48 208z"/></svg>
132
+ </div>
133
+ </template>
134
+ </Textbox>
135
+ </div>
136
+ </slot>
137
+ </div>
138
+
139
+ <!-- Formula bar — spreadsheet-style editor for client-side virtual
140
+ columns. Formulas reference pivot result columns as [key] and
141
+ are recomputed locally, no server round-trip. -->
142
+ <div v-if="pivotReady && Boolean(toolbar)" class="flex flex-col gap-1 px-3 md:px-0">
143
+ <div class="flex flex-row items-center gap-2 bg-base-300 border-[1px] border-border-50 rounded-lg px-3 py-1">
144
+ <em class="font-serif italic font-bold text-text-300 select-none" title="Virtual columns — calculated client-side">fx</em>
145
+
146
+ <select class="appearance-none bg-transparent outline-none max-w-[160px] text-ellipsis border-r-[1px] border-border-50 pr-2"
147
+ :value="formulaBar.key"
148
+ @change="e => selectFormula(e.target.value)">
149
+ <option value="">+ New column</option>
150
+ <option v-for="vc in virtualColumns" :key="vc.key" :value="vc.key">{{ vc.label || vc.key }}</option>
151
+ </select>
152
+
153
+ <input class="w-[140px] bg-transparent outline-none border-r-[1px] border-border-50 pr-2"
154
+ placeholder="Column name"
155
+ v-model="formulaBar.name"
156
+ @keyup.enter="applyFormula" />
157
+
158
+ <input class="flex-1 bg-transparent outline-none font-mono text-sm"
159
+ placeholder='=([_amount-sum] - [_cost-sum]) / [_amount-sum]'
160
+ v-model="formulaBar.formula"
161
+ @focus="formulaBar.focused = true"
162
+ @blur="formulaBar.focused = false"
163
+ @keyup.enter="applyFormula" />
164
+
165
+ <button type="button" class="text-primary px-1" title="Apply formula (Enter)" @click="applyFormula">
166
+ <svg width="14" height="14" class="fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/></svg>
167
+ </button>
168
+ <button v-if="formulaBar.key" type="button" class="px-1" title="Remove this virtual column" @click="removeFormula">
169
+ <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>
170
+ </button>
171
+ </div>
172
+
173
+ <div v-if="formulaBar.error" class="text-red-500 text-sm px-2">
174
+ {{ formulaBar.error }}
175
+ </div>
176
+
177
+ <div v-if="formulaBar.focused && formulaRefs.length > 0"
178
+ class="flex flex-row flex-wrap gap-1 px-1">
179
+ <button v-for="key in formulaRefs" :key="key"
180
+ type="button"
181
+ class="px-2 rounded-md bg-base-300 border-[1px] border-border-50 text-xs text-text-400 hover:text-primary hover:border-primary font-mono"
182
+ @mousedown.prevent="insertRef(key)">
183
+ [{{ key }}]
184
+ </button>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="flex-1 flex" ref="content">
189
+ <div v-if="readyState === 2" class="flex-1 flex items-center justify-center">
190
+ <svg class="animate-spin" width="36" height="36" 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>
191
+ </div>
192
+
193
+ <div v-else-if="!pivotReady" class="flex-1 flex flex-col items-center justify-center gap-3 text-text-300">
194
+ <p>Add at least one group or aggregate to build the pivot.</p>
195
+ <button type="button" class="text-primary" @click="openPreset">Open preset editor</button>
196
+ </div>
197
+
198
+ <div v-else class="flex-1 flex flex-col">
199
+ <VirtualTable
200
+ ref="table"
201
+ :columns="columns"
202
+ class="flex-1 bg-base-300"
203
+ :items="dataItems"
204
+ :enumCache="enumCache"
205
+ :freeze-left="freezeLeft"
206
+ :item-class="itemClass"
207
+ @freeze="freeze"
208
+ @unfreeze="unfreeze"
209
+ @item-click="onTableItemClick"
210
+ @dragover="(...args) => $emit('dragover', ...args)">
211
+
212
+ <template v-for="(_, slot) in headerSlots" #[slot]="{ item, index }">
213
+ <div :class="getHeader(slot.replace('col-', ''))">
214
+ <slot :name="slot" :item="item" :index="index"></slot>
215
+ </div>
216
+ </template>
217
+
218
+ <template v-for="(_, slot) in contentSlots" #[slot]="{ column, item, index, selected }">
219
+ <slot :name="slot" :item="item" :index="index" :selected="selected" :column="column"></slot>
220
+ </template>
221
+
222
+ </VirtualTable>
223
+ </div>
224
+
225
+ <PresetSelector ref="presetSelector" :config="cConfig" @select="load" />
226
+ </div>
227
+ </div>
228
+
229
+ </div>
230
+
231
+ </div>
232
+
233
+ </div>
234
+ </template>
235
+
236
+ <script>
237
+
238
+ import VirtualTable from "./VirtualTable.vue";
239
+ import PresetSelector from "./PresetSelector.vue";
240
+ import PresetBar from "./PresetBar.vue";
241
+ import {generatePivotColumns, generateTotalColumns, parsePivotKey, setupConfig} from "../utils/preset-selector.mjs";
242
+ import {invokeAfterIdle} from "../utils/helpers.mjs";
243
+ import {compileFormula} from "../utils/pivot-formula.mjs";
244
+
245
+ export default{
246
+
247
+ emits: [ 'dragover', 'after-load', 'open-preset', 'signal', 'pivot-item-click', 'item-click' ],
248
+
249
+ inject: [ 'useSocket', 'toast' ],
250
+
251
+ props: {
252
+ config: Object,
253
+ defaultConfig: Function,
254
+ presetKey: String,
255
+ subscribeKey: String,
256
+ controller: String,
257
+ title: String,
258
+ updateInterval: {
259
+ type: Number,
260
+ default: 1200
261
+ },
262
+
263
+ src: undefined,
264
+ headerClass: String,
265
+ presetMode: {
266
+ type: String,
267
+ default: "sidebar"
268
+ },
269
+ containerClass: {
270
+ type: String,
271
+ default: "md:p-8 md:gap-5"
272
+ },
273
+ itemClass: String,
274
+ toolbar: {
275
+ type: [ String, Boolean ],
276
+ default: true
277
+ },
278
+ itemsFn: Function,
279
+ searchable: {
280
+ type: [ Boolean, String ],
281
+ default: true
282
+ }
283
+ },
284
+
285
+ methods: {
286
+
287
+ // Merge server (model-derived) column types into every column def that
288
+ // doesn't declare one — includes dotted relationship keys resolved by the
289
+ // pivot-list mixin, so date/number/boolean pivot behaviors kick in without
290
+ // declaring types by hand.
291
+ applyColumnTypes(types){
292
+ if(!types || typeof types !== 'object') return
293
+ const fill = (columns) => (columns ?? []).forEach(column => {
294
+ if(column.key && !column.type && types[column.key] && types[column.key] !== 'string'){
295
+ column.type = types[column.key]
296
+ }
297
+ })
298
+ fill(this.cConfig.columns)
299
+ ;(this.cConfig.presets ?? []).forEach(preset => fill(preset.columns))
300
+ ;(this.cConfig.sharedPresets ?? []).forEach(preset => fill(preset.columns))
301
+ },
302
+
303
+ getEnumText(key, value){
304
+
305
+ let enumText
306
+
307
+ const column = this.cConfig.columns.find(_ => _.key === key)
308
+
309
+ if(Array.isArray(column?.typeParams)){
310
+ const typeParam = (column.typeParams ?? []).filter((_) => _.value === value).pop()
311
+ enumText = typeParam && typeParam.text ? typeParam.text : enumText
312
+ }
313
+
314
+ if(this.enumCache && this.enumCache[key] && this.enumCache[key][value]){
315
+ enumText = this.enumCache[key][value].text ?? enumText
316
+ }
317
+
318
+ return enumText ? enumText : value
319
+ },
320
+
321
+ getHeader(column){
322
+
323
+ return [
324
+ this.$style.header,
325
+ (this.preset.filters ?? []).findIndex((_) => _.key === column.key && _.enabled) >= 0 ?
326
+ this.$style.headerSelected : ''
327
+ ]
328
+ .join(' ')
329
+ },
330
+
331
+ load(){
332
+ if(!this.computedSrc || !this.preset) return Promise.resolve()
333
+
334
+ if(!this.preset.columns){
335
+ this.preset.columns = JSON.parse(JSON.stringify(this.cConfig.columns))
336
+ }
337
+
338
+ if(!this.pivotReady){
339
+ this.data.items = []
340
+ return Promise.resolve()
341
+ }
342
+
343
+ this.readyState = 2
344
+ return this.useSocket().send(this.computedSrc, {
345
+ ...this.preset,
346
+ id: undefined,
347
+ uid: undefined,
348
+ })
349
+ .then(data => {
350
+ if(data?.items){
351
+ this.applyColumnTypes(data.columnTypes)
352
+ generatePivotColumns(this.preset, data.items)
353
+ generateTotalColumns(this.preset, data.items)
354
+ Object.assign(this.data, data ?? {})
355
+ this.applyVirtualColumns()
356
+ this.loadEnums(this.data.items)
357
+ this.$emit('after-load')
358
+ }
359
+ })
360
+ .catch(err => this.toast(err))
361
+ .finally(_ => this.readyState = 1)
362
+ },
363
+
364
+ async loadPreset(){
365
+ if(!Object.keys(this.$route.query).map(_ => _.toLowerCase()).includes('reset')){
366
+ if(this.presetKey){
367
+ const config = await this.useSocket().send(this.presetSrc, { key:this.presetKey })
368
+ if(config && (config.presets ?? []).length > 0){
369
+ Object.assign(this.cConfig, config)
370
+
371
+ if(this.$route.query?.search)
372
+ this.preset.search = this.$route.query.search
373
+ }
374
+ else{
375
+ const defaultConfig = setupConfig(typeof this.defaultConfig === 'function' ? await this.defaultConfig() : {})
376
+ Object.assign(this.cConfig, defaultConfig)
377
+ }
378
+ }
379
+ else{
380
+ const defaultConfig = setupConfig(typeof this.defaultConfig === 'function' ? await this.defaultConfig() : {})
381
+ Object.assign(this.cConfig, defaultConfig)
382
+ }
383
+ }
384
+ else{
385
+ if(this.defaultConfig){
386
+ const defaultConfig = setupConfig(await this.defaultConfig())
387
+ Object.assign(this.cConfig, defaultConfig)
388
+ }
389
+
390
+ const query = {}
391
+ for(let key in this.$route.query){
392
+ if(key.toLowerCase() !== 'reset')
393
+ query[key] = this.$route.query[key]
394
+ }
395
+ this.$router.replace({
396
+ ...this.$route,
397
+ query
398
+ })
399
+ }
400
+ },
401
+
402
+ async loadEnums(items){
403
+ if(!items) return
404
+
405
+ return Promise.all((this.preset.columns ?? []).map(async column => {
406
+
407
+ if(column.enumSrc) {
408
+
409
+ if(!this.enumCache[column.key])
410
+ this.enumCache[column.key] = {}
411
+
412
+ for(let item of items){
413
+ if(item[column.key] && !this.enumCache[column.key][item[column.key]]){
414
+ this.enumCache[column.key][item[column.key]] = { value:item[column.key] }
415
+ }
416
+ }
417
+
418
+ const reqItems = Object.values(this.enumCache[column.key]).filter(_ => !_.text).map(_ => _.value)
419
+
420
+ if(reqItems.length > 0 && reqItems !== this.lastEnumItems){
421
+
422
+ const [ src, key, attr ] = column.enumSrc.split(':')
423
+ const res = await this.useSocket().send(src, {
424
+ columns: [
425
+ { key:attr, visible:true }
426
+ ],
427
+ filters: [
428
+ {
429
+ key:key, value: [ { operator:'in', value:reqItems } ]
430
+ }
431
+ ]
432
+ });
433
+
434
+ (res.items ?? []).forEach(_ => {
435
+ this.enumCache[column.key][_[key]] = {
436
+ text: _[attr] ?? _[key],
437
+ value: _[key],
438
+ }
439
+ })
440
+
441
+ for(let key in this.enumCache[column.key]){
442
+ if(!this.enumCache[column.key][key].text)
443
+ this.enumCache[column.key][key].text = key
444
+ }
445
+
446
+ this.lastEnumItems = reqItems
447
+ }
448
+ }
449
+
450
+ }))
451
+ },
452
+
453
+ // ------------------------------------------------ virtual columns
454
+
455
+ // Recompute every virtual column over the loaded pivot rows (total row
456
+ // included — the formula runs on the totals like a spreadsheet would) and
457
+ // sync their column defs into pivot.columns.
458
+ applyVirtualColumns(){
459
+ const pivot = (this.preset ?? {}).pivot
460
+ if(!pivot) return
461
+
462
+ const virtuals = this.virtualColumns
463
+ const items = this.data.items ?? []
464
+
465
+ // drop defs whose virtual column no longer exists
466
+ if(Array.isArray(pivot.columns)){
467
+ for(let i = pivot.columns.length - 1 ; i >= 0 ; i--){
468
+ if(pivot.columns[i].virtual && !virtuals.find(_ => _.key === pivot.columns[i].key)){
469
+ pivot.columns.splice(i, 1)
470
+ }
471
+ }
472
+ }
473
+
474
+ for(const vc of virtuals){
475
+ let compiled
476
+ try{
477
+ compiled = compileFormula(vc.formula)
478
+ }
479
+ catch(err){
480
+ compiled = { evaluate: () => err.code ?? '#ERROR!' }
481
+ }
482
+
483
+ let isString = false
484
+ for(const item of items){
485
+ const value = compiled.evaluate(item)
486
+ item[vc.key] = value
487
+ if(typeof value === 'string' && !value.startsWith('#')) isString = true
488
+ }
489
+
490
+ const type = vc.type ?? (isString ? 'string' : 'number')
491
+ const def = (pivot.columns ?? []).find(_ => _.key === vc.key)
492
+ if(def){
493
+ Object.assign(def, { label: vc.label || vc.key, type, virtual: true })
494
+ }
495
+ else{
496
+ (pivot.columns = pivot.columns ?? []).push({
497
+ key: vc.key,
498
+ label: vc.label || 'Formula',
499
+ tooltip: vc.formula,
500
+ type,
501
+ virtual: true
502
+ })
503
+ }
504
+ }
505
+ },
506
+
507
+ selectFormula(key){
508
+ const vc = this.virtualColumns.find(_ => _.key === key)
509
+ Object.assign(this.formulaBar, vc ?
510
+ { key, name: vc.label, formula: vc.formula, error: '' } :
511
+ { key: '', name: '', formula: '', error: '' })
512
+ },
513
+
514
+ applyFormula(){
515
+ const { key, name, formula } = this.formulaBar
516
+ if(!`${formula ?? ''}`.replace(/^\s*=/, '').trim()) return
517
+
518
+ try{
519
+ compileFormula(formula)
520
+ }
521
+ catch(err){
522
+ this.formulaBar.error = err.message
523
+ return
524
+ }
525
+ this.formulaBar.error = ''
526
+
527
+ let vc = this.virtualColumns.find(_ => _.key === key)
528
+ if(!vc){
529
+ vc = { key: `_vc_${Math.random().toString(36).slice(2, 8)}` }
530
+ this.virtualColumns.push(vc)
531
+ this.formulaBar.key = vc.key
532
+ }
533
+ vc.label = name || vc.label || 'Formula'
534
+ vc.formula = formula
535
+
536
+ this.applyVirtualColumns()
537
+ },
538
+
539
+ removeFormula(){
540
+ const idx = this.virtualColumns.findIndex(_ => _.key === this.formulaBar.key)
541
+ if(idx >= 0) this.virtualColumns.splice(idx, 1)
542
+ this.selectFormula('')
543
+ this.applyVirtualColumns()
544
+ },
545
+
546
+ insertRef(key){
547
+ this.formulaBar.formula = `${this.formulaBar.formula ?? ''}[${key}]`
548
+ },
549
+
550
+ // ------------------------------------------------ presets
551
+
552
+ resize1(w){
553
+ if(this.sidebar.width + w >= 200 && this.sidebar.width + w <= 600){
554
+ this.sidebar.width += w
555
+ }
556
+ },
557
+
558
+ selectPreset(idx){
559
+ if(this.configPresets[idx]){
560
+ this.configParams.presetIdx = this.configPresets[idx].uid
561
+ this.selectFormula('')
562
+ this.load()
563
+ this.$refs.presetSelector?.close()
564
+ this.$refs.contextMenu?.close()
565
+ }
566
+ },
567
+
568
+ savePreset: invokeAfterIdle(function() {
569
+ if(this.presetKey) {
570
+ this.useSocket().send(this.presetSrc,
571
+ {key: this.presetKey, config: this.cConfig})
572
+ }
573
+ }),
574
+
575
+ openPreset(){
576
+
577
+ switch(this.computedPresetMode){
578
+
579
+ case 'external':
580
+ this.$emit('open-preset')
581
+ break
582
+
583
+ case 'sidebar':
584
+ this.sidebar.open = true
585
+ break
586
+
587
+ default:
588
+ this.$refs.presetSelector.open()
589
+ break
590
+
591
+ }
592
+ },
593
+
594
+ togglePreset(){
595
+ switch(this.computedPresetMode){
596
+
597
+ case 'sidebar':
598
+ this.sidebar.open = !this.sidebar.open
599
+ break
600
+
601
+ case 'popup':
602
+ this.$refs.presetSelector.open()
603
+ break
604
+ }
605
+ },
606
+
607
+ // Pivot rows are aggregates — realtime signals can't be patched in, so
608
+ // re-run the pivot query (debounced: bursts collapse into one reload).
609
+ onSignal(event, items){
610
+ if(!this._pivotReload){
611
+ this._pivotReload = invokeAfterIdle(() => this.load(), { delay: this.updateInterval, flushOnExit: false })
612
+ }
613
+ this._pivotReload()
614
+ this.$emit('signal', event, items)
615
+ },
616
+
617
+ // Clicking an aggregate cell emits `pivot-item-click` with the equivalent
618
+ // row filters — data-type aware (booleans compare as booleans, date
619
+ // buckets become format_* fn filters). The host decides what to do with
620
+ // them (navigate to a List, open a modal, …) — PivotList itself renders
621
+ // only the single pivot table.
622
+ onTableItemClick(item, column){
623
+ if(item._type === 'totalRow') return
624
+
625
+ if(!(column.key ?? '').startsWith('_') || column.virtual){
626
+ this.$emit('item-click', item, column)
627
+ return
628
+ }
629
+
630
+ const filters = []
631
+
632
+ for(let idx in this.preset.pivot.rows){
633
+ const pivotRow = this.preset.pivot.rows[idx]
634
+ const key = pivotRow.key
635
+ const presetColumn = this.preset.columns.find(_ => _.key === key) ?? pivotRow
636
+ let value = item[pivotRow.key]
637
+ switch(presetColumn.type){
638
+
639
+ case 'bool':
640
+ case 'boolean':
641
+ filters.push({
642
+ key,
643
+ value: [{
644
+ operator: '=',
645
+ value: value === 'true' || value === true || value === 1
646
+ }]
647
+ })
648
+ break
649
+
650
+ case 'date':
651
+ switch(pivotRow.aggregate ?? pivotRow.aggregrate){
652
+
653
+ case 'hour':
654
+ value = value.split('-')
655
+ filters.push({
656
+ key,
657
+ value: [{
658
+ operator: '=',
659
+ value: value[0].split(':')[0],
660
+ fn: 'format_hour'
661
+ }]
662
+ })
663
+ break
664
+
665
+ case 'date':
666
+ case 'week':
667
+ case 'month':
668
+ case 'quarter':
669
+ case 'year': {
670
+ const bucket = pivotRow.aggregate ?? pivotRow.aggregrate
671
+ filters.push({
672
+ key,
673
+ value: [{
674
+ operator: '=',
675
+ value: value,
676
+ fn: `format_${bucket}`
677
+ }]
678
+ })
679
+ break
680
+ }
681
+
682
+ }
683
+ break
684
+
685
+ default:
686
+ filters.push({
687
+ key,
688
+ value: [{
689
+ operator: '=',
690
+ value
691
+ }]
692
+ })
693
+ break
694
+ }
695
+
696
+ }
697
+
698
+ // The clicked value column adds the drill-down filter:
699
+ // spread columns -> spread field = the clicked group value
700
+ // plain sum -> measure > 0
701
+ const parsed = column.key === '_rowTotal' ? null : parsePivotKey(column.key, this.preset.pivot)
702
+
703
+ if(parsed && parsed.spread != null){
704
+ const key = parsed.spreadKey
705
+ const presetColumn = this.preset.columns.find(_ => _.key === key)
706
+ ?? (this.cConfig.columns ?? []).find(_ => _.key === key)
707
+ let value = parsed.spread
708
+ switch(presetColumn?.type){
709
+
710
+ case 'bool':
711
+ case 'boolean':
712
+ value = value === 'true'
713
+ break
714
+
715
+ default:
716
+ value = value === '(empty)' ? '' :
717
+ value === '(null)' ? null :
718
+ value.replaceAll('_', ' ')
719
+ break
720
+ }
721
+
722
+ filters.push({
723
+ key,
724
+ value: [{
725
+ operator: '=',
726
+ value
727
+ }]
728
+ })
729
+ }
730
+ else if(parsed && parsed.aggregate === 'sum'){
731
+ filters.push({
732
+ key: parsed.field,
733
+ value: [{
734
+ operator: '>',
735
+ value: 0
736
+ }]
737
+ })
738
+ }
739
+
740
+ this.$emit('pivot-item-click', { item, column, filters })
741
+ },
742
+
743
+ resize(){
744
+ this.$refs.table ? this.$refs.table.resize() : null
745
+ },
746
+
747
+ onConnect(reconnect){
748
+ if(reconnect){
749
+ this.load()
750
+ }
751
+ },
752
+
753
+ onKeyDown(e){
754
+
755
+ if(e.altKey){
756
+
757
+ switch(e.code){
758
+
759
+ case 'Backquote':
760
+ this.togglePreset()
761
+ break
762
+
763
+ default:
764
+ if(e.code.startsWith('Digit')){
765
+ const idx = parseInt(e.code.substring(5)) - 1
766
+ if(this.configPresets[idx]) {
767
+ this.selectPreset(idx)
768
+ }
769
+ }
770
+ break
771
+ }
772
+
773
+ }
774
+ },
775
+
776
+ freeze(key){
777
+ const freezed = this.columns.find(_ => _.freeze)
778
+ if(freezed){
779
+ delete freezed.freeze
780
+ }
781
+
782
+ const column = this.columns.find(_ => _.key === key)
783
+ if(column){
784
+ column.freeze = true
785
+ }
786
+ },
787
+
788
+ unfreeze(key){
789
+
790
+ const column = this.columns.find(_ => _.key === key)
791
+ if(column){
792
+ delete column.freeze
793
+ }
794
+ },
795
+
796
+ onVisibleChange({ inViewport }){
797
+ if(inViewport){
798
+ this.$addResizeListener(this.$el, () => this.compPrefix = this.$util.calculateMediaPrefix(this.$el.clientWidth))
799
+
800
+ this.loadPreset()
801
+ .finally(() => {
802
+ this.readyState = 1
803
+ this.$nextTick(() => {
804
+ this.load()
805
+ })
806
+ })
807
+
808
+ if(this.subscribeKey){
809
+ this.useSocket().send('user.subscribe', { name:this.subscribeKey })
810
+ this.useSocket().on(this.subscribeKey, this.onSignal)
811
+ }
812
+
813
+ window.addEventListener('keydown', this.onKeyDown)
814
+
815
+ this.useSocket().on('connect', this.onConnect)
816
+ }
817
+ },
818
+
819
+ },
820
+
821
+ components: {PresetBar, PresetSelector, VirtualTable},
822
+
823
+ unmounted() {
824
+ this.$removeResizeListener(this.$el)
825
+ window.removeEventListener('keydown', this.onKeyDown)
826
+
827
+ if(this.subscribeKey) {
828
+ this.useSocket().send('user.unsubscribe', {name: this.subscribeKey})
829
+ this.useSocket().off(this.subscribeKey, this.onSignal)
830
+ }
831
+
832
+ this.useSocket().off('connect', this.onConnect)
833
+ },
834
+
835
+ computed: {
836
+
837
+ columns(){
838
+ return ((this.preset ?? {}).pivot ?? {}).columns ?? []
839
+ },
840
+
841
+ cConfig(){
842
+ return setupConfig(this.config ?? this._config)
843
+ },
844
+
845
+ contentSlots(){
846
+ const slots = {}
847
+ for(let key in this.$slots){
848
+ if(!key.startsWith('col-'))
849
+ slots[key] = this.$slots[key]
850
+ }
851
+ return slots
852
+ },
853
+
854
+ headerSlots(){
855
+ const slots = {}
856
+ for(let key in this.$slots){
857
+ if(key.startsWith('col-'))
858
+ slots[key] = this.$slots[key]
859
+ }
860
+ return slots
861
+ },
862
+
863
+ computedPresetMode(){
864
+ return this.$device.type === 'mobile' ? 'popup' : this.presetMode
865
+ },
866
+
867
+ dataItems(){
868
+ // pivot results arrive ordered by the server (pivot.sorts falling back
869
+ // to preset.sorts) — no client-side re-sorting
870
+ if(typeof this.itemsFn === 'function'){
871
+ return this.itemsFn(this.data.items)
872
+ }
873
+ return this.data.items
874
+ },
875
+
876
+ // A PivotList preset is ALWAYS in pivot mode — that's the component's
877
+ // contract with the pivot-list server mixin.
878
+ preset(){
879
+ const preset = this.configPresets.find(_ => _.uid === this.configParams.presetIdx) ??
880
+ this.configSharedPresets.find(_ => _.uid === this.configParams.presetIdx)
881
+
882
+ if(preset){
883
+ if(!preset.pivot || typeof preset.pivot !== 'object' || Array.isArray(preset.pivot)){
884
+ preset.pivot = { rows:[], values:[], columns:[] }
885
+ }
886
+ preset.pivot.enabled = true
887
+
888
+ if(this.$route.query?.search)
889
+ preset.search = this.$route.query.search
890
+ }
891
+
892
+ return preset
893
+ },
894
+
895
+ pivotReady(){
896
+ const pivot = (this.preset ?? {}).pivot ?? {}
897
+ return (pivot.rows ?? []).length > 0 || (pivot.values ?? []).length > 0
898
+ },
899
+
900
+ virtualColumns(){
901
+ const pivot = (this.preset ?? {}).pivot
902
+ if(!pivot) return []
903
+ if(!Array.isArray(pivot.virtualColumns))
904
+ pivot.virtualColumns = []
905
+ return pivot.virtualColumns
906
+ },
907
+
908
+ // Insertable [refs] for the formula bar: every pivot result column that
909
+ // isn't itself a formula.
910
+ formulaRefs(){
911
+ return this.columns.filter(_ => _.key && !_.virtual).map(_ => _.key)
912
+ },
913
+
914
+ presetSrc(){
915
+ return this.controller ?
916
+ `${this.controller}.preset` :
917
+ 'user.preset'
918
+ },
919
+
920
+ configParams(){
921
+ if(!this.cConfig.params)
922
+ this.cConfig.params = {}
923
+
924
+ if(this.configPresets.length > 0 &&
925
+ (!this.cConfig.params.presetIdx ||
926
+ !this.configPresets.find(_ => _.uid === this.cConfig.params.presetIdx)))
927
+ this.cConfig.params.presetIdx = this.configPresets[0].uid
928
+
929
+ return this.cConfig.params
930
+ },
931
+
932
+ configPresets(){
933
+ return this.cConfig.presets ?? []
934
+ },
935
+
936
+ configSharedPresets(){
937
+ return this.cConfig.sharedPresets ?? []
938
+ },
939
+
940
+ canSearch(){
941
+ return [ 1, true, 'true' ].includes(this.searchable)
942
+ },
943
+
944
+ sidebar(){
945
+
946
+ if(!this.configParams.sidebar || !('open' in this.configParams.sidebar))
947
+ this.configParams.sidebar = {
948
+ open: false,
949
+ width: 360
950
+ }
951
+
952
+ return this.configParams.sidebar
953
+ },
954
+
955
+ sidebarStyle(){
956
+ return {
957
+ width: !this.sidebar.open ? 0 : this.sidebar.width + 'px'
958
+ }
959
+ },
960
+
961
+ computedSrc(){
962
+ return this.src ?? (this.controller ? `${this.controller}.pivot-load` : null)
963
+ },
964
+
965
+ freezeLeft(){
966
+ return this.columns.findIndex(_ => _.freeze) + 1
967
+ },
968
+
969
+ },
970
+
971
+ data(){
972
+ return {
973
+ readyState: 0,
974
+ data: {
975
+ items: []
976
+ },
977
+ compPrefix: '',
978
+ enumCache: {},
979
+ lastEnumItems: null,
980
+ formulaBar: {
981
+ key: '',
982
+ name: '',
983
+ formula: '',
984
+ error: '',
985
+ focused: false
986
+ },
987
+ _config: {
988
+ columns:[],
989
+ presets:[]
990
+ }
991
+ }
992
+ },
993
+
994
+ provide(){
995
+ return {
996
+ listStyle: this.$style,
997
+ load: this.load
998
+ }
999
+ },
1000
+
1001
+ watch: {
1002
+
1003
+ cConfig: {
1004
+ deep: true,
1005
+ handler(){
1006
+ this.savePreset()
1007
+ }
1008
+ },
1009
+
1010
+ },
1011
+
1012
+ }
1013
+
1014
+ </script>
1015
+
1016
+ <style module>
1017
+
1018
+ .resize1{
1019
+ @apply w-[3px] cursor-ew-resize absolute top-0 right-0 bottom-0;
1020
+ }
1021
+
1022
+ .pv-green1, .pv-green2, .pv-green3, .pv-green4, .pv-green5{
1023
+ @apply rounded-lg text-right text-text cursor-pointer;
1024
+ }
1025
+ .pv-green1{
1026
+ background-color: #d3f3df;
1027
+ }
1028
+ .pv-green2{
1029
+ background-color: #bdeecf;
1030
+ }
1031
+ .pv-green3{
1032
+ background-color: #a7e8bf;
1033
+ }
1034
+ .pv-green4{
1035
+ background-color: #91e2af;
1036
+ }
1037
+ .pv-green5{
1038
+ background-color: #7adc9e;
1039
+ }
1040
+ html[data-theme="dark"] .pv-green1{
1041
+ @apply text-text;
1042
+ background-color: #031409;
1043
+ }
1044
+ html[data-theme="dark"] .pv-green2{
1045
+ @apply text-text;
1046
+ background-color: #072713;
1047
+ }
1048
+ html[data-theme="dark"] .pv-green3{
1049
+ @apply text-text;
1050
+ background-color: #0a3b1c;
1051
+ }
1052
+ html[data-theme="dark"] .pv-green4{
1053
+ @apply text-text;
1054
+ background-color: #0e4f26;
1055
+ }
1056
+ html[data-theme="dark"] .pv-green5{
1057
+ @apply text-text;
1058
+ background-color: #11632f;
1059
+ }
1060
+
1061
+ .pv-red1, .pv-red2, .pv-red3, .pv-red4, .pv-red5{
1062
+ @apply rounded-lg text-right text-text cursor-pointer;
1063
+ }
1064
+ .pv-red1{
1065
+ background-color: #fce9e9;
1066
+ }
1067
+ .pv-red2{
1068
+ background-color: #f8d4d4;
1069
+ }
1070
+ .pv-red3{
1071
+ background-color: #f5bebe;
1072
+ }
1073
+ .pv-red4{
1074
+ background-color: #f1a8a8;
1075
+ }
1076
+ .pv-red5{
1077
+ background-color: #ee9393;
1078
+ }
1079
+ html[data-theme="dark"] .pv-red1{
1080
+ @apply text-text;
1081
+ background-color: #160404;
1082
+ }
1083
+ html[data-theme="dark"] .pv-red2{
1084
+ @apply text-text;
1085
+ background-color: #2c0808;
1086
+ }
1087
+ html[data-theme="dark"] .pv-red3{
1088
+ @apply text-text;
1089
+ background-color: #420b0b;
1090
+ }
1091
+ html[data-theme="dark"] .pv-red4{
1092
+ @apply text-text;
1093
+ background-color: #580f0f;
1094
+ }
1095
+ html[data-theme="dark"] .pv-red5{
1096
+ @apply text-text;
1097
+ background-color: #6e1313;
1098
+ }
1099
+
1100
+ .pv-blue1, .pv-blue2, .pv-blue3, .pv-blue4, .pv-blue5{
1101
+ @apply rounded-lg text-right text-text cursor-pointer;
1102
+ }
1103
+ .pv-blue1{
1104
+ background-color: #e9effd;
1105
+ }
1106
+ .pv-blue2{
1107
+ background-color: #d3e0fb;
1108
+ }
1109
+ .pv-blue3{
1110
+ background-color: #bed0f9;
1111
+ }
1112
+ .pv-blue4{
1113
+ background-color: #a8c1f7;
1114
+ }
1115
+ .pv-blue5{
1116
+ background-color: #92b1f5;
1117
+ }
1118
+ html[data-theme="dark"] .pv-blue1{
1119
+ @apply text-text;
1120
+ background-color: #040a17;
1121
+ }
1122
+ html[data-theme="dark"] .pv-blue2{
1123
+ @apply text-text;
1124
+ background-color: #07142f;
1125
+ }
1126
+ html[data-theme="dark"] .pv-blue3{
1127
+ @apply text-text;
1128
+ background-color: #0b1e46;
1129
+ }
1130
+ html[data-theme="dark"] .pv-blue4{
1131
+ @apply text-text;
1132
+ background-color: #0f285e;
1133
+ }
1134
+ html[data-theme="dark"] .pv-blue5{
1135
+ @apply text-text;
1136
+ background-color: #133276;
1137
+ }
1138
+
1139
+ .pv-yellow1, .pv-yellow2, .pv-yellow3, .pv-yellow4, .pv-yellow5{
1140
+ @apply rounded-lg text-right text-text cursor-pointer;
1141
+ }
1142
+ .pv-yellow1{
1143
+ background-color: #fdf7e6;
1144
+ }
1145
+ .pv-yellow2{
1146
+ background-color: #fbf0ce;
1147
+ }
1148
+ .pv-yellow3{
1149
+ background-color: #f9e8b5;
1150
+ }
1151
+ .pv-yellow4{
1152
+ background-color: #f7e19c;
1153
+ }
1154
+ .pv-yellow5{
1155
+ background-color: #f5d984;
1156
+ }
1157
+ html[data-theme="dark"] .pv-yellow1{
1158
+ @apply text-text;
1159
+ background-color: #171201;
1160
+ }
1161
+ html[data-theme="dark"] .pv-yellow2{
1162
+ @apply text-text;
1163
+ background-color: #2f2402;
1164
+ }
1165
+ html[data-theme="dark"] .pv-yellow3{
1166
+ @apply text-text;
1167
+ background-color: #463602;
1168
+ }
1169
+ html[data-theme="dark"] .pv-yellow4{
1170
+ @apply text-text;
1171
+ background-color: #5e4803;
1172
+ }
1173
+ html[data-theme="dark"] .pv-yellow5{
1174
+ @apply text-text;
1175
+ background-color: #755a04;
1176
+ }
1177
+
1178
+ .pv-fuchsia1, .pv-fuchsia2, .pv-fuchsia3, .pv-fuchsia4, .pv-fuchsia5{
1179
+ @apply rounded-lg text-right text-text cursor-pointer;
1180
+ }
1181
+ .pv-fuchsia1{
1182
+ background-color: #f6e8f7;
1183
+ }
1184
+ .pv-fuchsia2{
1185
+ background-color: #edd2ef;
1186
+ }
1187
+ .pv-fuchsia3{
1188
+ background-color: #e3bbe7;
1189
+ }
1190
+ .pv-fuchsia4{
1191
+ background-color: #daa4df;
1192
+ }
1193
+ .pv-fuchsia5{
1194
+ background-color: #d18ed7;
1195
+ }
1196
+ html[data-theme="dark"] .pv-fuchsia1{
1197
+ @apply text-text;
1198
+ background-color: #100311;
1199
+ }
1200
+ html[data-theme="dark"] .pv-fuchsia2{
1201
+ @apply text-text;
1202
+ background-color: #210623;
1203
+ }
1204
+ html[data-theme="dark"] .pv-fuchsia3{
1205
+ @apply text-text;
1206
+ background-color: #310834;
1207
+ }
1208
+ html[data-theme="dark"] .pv-fuchsia4{
1209
+ @apply text-text;
1210
+ background-color: #410b46;
1211
+ }
1212
+ html[data-theme="dark"] .pv-fuchsia5{
1213
+ @apply text-text;
1214
+ background-color: #520e58;
1215
+ }
1216
+
1217
+ .pv-gradient1, .pv-gradient2, .pv-gradient3, .pv-gradient4, .pv-gradient5, .pv-default{
1218
+ @apply rounded-lg text-right text-text cursor-pointer;
1219
+ }
1220
+ .pv-gradient1{
1221
+ @apply bg-gradient-to-r from-cyan-500 to-blue-500;
1222
+ }
1223
+ .pv-gradient2{
1224
+ @apply bg-gradient-to-r from-sky-500 to-indigo-500;
1225
+ }
1226
+ .pv-gradient3{
1227
+ @apply bg-gradient-to-r from-violet-500 to-fuchsia-500;
1228
+ }
1229
+ .pv-gradient4{
1230
+ @apply bg-gradient-to-r from-purple-500 to-pink-500;
1231
+ }
1232
+ .pv-gradient5{
1233
+ @apply bg-gradient-to-r from-pink-500 to-teal-500;
1234
+ }
1235
+ .pv-default{
1236
+ @apply text-primary;
1237
+ }
1238
+
1239
+ </style>