@escalated-dev/escalated 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +380 -361
  3. package/package.json +85 -84
  4. package/src/components/ActiveChatsPanel.vue +246 -246
  5. package/src/components/AdminDashboard.stories.js +1423 -1423
  6. package/src/components/AttachmentList.vue +65 -65
  7. package/src/components/ChatActionBar.vue +226 -226
  8. package/src/components/ChatBubble.vue +162 -162
  9. package/src/components/ChatComposer.vue +308 -308
  10. package/src/components/ChatQueue.vue +222 -222
  11. package/src/components/ChatThread.vue +197 -197
  12. package/src/components/CustomTicketActions.stories.js +71 -0
  13. package/src/components/CustomTicketActions.vue +101 -0
  14. package/src/components/EscalatedLayout.vue +619 -604
  15. package/src/components/FollowButton.vue +20 -1
  16. package/src/components/MentionDropdown.vue +154 -154
  17. package/src/components/MetricComparison.vue +89 -89
  18. package/src/components/PercentileChart.vue +67 -67
  19. package/src/components/PeriodSelector.vue +98 -98
  20. package/src/components/PresenceIndicator.vue +228 -213
  21. package/src/components/ReplyComposer.vue +336 -336
  22. package/src/components/ReplyThread.vue +153 -153
  23. package/src/components/ReportChart.vue +395 -395
  24. package/src/components/ReportExportButton.vue +108 -108
  25. package/src/components/SaveViewDialog.vue +172 -172
  26. package/src/components/SavedViewSidebar.vue +208 -208
  27. package/src/components/SkillTagManager.vue +3 -0
  28. package/src/components/SnoozeButton.vue +247 -247
  29. package/src/components/TicketFilters.vue +270 -270
  30. package/src/components/TicketList.vue +655 -655
  31. package/src/components/TicketSidebar.vue +4 -0
  32. package/src/components/TicketSplitDialog.vue +98 -98
  33. package/src/components/TicketSubjects.stories.js +63 -0
  34. package/src/components/TicketSubjects.vue +107 -0
  35. package/src/components/TwoFactorSetup.vue +1 -37
  36. package/src/components/VariableTokenInput.vue +111 -111
  37. package/src/components/WorkflowActionConfig.vue +287 -287
  38. package/src/components/WorkflowActionList.vue +350 -350
  39. package/src/components/WorkflowConditionBuilder.vue +265 -265
  40. package/src/components/WorkflowTriggerSelector.vue +125 -125
  41. package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
  42. package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
  43. package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
  44. package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
  45. package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
  46. package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
  47. package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
  48. package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
  49. package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
  50. package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
  51. package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
  52. package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
  53. package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
  54. package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
  55. package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
  56. package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
  57. package/src/composables/useChat.js +187 -187
  58. package/src/composables/useI18n.js +102 -102
  59. package/src/composables/useMentions.js +134 -134
  60. package/src/composables/usePermissions.js +31 -0
  61. package/src/composables/useRealtime.js +182 -182
  62. package/src/index.js +85 -84
  63. package/src/locales/README.md +13 -13
  64. package/src/locales/ar.json +727 -727
  65. package/src/locales/de.json +690 -690
  66. package/src/locales/en.json +889 -729
  67. package/src/locales/es.json +691 -691
  68. package/src/locales/fr.json +691 -691
  69. package/src/locales/index.js +16 -16
  70. package/src/locales/it.json +727 -727
  71. package/src/locales/ja.json +727 -727
  72. package/src/locales/ko.json +727 -727
  73. package/src/locales/nl.json +727 -727
  74. package/src/locales/pl.json +727 -727
  75. package/src/locales/pt-BR.json +727 -727
  76. package/src/locales/ru.json +727 -727
  77. package/src/locales/tr.json +727 -727
  78. package/src/locales/zh-CN.json +727 -727
  79. package/src/pages/Admin/Newsletters/Compose.vue +192 -0
  80. package/src/pages/Admin/Newsletters/Edit.vue +24 -0
  81. package/src/pages/Admin/Newsletters/Index.vue +141 -0
  82. package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
  83. package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
  84. package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
  85. package/src/pages/Admin/Newsletters/Settings.vue +85 -0
  86. package/src/pages/Admin/Newsletters/Show.vue +140 -0
  87. package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
  88. package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
  89. package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
  90. package/src/pages/Admin/Newsletters/usePreview.js +44 -0
  91. package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
  92. package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
  93. package/src/pages/Admin/Reports/Cohorts.vue +180 -180
  94. package/src/pages/Admin/Reports/Comparison.vue +141 -141
  95. package/src/pages/Admin/Reports/Dashboard.vue +226 -226
  96. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
  97. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
  98. package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
  99. package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
  100. package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
  101. package/src/pages/Admin/Settings.vue +1008 -1008
  102. package/src/pages/Admin/Skills/Form.vue +30 -10
  103. package/src/pages/Admin/Tickets/Index.vue +126 -126
  104. package/src/pages/Admin/Tickets/Show.vue +254 -249
  105. package/src/pages/Admin/Users/Index.vue +145 -145
  106. package/src/pages/Admin/Workflows/Builder.vue +207 -207
  107. package/src/pages/Admin/Workflows/Index.vue +235 -235
  108. package/src/pages/Admin/Workflows/Logs.vue +286 -286
  109. package/src/pages/Agent/TicketIndex.vue +112 -112
  110. package/src/pages/Agent/TicketShow.vue +547 -494
  111. package/src/pages/Customer/Index.vue +56 -56
  112. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
  113. package/src/plugin.js +175 -175
  114. package/src/utils/mentionHighlight.js +24 -24
  115. package/src/widget/EscalatedWidget.vue +804 -804
  116. package/src/widget/index.js +254 -254
@@ -1,395 +1,395 @@
1
- <script setup>
2
- import { computed, ref, onMounted } from 'vue';
3
-
4
- const props = defineProps({
5
- type: {
6
- type: String,
7
- default: 'bar',
8
- validator: (v) => ['line', 'bar', 'area', 'histogram', 'stacked-bar', 'pie'].includes(v),
9
- },
10
- data: { type: Array, default: () => [] },
11
- title: { type: String, default: '' },
12
- height: { type: Number, default: 240 },
13
- colors: { type: Array, default: () => ['#06b6d4', '#8b5cf6', '#f59e0b', '#10b981', '#ef4444', '#3b82f6'] },
14
- xLabel: { type: String, default: '' },
15
- yLabel: { type: String, default: '' },
16
- stacked: { type: Boolean, default: false },
17
- });
18
-
19
- const svgRef = ref(null);
20
- const tooltip = ref({ show: false, x: 0, y: 0, label: '', value: '' });
21
- const animated = ref(false);
22
-
23
- onMounted(() => {
24
- setTimeout(() => {
25
- animated.value = true;
26
- }, 50);
27
- });
28
-
29
- const padding = { top: 20, right: 20, bottom: 40, left: 50 };
30
- const chartWidth = 600;
31
- const chartHeight = computed(() => props.height);
32
- const innerWidth = chartWidth - padding.left - padding.right;
33
- const innerHeight = computed(() => chartHeight.value - padding.top - padding.bottom);
34
-
35
- const maxValue = computed(() => {
36
- if (!props.data.length) return 1;
37
- if (props.type === 'stacked-bar') {
38
- return Math.max(
39
- ...props.data.map((d) => {
40
- const vals = Array.isArray(d.values) ? d.values : [d.value || 0];
41
- return vals.reduce((a, b) => a + b, 0);
42
- }),
43
- 1,
44
- );
45
- }
46
- return Math.max(...props.data.map((d) => d.value || 0), 1);
47
- });
48
-
49
- const barWidth = computed(() => {
50
- if (!props.data.length) return 0;
51
- return Math.min(innerWidth / props.data.length - 4, 40);
52
- });
53
-
54
- const linePath = computed(() => {
55
- if (!props.data.length) return '';
56
- const stepX = innerWidth / Math.max(props.data.length - 1, 1);
57
- return props.data
58
- .map((d, i) => {
59
- const x = padding.left + i * stepX;
60
- const y = padding.top + innerHeight.value - (d.value / maxValue.value) * innerHeight.value;
61
- return `${i === 0 ? 'M' : 'L'}${x},${y}`;
62
- })
63
- .join(' ');
64
- });
65
-
66
- const areaPath = computed(() => {
67
- if (!props.data.length) return '';
68
- const stepX = innerWidth / Math.max(props.data.length - 1, 1);
69
- const baseline = padding.top + innerHeight.value;
70
- const top = props.data
71
- .map((d, i) => {
72
- const x = padding.left + i * stepX;
73
- const y = padding.top + innerHeight.value - (d.value / maxValue.value) * innerHeight.value;
74
- return `${i === 0 ? 'M' : 'L'}${x},${y}`;
75
- })
76
- .join(' ');
77
- const lastX = padding.left + (props.data.length - 1) * stepX;
78
- return `${top} L${lastX},${baseline} L${padding.left},${baseline} Z`;
79
- });
80
-
81
- const yTicks = computed(() => {
82
- const ticks = [];
83
- const steps = 5;
84
- for (let i = 0; i <= steps; i++) {
85
- const val = Math.round((maxValue.value / steps) * i);
86
- const y = padding.top + innerHeight.value - (val / maxValue.value) * innerHeight.value;
87
- ticks.push({ value: val, y });
88
- }
89
- return ticks;
90
- });
91
-
92
- const pieSlices = computed(() => {
93
- if (!props.data.length) return [];
94
- const total = props.data.reduce((s, d) => s + (d.value || 0), 0);
95
- if (total === 0) return [];
96
- const cx = chartWidth / 2;
97
- const cy = chartHeight.value / 2;
98
- const r = Math.min(cx, cy) - 30;
99
- let startAngle = -Math.PI / 2;
100
- return props.data.map((d, i) => {
101
- const angle = (d.value / total) * Math.PI * 2;
102
- const endAngle = startAngle + angle;
103
- const x1 = cx + r * Math.cos(startAngle);
104
- const y1 = cy + r * Math.sin(startAngle);
105
- const x2 = cx + r * Math.cos(endAngle);
106
- const y2 = cy + r * Math.sin(endAngle);
107
- const largeArc = angle > Math.PI ? 1 : 0;
108
- const path = `M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${largeArc} 1 ${x2},${y2} Z`;
109
- const midAngle = startAngle + angle / 2;
110
- const labelX = cx + r * 0.65 * Math.cos(midAngle);
111
- const labelY = cy + r * 0.65 * Math.sin(midAngle);
112
- startAngle = endAngle;
113
- return {
114
- path,
115
- color: props.colors[i % props.colors.length],
116
- label: d.label,
117
- value: d.value,
118
- percent: Math.round((d.value / total) * 100),
119
- labelX,
120
- labelY,
121
- };
122
- });
123
- });
124
-
125
- function showTooltip(event, label, value) {
126
- const rect = svgRef.value?.getBoundingClientRect();
127
- if (!rect) return;
128
- tooltip.value = {
129
- show: true,
130
- x: event.clientX - rect.left,
131
- y: event.clientY - rect.top - 10,
132
- label,
133
- value: typeof value === 'number' ? value.toLocaleString() : value,
134
- };
135
- }
136
-
137
- function hideTooltip() {
138
- tooltip.value.show = false;
139
- }
140
-
141
- function getBarX(index) {
142
- const step = innerWidth / props.data.length;
143
- return padding.left + index * step + (step - barWidth.value) / 2;
144
- }
145
-
146
- function getBarHeight(value) {
147
- return (value / maxValue.value) * innerHeight.value;
148
- }
149
-
150
- function getBarY(value) {
151
- return padding.top + innerHeight.value - getBarHeight(value);
152
- }
153
- </script>
154
-
155
- <template>
156
- <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
157
- <h3 v-if="title" class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">{{ title }}</h3>
158
- <div
159
- v-if="!data.length"
160
- class="flex items-center justify-center py-12 text-sm text-[var(--esc-panel-text-muted)]"
161
- >
162
- No data available
163
- </div>
164
- <div v-else class="relative">
165
- <svg
166
- ref="svgRef"
167
- :viewBox="`0 0 ${chartWidth} ${chartHeight}`"
168
- class="w-full"
169
- preserveAspectRatio="xMidYMid meet"
170
- >
171
- <!-- Grid lines -->
172
- <template v-if="type !== 'pie'">
173
- <line
174
- v-for="tick in yTicks"
175
- :key="tick.value"
176
- :x1="padding.left"
177
- :y1="tick.y"
178
- :x2="chartWidth - padding.right"
179
- :y2="tick.y"
180
- stroke="var(--esc-panel-border, #333)"
181
- stroke-width="0.5"
182
- stroke-dasharray="4 4"
183
- />
184
- <text
185
- v-for="tick in yTicks"
186
- :key="'label-' + tick.value"
187
- :x="padding.left - 8"
188
- :y="tick.y + 4"
189
- text-anchor="end"
190
- class="fill-[var(--esc-panel-text-muted)] text-[10px]"
191
- >
192
- {{ tick.value }}
193
- </text>
194
- </template>
195
-
196
- <!-- Bar chart -->
197
- <template v-if="type === 'bar' || type === 'histogram'">
198
- <rect
199
- v-for="(d, i) in data"
200
- :key="i"
201
- :x="getBarX(i)"
202
- :y="animated ? getBarY(d.value) : padding.top + innerHeight"
203
- :width="barWidth"
204
- :height="animated ? getBarHeight(d.value) : 0"
205
- :fill="d.color || colors[i % colors.length]"
206
- rx="3"
207
- class="transition-all duration-700 ease-out"
208
- style="cursor: pointer"
209
- @mouseenter="showTooltip($event, d.label, d.value)"
210
- @mouseleave="hideTooltip"
211
- />
212
- <text
213
- v-for="(d, i) in data"
214
- :key="'x-' + i"
215
- :x="getBarX(i) + barWidth / 2"
216
- :y="chartHeight - 8"
217
- text-anchor="middle"
218
- class="fill-[var(--esc-panel-text-muted)] text-[10px]"
219
- >
220
- {{ d.label?.length > 8 ? d.label.slice(0, 8) + '..' : d.label }}
221
- </text>
222
- </template>
223
-
224
- <!-- Stacked bar -->
225
- <template v-if="type === 'stacked-bar'">
226
- <template v-for="(d, i) in data" :key="i">
227
- <rect
228
- v-for="(val, si) in d.values || [d.value]"
229
- :key="si"
230
- :x="getBarX(i)"
231
- :y="
232
- animated
233
- ? getBarY((d.values || [d.value]).slice(0, si + 1).reduce((a, b) => a + b, 0))
234
- : padding.top + innerHeight
235
- "
236
- :width="barWidth"
237
- :height="animated ? getBarHeight(val) : 0"
238
- :fill="colors[si % colors.length]"
239
- class="transition-all duration-700 ease-out"
240
- style="cursor: pointer"
241
- @mouseenter="showTooltip($event, d.label + ' (' + (d.seriesLabels?.[si] || si) + ')', val)"
242
- @mouseleave="hideTooltip"
243
- />
244
- </template>
245
- <text
246
- v-for="(d, i) in data"
247
- :key="'sx-' + i"
248
- :x="getBarX(i) + barWidth / 2"
249
- :y="chartHeight - 8"
250
- text-anchor="middle"
251
- class="fill-[var(--esc-panel-text-muted)] text-[10px]"
252
- >
253
- {{ d.label?.length > 8 ? d.label.slice(0, 8) + '..' : d.label }}
254
- </text>
255
- </template>
256
-
257
- <!-- Line chart -->
258
- <template v-if="type === 'line'">
259
- <path
260
- :d="linePath"
261
- fill="none"
262
- :stroke="colors[0]"
263
- stroke-width="2.5"
264
- stroke-linecap="round"
265
- stroke-linejoin="round"
266
- class="transition-all duration-700"
267
- />
268
- <circle
269
- v-for="(d, i) in data"
270
- :key="i"
271
- :cx="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
272
- :cy="
273
- animated
274
- ? padding.top + innerHeight - (d.value / maxValue) * innerHeight
275
- : padding.top + innerHeight
276
- "
277
- r="4"
278
- :fill="colors[0]"
279
- class="transition-all duration-700 ease-out"
280
- style="cursor: pointer"
281
- @mouseenter="showTooltip($event, d.label, d.value)"
282
- @mouseleave="hideTooltip"
283
- />
284
- <text
285
- v-for="(d, i) in data"
286
- :key="'lx-' + i"
287
- :x="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
288
- :y="chartHeight - 8"
289
- text-anchor="middle"
290
- class="fill-[var(--esc-panel-text-muted)] text-[10px]"
291
- >
292
- {{ d.label?.length > 6 ? d.label.slice(0, 6) : d.label }}
293
- </text>
294
- </template>
295
-
296
- <!-- Area chart -->
297
- <template v-if="type === 'area'">
298
- <defs>
299
- <linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
300
- <stop offset="0%" :stop-color="colors[0]" stop-opacity="0.3" />
301
- <stop offset="100%" :stop-color="colors[0]" stop-opacity="0.02" />
302
- </linearGradient>
303
- </defs>
304
- <path :d="areaPath" fill="url(#areaGrad)" class="transition-all duration-700" />
305
- <path
306
- :d="linePath"
307
- fill="none"
308
- :stroke="colors[0]"
309
- stroke-width="2"
310
- stroke-linecap="round"
311
- stroke-linejoin="round"
312
- class="transition-all duration-700"
313
- />
314
- <circle
315
- v-for="(d, i) in data"
316
- :key="i"
317
- :cx="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
318
- :cy="
319
- animated
320
- ? padding.top + innerHeight - (d.value / maxValue) * innerHeight
321
- : padding.top + innerHeight
322
- "
323
- r="3.5"
324
- :fill="colors[0]"
325
- class="transition-all duration-700 ease-out"
326
- style="cursor: pointer"
327
- @mouseenter="showTooltip($event, d.label, d.value)"
328
- @mouseleave="hideTooltip"
329
- />
330
- </template>
331
-
332
- <!-- Pie chart -->
333
- <template v-if="type === 'pie'">
334
- <path
335
- v-for="(slice, i) in pieSlices"
336
- :key="i"
337
- :d="slice.path"
338
- :fill="slice.color"
339
- stroke="var(--esc-panel-surface, #111)"
340
- stroke-width="2"
341
- style="cursor: pointer"
342
- @mouseenter="showTooltip($event, slice.label, `${slice.value} (${slice.percent}%)`)"
343
- @mouseleave="hideTooltip"
344
- />
345
- <text
346
- v-for="(slice, i) in pieSlices"
347
- :key="'pl-' + i"
348
- :x="slice.labelX"
349
- :y="slice.labelY"
350
- text-anchor="middle"
351
- class="fill-white text-[10px] font-medium"
352
- >
353
- {{ slice.percent }}%
354
- </text>
355
- </template>
356
-
357
- <!-- Axis labels -->
358
- <text
359
- v-if="xLabel"
360
- :x="chartWidth / 2"
361
- :y="chartHeight - 2"
362
- text-anchor="middle"
363
- class="fill-[var(--esc-panel-text-muted)] text-[11px]"
364
- >
365
- {{ xLabel }}
366
- </text>
367
- </svg>
368
-
369
- <!-- Tooltip -->
370
- <div
371
- v-if="tooltip.show"
372
- class="pointer-events-none absolute z-20 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-active)] px-3 py-2 text-xs shadow-lg"
373
- :style="{ left: tooltip.x + 'px', top: tooltip.y + 'px', transform: 'translate(-50%, -100%)' }"
374
- >
375
- <div class="font-medium text-[var(--esc-panel-text)]">{{ tooltip.value }}</div>
376
- <div class="text-[var(--esc-panel-text-muted)]">{{ tooltip.label }}</div>
377
- </div>
378
- </div>
379
-
380
- <!-- Legend for stacked/pie -->
381
- <div v-if="(type === 'pie' || type === 'stacked-bar') && data.length" class="mt-4 flex flex-wrap gap-3">
382
- <div
383
- v-for="(d, i) in type === 'pie' ? data : data[0]?.seriesLabels || []"
384
- :key="i"
385
- class="flex items-center gap-1.5"
386
- >
387
- <span
388
- class="inline-block h-2.5 w-2.5 rounded-full"
389
- :style="{ backgroundColor: colors[i % colors.length] }"
390
- ></span>
391
- <span class="text-xs text-[var(--esc-panel-text-muted)]">{{ type === 'pie' ? d.label : d }}</span>
392
- </div>
393
- </div>
394
- </div>
395
- </template>
1
+ <script setup>
2
+ import { computed, ref, onMounted } from 'vue';
3
+
4
+ const props = defineProps({
5
+ type: {
6
+ type: String,
7
+ default: 'bar',
8
+ validator: (v) => ['line', 'bar', 'area', 'histogram', 'stacked-bar', 'pie'].includes(v),
9
+ },
10
+ data: { type: Array, default: () => [] },
11
+ title: { type: String, default: '' },
12
+ height: { type: Number, default: 240 },
13
+ colors: { type: Array, default: () => ['#06b6d4', '#8b5cf6', '#f59e0b', '#10b981', '#ef4444', '#3b82f6'] },
14
+ xLabel: { type: String, default: '' },
15
+ yLabel: { type: String, default: '' },
16
+ stacked: { type: Boolean, default: false },
17
+ });
18
+
19
+ const svgRef = ref(null);
20
+ const tooltip = ref({ show: false, x: 0, y: 0, label: '', value: '' });
21
+ const animated = ref(false);
22
+
23
+ onMounted(() => {
24
+ setTimeout(() => {
25
+ animated.value = true;
26
+ }, 50);
27
+ });
28
+
29
+ const padding = { top: 20, right: 20, bottom: 40, left: 50 };
30
+ const chartWidth = 600;
31
+ const chartHeight = computed(() => props.height);
32
+ const innerWidth = chartWidth - padding.left - padding.right;
33
+ const innerHeight = computed(() => chartHeight.value - padding.top - padding.bottom);
34
+
35
+ const maxValue = computed(() => {
36
+ if (!props.data.length) return 1;
37
+ if (props.type === 'stacked-bar') {
38
+ return Math.max(
39
+ ...props.data.map((d) => {
40
+ const vals = Array.isArray(d.values) ? d.values : [d.value || 0];
41
+ return vals.reduce((a, b) => a + b, 0);
42
+ }),
43
+ 1,
44
+ );
45
+ }
46
+ return Math.max(...props.data.map((d) => d.value || 0), 1);
47
+ });
48
+
49
+ const barWidth = computed(() => {
50
+ if (!props.data.length) return 0;
51
+ return Math.min(innerWidth / props.data.length - 4, 40);
52
+ });
53
+
54
+ const linePath = computed(() => {
55
+ if (!props.data.length) return '';
56
+ const stepX = innerWidth / Math.max(props.data.length - 1, 1);
57
+ return props.data
58
+ .map((d, i) => {
59
+ const x = padding.left + i * stepX;
60
+ const y = padding.top + innerHeight.value - (d.value / maxValue.value) * innerHeight.value;
61
+ return `${i === 0 ? 'M' : 'L'}${x},${y}`;
62
+ })
63
+ .join(' ');
64
+ });
65
+
66
+ const areaPath = computed(() => {
67
+ if (!props.data.length) return '';
68
+ const stepX = innerWidth / Math.max(props.data.length - 1, 1);
69
+ const baseline = padding.top + innerHeight.value;
70
+ const top = props.data
71
+ .map((d, i) => {
72
+ const x = padding.left + i * stepX;
73
+ const y = padding.top + innerHeight.value - (d.value / maxValue.value) * innerHeight.value;
74
+ return `${i === 0 ? 'M' : 'L'}${x},${y}`;
75
+ })
76
+ .join(' ');
77
+ const lastX = padding.left + (props.data.length - 1) * stepX;
78
+ return `${top} L${lastX},${baseline} L${padding.left},${baseline} Z`;
79
+ });
80
+
81
+ const yTicks = computed(() => {
82
+ const ticks = [];
83
+ const steps = 5;
84
+ for (let i = 0; i <= steps; i++) {
85
+ const val = Math.round((maxValue.value / steps) * i);
86
+ const y = padding.top + innerHeight.value - (val / maxValue.value) * innerHeight.value;
87
+ ticks.push({ value: val, y });
88
+ }
89
+ return ticks;
90
+ });
91
+
92
+ const pieSlices = computed(() => {
93
+ if (!props.data.length) return [];
94
+ const total = props.data.reduce((s, d) => s + (d.value || 0), 0);
95
+ if (total === 0) return [];
96
+ const cx = chartWidth / 2;
97
+ const cy = chartHeight.value / 2;
98
+ const r = Math.min(cx, cy) - 30;
99
+ let startAngle = -Math.PI / 2;
100
+ return props.data.map((d, i) => {
101
+ const angle = (d.value / total) * Math.PI * 2;
102
+ const endAngle = startAngle + angle;
103
+ const x1 = cx + r * Math.cos(startAngle);
104
+ const y1 = cy + r * Math.sin(startAngle);
105
+ const x2 = cx + r * Math.cos(endAngle);
106
+ const y2 = cy + r * Math.sin(endAngle);
107
+ const largeArc = angle > Math.PI ? 1 : 0;
108
+ const path = `M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${largeArc} 1 ${x2},${y2} Z`;
109
+ const midAngle = startAngle + angle / 2;
110
+ const labelX = cx + r * 0.65 * Math.cos(midAngle);
111
+ const labelY = cy + r * 0.65 * Math.sin(midAngle);
112
+ startAngle = endAngle;
113
+ return {
114
+ path,
115
+ color: props.colors[i % props.colors.length],
116
+ label: d.label,
117
+ value: d.value,
118
+ percent: Math.round((d.value / total) * 100),
119
+ labelX,
120
+ labelY,
121
+ };
122
+ });
123
+ });
124
+
125
+ function showTooltip(event, label, value) {
126
+ const rect = svgRef.value?.getBoundingClientRect();
127
+ if (!rect) return;
128
+ tooltip.value = {
129
+ show: true,
130
+ x: event.clientX - rect.left,
131
+ y: event.clientY - rect.top - 10,
132
+ label,
133
+ value: typeof value === 'number' ? value.toLocaleString() : value,
134
+ };
135
+ }
136
+
137
+ function hideTooltip() {
138
+ tooltip.value.show = false;
139
+ }
140
+
141
+ function getBarX(index) {
142
+ const step = innerWidth / props.data.length;
143
+ return padding.left + index * step + (step - barWidth.value) / 2;
144
+ }
145
+
146
+ function getBarHeight(value) {
147
+ return (value / maxValue.value) * innerHeight.value;
148
+ }
149
+
150
+ function getBarY(value) {
151
+ return padding.top + innerHeight.value - getBarHeight(value);
152
+ }
153
+ </script>
154
+
155
+ <template>
156
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
157
+ <h3 v-if="title" class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">{{ title }}</h3>
158
+ <div
159
+ v-if="!data.length"
160
+ class="flex items-center justify-center py-12 text-sm text-[var(--esc-panel-text-muted)]"
161
+ >
162
+ No data available
163
+ </div>
164
+ <div v-else class="relative">
165
+ <svg
166
+ ref="svgRef"
167
+ :viewBox="`0 0 ${chartWidth} ${chartHeight}`"
168
+ class="w-full"
169
+ preserveAspectRatio="xMidYMid meet"
170
+ >
171
+ <!-- Grid lines -->
172
+ <template v-if="type !== 'pie'">
173
+ <line
174
+ v-for="tick in yTicks"
175
+ :key="tick.value"
176
+ :x1="padding.left"
177
+ :y1="tick.y"
178
+ :x2="chartWidth - padding.right"
179
+ :y2="tick.y"
180
+ stroke="var(--esc-panel-border, #333)"
181
+ stroke-width="0.5"
182
+ stroke-dasharray="4 4"
183
+ />
184
+ <text
185
+ v-for="tick in yTicks"
186
+ :key="'label-' + tick.value"
187
+ :x="padding.left - 8"
188
+ :y="tick.y + 4"
189
+ text-anchor="end"
190
+ class="fill-[var(--esc-panel-text-muted)] text-[10px]"
191
+ >
192
+ {{ tick.value }}
193
+ </text>
194
+ </template>
195
+
196
+ <!-- Bar chart -->
197
+ <template v-if="type === 'bar' || type === 'histogram'">
198
+ <rect
199
+ v-for="(d, i) in data"
200
+ :key="i"
201
+ :x="getBarX(i)"
202
+ :y="animated ? getBarY(d.value) : padding.top + innerHeight"
203
+ :width="barWidth"
204
+ :height="animated ? getBarHeight(d.value) : 0"
205
+ :fill="d.color || colors[i % colors.length]"
206
+ rx="3"
207
+ class="transition-all duration-700 ease-out"
208
+ style="cursor: pointer"
209
+ @mouseenter="showTooltip($event, d.label, d.value)"
210
+ @mouseleave="hideTooltip"
211
+ />
212
+ <text
213
+ v-for="(d, i) in data"
214
+ :key="'x-' + i"
215
+ :x="getBarX(i) + barWidth / 2"
216
+ :y="chartHeight - 8"
217
+ text-anchor="middle"
218
+ class="fill-[var(--esc-panel-text-muted)] text-[10px]"
219
+ >
220
+ {{ d.label?.length > 8 ? d.label.slice(0, 8) + '..' : d.label }}
221
+ </text>
222
+ </template>
223
+
224
+ <!-- Stacked bar -->
225
+ <template v-if="type === 'stacked-bar'">
226
+ <template v-for="(d, i) in data" :key="i">
227
+ <rect
228
+ v-for="(val, si) in d.values || [d.value]"
229
+ :key="si"
230
+ :x="getBarX(i)"
231
+ :y="
232
+ animated
233
+ ? getBarY((d.values || [d.value]).slice(0, si + 1).reduce((a, b) => a + b, 0))
234
+ : padding.top + innerHeight
235
+ "
236
+ :width="barWidth"
237
+ :height="animated ? getBarHeight(val) : 0"
238
+ :fill="colors[si % colors.length]"
239
+ class="transition-all duration-700 ease-out"
240
+ style="cursor: pointer"
241
+ @mouseenter="showTooltip($event, d.label + ' (' + (d.seriesLabels?.[si] || si) + ')', val)"
242
+ @mouseleave="hideTooltip"
243
+ />
244
+ </template>
245
+ <text
246
+ v-for="(d, i) in data"
247
+ :key="'sx-' + i"
248
+ :x="getBarX(i) + barWidth / 2"
249
+ :y="chartHeight - 8"
250
+ text-anchor="middle"
251
+ class="fill-[var(--esc-panel-text-muted)] text-[10px]"
252
+ >
253
+ {{ d.label?.length > 8 ? d.label.slice(0, 8) + '..' : d.label }}
254
+ </text>
255
+ </template>
256
+
257
+ <!-- Line chart -->
258
+ <template v-if="type === 'line'">
259
+ <path
260
+ :d="linePath"
261
+ fill="none"
262
+ :stroke="colors[0]"
263
+ stroke-width="2.5"
264
+ stroke-linecap="round"
265
+ stroke-linejoin="round"
266
+ class="transition-all duration-700"
267
+ />
268
+ <circle
269
+ v-for="(d, i) in data"
270
+ :key="i"
271
+ :cx="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
272
+ :cy="
273
+ animated
274
+ ? padding.top + innerHeight - (d.value / maxValue) * innerHeight
275
+ : padding.top + innerHeight
276
+ "
277
+ r="4"
278
+ :fill="colors[0]"
279
+ class="transition-all duration-700 ease-out"
280
+ style="cursor: pointer"
281
+ @mouseenter="showTooltip($event, d.label, d.value)"
282
+ @mouseleave="hideTooltip"
283
+ />
284
+ <text
285
+ v-for="(d, i) in data"
286
+ :key="'lx-' + i"
287
+ :x="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
288
+ :y="chartHeight - 8"
289
+ text-anchor="middle"
290
+ class="fill-[var(--esc-panel-text-muted)] text-[10px]"
291
+ >
292
+ {{ d.label?.length > 6 ? d.label.slice(0, 6) : d.label }}
293
+ </text>
294
+ </template>
295
+
296
+ <!-- Area chart -->
297
+ <template v-if="type === 'area'">
298
+ <defs>
299
+ <linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
300
+ <stop offset="0%" :stop-color="colors[0]" stop-opacity="0.3" />
301
+ <stop offset="100%" :stop-color="colors[0]" stop-opacity="0.02" />
302
+ </linearGradient>
303
+ </defs>
304
+ <path :d="areaPath" fill="url(#areaGrad)" class="transition-all duration-700" />
305
+ <path
306
+ :d="linePath"
307
+ fill="none"
308
+ :stroke="colors[0]"
309
+ stroke-width="2"
310
+ stroke-linecap="round"
311
+ stroke-linejoin="round"
312
+ class="transition-all duration-700"
313
+ />
314
+ <circle
315
+ v-for="(d, i) in data"
316
+ :key="i"
317
+ :cx="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
318
+ :cy="
319
+ animated
320
+ ? padding.top + innerHeight - (d.value / maxValue) * innerHeight
321
+ : padding.top + innerHeight
322
+ "
323
+ r="3.5"
324
+ :fill="colors[0]"
325
+ class="transition-all duration-700 ease-out"
326
+ style="cursor: pointer"
327
+ @mouseenter="showTooltip($event, d.label, d.value)"
328
+ @mouseleave="hideTooltip"
329
+ />
330
+ </template>
331
+
332
+ <!-- Pie chart -->
333
+ <template v-if="type === 'pie'">
334
+ <path
335
+ v-for="(slice, i) in pieSlices"
336
+ :key="i"
337
+ :d="slice.path"
338
+ :fill="slice.color"
339
+ stroke="var(--esc-panel-surface, #111)"
340
+ stroke-width="2"
341
+ style="cursor: pointer"
342
+ @mouseenter="showTooltip($event, slice.label, `${slice.value} (${slice.percent}%)`)"
343
+ @mouseleave="hideTooltip"
344
+ />
345
+ <text
346
+ v-for="(slice, i) in pieSlices"
347
+ :key="'pl-' + i"
348
+ :x="slice.labelX"
349
+ :y="slice.labelY"
350
+ text-anchor="middle"
351
+ class="fill-white text-[10px] font-medium"
352
+ >
353
+ {{ slice.percent }}%
354
+ </text>
355
+ </template>
356
+
357
+ <!-- Axis labels -->
358
+ <text
359
+ v-if="xLabel"
360
+ :x="chartWidth / 2"
361
+ :y="chartHeight - 2"
362
+ text-anchor="middle"
363
+ class="fill-[var(--esc-panel-text-muted)] text-[11px]"
364
+ >
365
+ {{ xLabel }}
366
+ </text>
367
+ </svg>
368
+
369
+ <!-- Tooltip -->
370
+ <div
371
+ v-if="tooltip.show"
372
+ class="pointer-events-none absolute z-20 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-active)] px-3 py-2 text-xs shadow-lg"
373
+ :style="{ left: tooltip.x + 'px', top: tooltip.y + 'px', transform: 'translate(-50%, -100%)' }"
374
+ >
375
+ <div class="font-medium text-[var(--esc-panel-text)]">{{ tooltip.value }}</div>
376
+ <div class="text-[var(--esc-panel-text-muted)]">{{ tooltip.label }}</div>
377
+ </div>
378
+ </div>
379
+
380
+ <!-- Legend for stacked/pie -->
381
+ <div v-if="(type === 'pie' || type === 'stacked-bar') && data.length" class="mt-4 flex flex-wrap gap-3">
382
+ <div
383
+ v-for="(d, i) in type === 'pie' ? data : data[0]?.seriesLabels || []"
384
+ :key="i"
385
+ class="flex items-center gap-1.5"
386
+ >
387
+ <span
388
+ class="inline-block h-2.5 w-2.5 rounded-full"
389
+ :style="{ backgroundColor: colors[i % colors.length] }"
390
+ ></span>
391
+ <span class="text-xs text-[var(--esc-panel-text-muted)]">{{ type === 'pie' ? d.label : d }}</span>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ </template>