@escalated-dev/escalated 0.6.0 → 0.7.1

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 (187) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +125 -2
  3. package/package.json +40 -5
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -0
  6. package/src/components/ActivityTimeline.vue +83 -66
  7. package/src/components/AdminDashboard.stories.js +1423 -0
  8. package/src/components/AgentLoadIndicator.stories.js +37 -0
  9. package/src/components/AgentLoadIndicator.vue +49 -0
  10. package/src/components/AgentPerformanceCard.vue +44 -0
  11. package/src/components/ArticleEditor.vue +82 -0
  12. package/src/components/AssigneeSelect.vue +37 -23
  13. package/src/components/AttachmentList.vue +65 -37
  14. package/src/components/AuditLogEntry.vue +95 -0
  15. package/src/components/BulkActionBar.vue +166 -97
  16. package/src/components/CategoryTree.vue +89 -0
  17. package/src/components/ChartWidget.vue +46 -0
  18. package/src/components/ChatActionBar.vue +226 -0
  19. package/src/components/ChatBubble.vue +162 -0
  20. package/src/components/ChatComposer.vue +308 -0
  21. package/src/components/ChatQueue.vue +222 -0
  22. package/src/components/ChatThread.vue +197 -0
  23. package/src/components/CollisionWarning.stories.js +30 -0
  24. package/src/components/CollisionWarning.vue +68 -0
  25. package/src/components/ConditionalFieldRules.vue +135 -0
  26. package/src/components/ContextPanel.vue +66 -0
  27. package/src/components/ContextPanelSection.vue +65 -0
  28. package/src/components/CustomFieldRenderer.vue +159 -0
  29. package/src/components/EscalatedLayout.vue +424 -70
  30. package/src/components/FileDropzone.vue +57 -41
  31. package/src/components/FollowButton.stories.js +30 -0
  32. package/src/components/FollowButton.vue +88 -63
  33. package/src/components/KeyboardShortcutHelp.vue +213 -123
  34. package/src/components/KnowledgePanel.vue +122 -0
  35. package/src/components/KpiCard.stories.js +32 -0
  36. package/src/components/KpiCard.vue +88 -0
  37. package/src/components/MacroDropdown.vue +150 -90
  38. package/src/components/MentionDropdown.vue +154 -0
  39. package/src/components/MetricComparison.vue +89 -0
  40. package/src/components/PercentileChart.vue +67 -0
  41. package/src/components/PeriodSelector.vue +98 -0
  42. package/src/components/PermissionMatrix.vue +107 -0
  43. package/src/components/PinnedNotes.vue +129 -90
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +138 -28
  46. package/src/components/PriorityBadge.stories.js +33 -0
  47. package/src/components/PriorityBadge.vue +46 -39
  48. package/src/components/QuickFilters.vue +65 -49
  49. package/src/components/ReplyComposer.vue +207 -30
  50. package/src/components/ReplyThread.vue +111 -21
  51. package/src/components/ReportChart.vue +395 -0
  52. package/src/components/ReportExportButton.vue +108 -0
  53. package/src/components/SatisfactionRating.vue +180 -128
  54. package/src/components/SaveViewDialog.vue +172 -0
  55. package/src/components/SavedViewSidebar.vue +208 -0
  56. package/src/components/ScheduleEditor.vue +104 -0
  57. package/src/components/SideConversation.vue +165 -0
  58. package/src/components/SideConversationList.vue +147 -0
  59. package/src/components/SkillTagManager.vue +79 -0
  60. package/src/components/SlaComplianceChart.vue +46 -0
  61. package/src/components/SlaTimer.stories.js +35 -0
  62. package/src/components/SlaTimer.vue +62 -49
  63. package/src/components/SnoozeButton.vue +247 -0
  64. package/src/components/StatsCard.stories.js +24 -0
  65. package/src/components/StatsCard.vue +18 -6
  66. package/src/components/StatusBadge.stories.js +53 -0
  67. package/src/components/StatusBadge.vue +78 -46
  68. package/src/components/TagSelect.vue +84 -53
  69. package/src/components/TicketFilters.vue +247 -46
  70. package/src/components/TicketLinkPanel.vue +193 -0
  71. package/src/components/TicketList.vue +536 -74
  72. package/src/components/TicketMergeDialog.vue +157 -0
  73. package/src/components/TicketSidebar.vue +167 -99
  74. package/src/components/TicketSplitDialog.vue +98 -0
  75. package/src/components/TicketTypeSelector.vue +21 -0
  76. package/src/components/TotpInput.stories.js +9 -0
  77. package/src/components/TotpInput.vue +81 -0
  78. package/src/components/TwoFactorSetup.vue +113 -0
  79. package/src/components/VariableTokenInput.vue +111 -0
  80. package/src/components/WorkflowActionConfig.vue +287 -0
  81. package/src/components/WorkflowActionList.vue +350 -0
  82. package/src/components/WorkflowConditionBuilder.vue +265 -0
  83. package/src/components/WorkflowTriggerSelector.vue +125 -0
  84. package/src/composables/useChat.js +187 -0
  85. package/src/composables/useDebouncedSearch.js +38 -0
  86. package/src/composables/useI18n.js +80 -80
  87. package/src/composables/useKeyboardShortcuts.js +46 -46
  88. package/src/composables/useMentions.js +134 -0
  89. package/src/composables/usePluginExtensions.js +159 -67
  90. package/src/composables/usePluginHooks.js +65 -0
  91. package/src/composables/useRealtime.js +182 -0
  92. package/src/defineEscalatedPlugin.js +76 -50
  93. package/src/index.js +77 -28
  94. package/src/locales/ar.json +727 -0
  95. package/src/locales/de.json +678 -430
  96. package/src/locales/en.json +717 -430
  97. package/src/locales/es.json +679 -430
  98. package/src/locales/fr.json +680 -431
  99. package/src/locales/index.js +12 -2
  100. package/src/locales/it.json +727 -0
  101. package/src/locales/ja.json +727 -0
  102. package/src/locales/ko.json +727 -0
  103. package/src/locales/nl.json +727 -0
  104. package/src/locales/pl.json +727 -0
  105. package/src/locales/pt-BR.json +727 -0
  106. package/src/locales/ru.json +727 -0
  107. package/src/locales/tr.json +727 -0
  108. package/src/locales/zh-CN.json +727 -0
  109. package/src/pages/Admin/ApiTokens/Index.vue +381 -217
  110. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  111. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  112. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  113. package/src/pages/Admin/CannedResponses/Index.vue +82 -25
  114. package/src/pages/Admin/Capacity/Index.vue +134 -0
  115. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  116. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  117. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  118. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  119. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  120. package/src/pages/Admin/Departments/Form.vue +35 -11
  121. package/src/pages/Admin/Departments/Index.vue +74 -21
  122. package/src/pages/Admin/EscalationRules/Form.vue +86 -25
  123. package/src/pages/Admin/EscalationRules/Index.vue +158 -37
  124. package/src/pages/Admin/Import/Connect.vue +95 -0
  125. package/src/pages/Admin/Import/Index.vue +102 -0
  126. package/src/pages/Admin/Import/Mapping.vue +110 -0
  127. package/src/pages/Admin/Import/Progress.vue +153 -0
  128. package/src/pages/Admin/Import/Review.vue +76 -0
  129. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  130. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  131. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  132. package/src/pages/Admin/Macros/Index.vue +448 -287
  133. package/src/pages/Admin/Plugins/Index.vue +327 -197
  134. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  135. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  136. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  137. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  138. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  139. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  140. package/src/pages/Admin/Reports/Dashboard.vue +226 -0
  141. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  142. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  143. package/src/pages/Admin/Reports/SlaReport.vue +162 -0
  144. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  145. package/src/pages/Admin/Reports.vue +206 -101
  146. package/src/pages/Admin/Roles/Form.vue +71 -0
  147. package/src/pages/Admin/Roles/Index.vue +122 -0
  148. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  149. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  150. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  151. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  152. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  153. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  154. package/src/pages/Admin/Settings.vue +847 -99
  155. package/src/pages/Admin/Skills/Form.vue +53 -0
  156. package/src/pages/Admin/Skills/Index.vue +94 -0
  157. package/src/pages/Admin/SlaPolicies/Form.vue +62 -19
  158. package/src/pages/Admin/SlaPolicies/Index.vue +85 -23
  159. package/src/pages/Admin/Statuses/Form.vue +114 -0
  160. package/src/pages/Admin/Statuses/Index.vue +153 -0
  161. package/src/pages/Admin/Tags/Index.vue +121 -35
  162. package/src/pages/Admin/Tickets/Index.vue +96 -9
  163. package/src/pages/Admin/Tickets/Show.vue +130 -37
  164. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  165. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  166. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  167. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  168. package/src/pages/Admin/Workflows/Index.vue +235 -0
  169. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  170. package/src/pages/Agent/Dashboard.vue +311 -186
  171. package/src/pages/Agent/TicketIndex.vue +80 -6
  172. package/src/pages/Agent/TicketShow.vue +399 -60
  173. package/src/pages/Customer/Create.vue +37 -10
  174. package/src/pages/Customer/Index.vue +9 -2
  175. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -0
  176. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  177. package/src/pages/Customer/Show.vue +69 -63
  178. package/src/pages/Guest/Create.vue +40 -15
  179. package/src/pages/Guest/Show.vue +124 -93
  180. package/src/plugin.js +111 -1
  181. package/src/utils/constants.js +30 -0
  182. package/src/utils/formatting.js +35 -0
  183. package/src/utils/mentionHighlight.js +24 -0
  184. package/src/utils/sanitizeHtml.js +78 -64
  185. package/src/widget/EscalatedWidget.vue +804 -0
  186. package/src/widget/index.js +254 -0
  187. package/src/pages/Admin/Plugins.vue +0 -129
@@ -0,0 +1,122 @@
1
+ <script setup>
2
+ import { ref } from 'vue';
3
+
4
+ defineProps({
5
+ ticketReference: { type: String, required: true },
6
+ });
7
+
8
+ const emit = defineEmits(['insert-link', 'quote']);
9
+ const searchTerm = ref('');
10
+ const expanded = ref(true);
11
+
12
+ // Placeholder results for knowledge base integration
13
+ const results = ref([]);
14
+ const loading = ref(false);
15
+ let debounceTimer = null;
16
+
17
+ function onSearch(val) {
18
+ window.clearTimeout(debounceTimer);
19
+ if (!val || val.length < 2) {
20
+ results.value = [];
21
+ return;
22
+ }
23
+ debounceTimer = window.setTimeout(() => search(val), 300);
24
+ }
25
+
26
+ function search(term) {
27
+ loading.value = true;
28
+ // Placeholder: in production, this would call a knowledge base API
29
+ results.value = [
30
+ {
31
+ id: 1,
32
+ title: `How to resolve "${term}" issues`,
33
+ excerpt: `This article explains common solutions for ${term}-related problems that customers frequently encounter.`,
34
+ url: '#',
35
+ },
36
+ {
37
+ id: 2,
38
+ title: `${term} - FAQ`,
39
+ excerpt: `Frequently asked questions about ${term} and related features.`,
40
+ url: '#',
41
+ },
42
+ {
43
+ id: 3,
44
+ title: `Troubleshooting ${term}`,
45
+ excerpt: `Step-by-step troubleshooting guide for ${term} problems.`,
46
+ url: '#',
47
+ },
48
+ ];
49
+ loading.value = false;
50
+ }
51
+
52
+ function insertLink(article) {
53
+ emit('insert-link', article);
54
+ }
55
+
56
+ function quoteArticle(article) {
57
+ emit('quote', article);
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <div class="rounded-xl border border-white/[0.06] bg-neutral-900/60 p-4">
63
+ <button
64
+ class="flex w-full items-center justify-between text-sm font-semibold text-neutral-200"
65
+ @click="expanded = !expanded"
66
+ >
67
+ <span>Knowledge Base</span>
68
+ <svg
69
+ :class="['h-4 w-4 text-neutral-500 transition-transform', expanded && 'rotate-180']"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ stroke-width="1.5"
73
+ viewBox="0 0 24 24"
74
+ >
75
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
76
+ </svg>
77
+ </button>
78
+
79
+ <div v-if="expanded" class="mt-3 space-y-3">
80
+ <input
81
+ v-model="searchTerm"
82
+ type="text"
83
+ placeholder="Search knowledge base..."
84
+ class="w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
85
+ @input="onSearch(searchTerm)"
86
+ />
87
+
88
+ <div v-if="loading" class="text-xs text-neutral-500">Searching...</div>
89
+
90
+ <div v-else-if="results.length" class="space-y-2">
91
+ <div
92
+ v-for="article in results"
93
+ :key="article.id"
94
+ class="rounded-lg border border-white/[0.04] bg-white/[0.02] p-3"
95
+ >
96
+ <h4 class="text-sm font-medium text-neutral-200">{{ article.title }}</h4>
97
+ <p class="mt-1 text-xs text-neutral-400 line-clamp-2">{{ article.excerpt }}</p>
98
+ <div class="mt-2 flex gap-2">
99
+ <button
100
+ class="rounded px-2 py-0.5 text-[10px] font-medium text-cyan-400 ring-1 ring-cyan-500/20 transition-colors hover:bg-cyan-500/10"
101
+ @click="insertLink(article)"
102
+ >
103
+ Insert Link
104
+ </button>
105
+ <button
106
+ class="rounded px-2 py-0.5 text-[10px] font-medium text-violet-400 ring-1 ring-violet-500/20 transition-colors hover:bg-violet-500/10"
107
+ @click="quoteArticle(article)"
108
+ >
109
+ Quote
110
+ </button>
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <div v-else-if="searchTerm.length >= 2 && !loading" class="text-xs text-neutral-500">
116
+ No articles found.
117
+ </div>
118
+
119
+ <div v-else class="text-xs text-neutral-500">Search for articles to help resolve this ticket.</div>
120
+ </div>
121
+ </div>
122
+ </template>
@@ -0,0 +1,32 @@
1
+ import KpiCard from './KpiCard.vue';
2
+
3
+ export default {
4
+ title: 'Components/KpiCard',
5
+ component: KpiCard,
6
+ };
7
+
8
+ export const Basic = { args: { label: 'Total Tickets', value: 1284 } };
9
+ export const WithPositiveTrend = { args: { label: 'Resolved', value: 892, trend: 12 } };
10
+ export const WithNegativeTrend = { args: { label: 'SLA Breaches', value: 7, trend: -34 } };
11
+
12
+ export const WithIcon = {
13
+ args: {
14
+ label: 'Active Agents',
15
+ value: 24,
16
+ trend: 8,
17
+ icon: 'M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z',
18
+ },
19
+ };
20
+
21
+ export const DashboardRow = {
22
+ render: () => ({
23
+ components: { KpiCard },
24
+ template: `
25
+ <div class="grid grid-cols-3 gap-4" style="max-width: 800px;">
26
+ <KpiCard label="Total Tickets" :value="1284" :trend="5" icon="M2.25 13.5h3.86a2.25 2.25 0 012.012 1.244l.256.512a2.25 2.25 0 002.013 1.244h3.218a2.25 2.25 0 002.013-1.244l.256-.512a2.25 2.25 0 012.013-1.244h3.859" />
27
+ <KpiCard label="Avg Response Time" value="1h 42m" :trend="-18" icon="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
28
+ <KpiCard label="CSAT Score" value="94%" :trend="3" icon="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
29
+ </div>
30
+ `,
31
+ }),
32
+ };
@@ -0,0 +1,88 @@
1
+ <script setup>
2
+ import { inject, computed } from 'vue';
3
+
4
+ defineProps({
5
+ label: { type: String, required: true },
6
+ value: { type: [Number, String], required: true },
7
+ trend: { type: Number, default: null },
8
+ icon: { type: String, default: null },
9
+ });
10
+
11
+ const escDark = inject(
12
+ 'esc-dark',
13
+ computed(() => false),
14
+ );
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ :class="[
20
+ 'rounded-xl border p-5',
21
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
22
+ ]"
23
+ >
24
+ <div class="flex items-start justify-between">
25
+ <div>
26
+ <div :class="['text-[13px] font-medium', escDark ? 'text-neutral-500' : 'text-gray-500']">
27
+ {{ label }}
28
+ </div>
29
+ <div :class="['mt-2 text-2xl font-bold tracking-tight', escDark ? 'text-white' : 'text-gray-900']">
30
+ {{ value }}
31
+ </div>
32
+ </div>
33
+ <div
34
+ v-if="icon"
35
+ :class="[
36
+ 'flex h-10 w-10 items-center justify-center rounded-lg',
37
+ escDark ? 'bg-white/[0.04]' : 'bg-gray-100',
38
+ ]"
39
+ >
40
+ <svg
41
+ :class="['h-5 w-5', escDark ? 'text-neutral-500' : 'text-gray-400']"
42
+ fill="none"
43
+ stroke="currentColor"
44
+ stroke-width="1.5"
45
+ viewBox="0 0 24 24"
46
+ >
47
+ <path stroke-linecap="round" stroke-linejoin="round" :d="icon" />
48
+ </svg>
49
+ </div>
50
+ </div>
51
+ <div v-if="trend !== null && trend !== 0" class="mt-3 flex items-center gap-1">
52
+ <svg
53
+ v-if="trend > 0"
54
+ :class="['h-4 w-4', escDark ? 'text-emerald-400' : 'text-emerald-500']"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="2"
58
+ viewBox="0 0 24 24"
59
+ >
60
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
61
+ </svg>
62
+ <svg
63
+ v-else
64
+ :class="['h-4 w-4', escDark ? 'text-red-400' : 'text-red-500']"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ stroke-width="2"
68
+ viewBox="0 0 24 24"
69
+ >
70
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 4.5l15 15m0 0V8.25m0 11.25H8.25" />
71
+ </svg>
72
+ <span
73
+ :class="[
74
+ 'text-xs font-medium',
75
+ trend > 0
76
+ ? escDark
77
+ ? 'text-emerald-400'
78
+ : 'text-emerald-600'
79
+ : escDark
80
+ ? 'text-red-400'
81
+ : 'text-red-600',
82
+ ]"
83
+ >
84
+ {{ Math.abs(trend) }}%
85
+ </span>
86
+ </div>
87
+ </div>
88
+ </template>
@@ -1,90 +1,150 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
- import { router } from '@inertiajs/vue3';
4
-
5
- const props = defineProps({
6
- macros: { type: Array, default: () => [] },
7
- action: { type: String, required: true },
8
- });
9
-
10
- const escDark = inject('esc-dark', computed(() => false));
11
- const open = ref(false);
12
- const processing = ref(false);
13
-
14
- function applyMacro(macroId) {
15
- if (processing.value) return;
16
- processing.value = true;
17
- router.post(props.action, { macro_id: macroId }, {
18
- preserveScroll: true,
19
- onFinish: () => {
20
- processing.value = false;
21
- open.value = false;
22
- },
23
- });
24
- }
25
-
26
- function toggle() {
27
- open.value = !open.value;
28
- }
29
-
30
- function close() {
31
- open.value = false;
32
- }
33
- </script>
34
-
35
- <template>
36
- <div class="relative">
37
- <!-- Trigger button -->
38
- <button @click="toggle"
39
- :class="['inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
40
- escDark
41
- ? 'border border-white/10 bg-white/[0.03] text-neutral-300 hover:bg-white/[0.06]'
42
- : 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50']"
43
- :disabled="processing">
44
- <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
45
- <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
46
- </svg>
47
- Apply Macro
48
- <svg :class="['h-4 w-4 transition-transform', open && 'rotate-180']" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
49
- <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
50
- </svg>
51
- </button>
52
-
53
- <!-- Backdrop -->
54
- <div v-if="open" class="fixed inset-0 z-30" @click="close"></div>
55
-
56
- <!-- Dropdown -->
57
- <Transition enter-active-class="transition duration-100 ease-out" enter-from-class="scale-95 opacity-0" enter-to-class="scale-100 opacity-100"
58
- leave-active-class="transition duration-75 ease-in" leave-from-class="scale-100 opacity-100" leave-to-class="scale-95 opacity-0">
59
- <div v-if="open"
60
- :class="['absolute right-0 z-40 mt-1 w-64 overflow-hidden rounded-xl border shadow-xl',
61
- escDark ? 'border-white/[0.06] bg-neutral-900' : 'border-gray-200 bg-white']">
62
- <div :class="['px-3 py-2 text-xs font-semibold uppercase tracking-wider',
63
- escDark ? 'text-neutral-500' : 'text-gray-500']">
64
- Macros
65
- </div>
66
- <div class="max-h-64 overflow-y-auto">
67
- <div v-if="!macros.length"
68
- :class="['px-3 py-4 text-center text-sm', escDark ? 'text-neutral-500' : 'text-gray-400']">
69
- No macros available
70
- </div>
71
- <button v-for="macro in macros" :key="macro.id"
72
- @click="applyMacro(macro.id)"
73
- :disabled="processing"
74
- :class="['flex w-full flex-col px-3 py-2.5 text-left transition-colors',
75
- escDark
76
- ? 'hover:bg-white/[0.04] disabled:opacity-50'
77
- : 'hover:bg-gray-50 disabled:opacity-50']">
78
- <span :class="['text-sm font-medium', escDark ? 'text-neutral-200' : 'text-gray-900']">
79
- {{ macro.name }}
80
- </span>
81
- <span v-if="macro.description"
82
- :class="['mt-0.5 text-xs', escDark ? 'text-neutral-500' : 'text-gray-500']">
83
- {{ macro.description }}
84
- </span>
85
- </button>
86
- </div>
87
- </div>
88
- </Transition>
89
- </div>
90
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ macros: { type: Array, default: () => [] },
7
+ action: { type: String, required: true },
8
+ });
9
+
10
+ const escDark = inject(
11
+ 'esc-dark',
12
+ computed(() => false),
13
+ );
14
+ const open = ref(false);
15
+ const processing = ref(false);
16
+
17
+ function applyMacro(macroId) {
18
+ if (processing.value) return;
19
+ processing.value = true;
20
+ router.post(
21
+ props.action,
22
+ { macro_id: macroId },
23
+ {
24
+ preserveScroll: true,
25
+ onFinish: () => {
26
+ processing.value = false;
27
+ open.value = false;
28
+ },
29
+ },
30
+ );
31
+ }
32
+
33
+ function toggle() {
34
+ open.value = !open.value;
35
+ }
36
+
37
+ function close() {
38
+ open.value = false;
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <div class="relative">
44
+ <!-- Trigger button -->
45
+ <button
46
+ :class="[
47
+ 'inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
48
+ escDark
49
+ ? 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-hover)]'
50
+ : 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
51
+ ]"
52
+ :disabled="processing"
53
+ aria-label="Apply macro"
54
+ :aria-expanded="open"
55
+ @click="toggle"
56
+ >
57
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
58
+ <path
59
+ stroke-linecap="round"
60
+ stroke-linejoin="round"
61
+ d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
62
+ />
63
+ </svg>
64
+ Apply Macro
65
+ <svg
66
+ :class="['h-4 w-4 transition-transform', open && 'rotate-180']"
67
+ fill="none"
68
+ stroke="currentColor"
69
+ stroke-width="1.5"
70
+ viewBox="0 0 24 24"
71
+ >
72
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
73
+ </svg>
74
+ </button>
75
+
76
+ <!-- Backdrop -->
77
+ <div v-if="open" class="fixed inset-0 z-30" @click="close"></div>
78
+
79
+ <!-- Dropdown -->
80
+ <Transition
81
+ enter-active-class="transition duration-100 ease-out"
82
+ enter-from-class="scale-95 opacity-0"
83
+ enter-to-class="scale-100 opacity-100"
84
+ leave-active-class="transition duration-75 ease-in"
85
+ leave-from-class="scale-100 opacity-100"
86
+ leave-to-class="scale-95 opacity-0"
87
+ >
88
+ <div
89
+ v-if="open"
90
+ :class="[
91
+ 'absolute right-0 z-40 mt-1 w-64 overflow-hidden rounded-xl border shadow-xl',
92
+ escDark
93
+ ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]'
94
+ : 'border-gray-200 bg-white',
95
+ ]"
96
+ >
97
+ <div
98
+ :class="[
99
+ 'px-3 py-2 text-xs font-semibold uppercase tracking-wider',
100
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
101
+ ]"
102
+ >
103
+ Macros
104
+ </div>
105
+ <div class="max-h-64 overflow-y-auto" role="menu">
106
+ <div
107
+ v-if="!macros.length"
108
+ :class="[
109
+ 'px-3 py-4 text-center text-sm',
110
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400',
111
+ ]"
112
+ >
113
+ No macros available
114
+ </div>
115
+ <button
116
+ v-for="macro in macros"
117
+ :key="macro.id"
118
+ role="menuitem"
119
+ :disabled="processing"
120
+ :class="[
121
+ 'flex w-full flex-col px-3 py-2.5 text-left transition-colors',
122
+ escDark
123
+ ? 'hover:bg-[var(--esc-panel-hover)] disabled:opacity-50'
124
+ : 'hover:bg-gray-50 disabled:opacity-50',
125
+ ]"
126
+ @click="applyMacro(macro.id)"
127
+ >
128
+ <span
129
+ :class="[
130
+ 'text-sm font-medium',
131
+ escDark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-900',
132
+ ]"
133
+ >
134
+ {{ macro.name }}
135
+ </span>
136
+ <span
137
+ v-if="macro.description"
138
+ :class="[
139
+ 'mt-0.5 text-xs',
140
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
141
+ ]"
142
+ >
143
+ {{ macro.description }}
144
+ </span>
145
+ </button>
146
+ </div>
147
+ </div>
148
+ </Transition>
149
+ </div>
150
+ </template>
@@ -0,0 +1,154 @@
1
+ <script setup>
2
+ import { ref, computed, inject, watch, nextTick } from 'vue';
3
+
4
+ const props = defineProps({
5
+ suggestions: { type: Array, default: () => [] },
6
+ visible: { type: Boolean, default: false },
7
+ loading: { type: Boolean, default: false },
8
+ });
9
+
10
+ const emit = defineEmits(['select', 'close']);
11
+
12
+ const escDark = inject(
13
+ 'esc-dark',
14
+ computed(() => false),
15
+ );
16
+
17
+ const activeIndex = ref(0);
18
+ const listRef = ref(null);
19
+
20
+ // Reset active index when suggestions change
21
+ watch(
22
+ () => props.suggestions,
23
+ () => {
24
+ activeIndex.value = 0;
25
+ },
26
+ );
27
+
28
+ function scrollActiveIntoView() {
29
+ nextTick(() => {
30
+ const list = listRef.value;
31
+ if (!list) return;
32
+ const item = list.children[activeIndex.value];
33
+ if (item) {
34
+ item.scrollIntoView({ block: 'nearest' });
35
+ }
36
+ });
37
+ }
38
+
39
+ function onKeydown(e) {
40
+ if (!props.visible) return false;
41
+
42
+ if (e.key === 'ArrowDown') {
43
+ e.preventDefault();
44
+ activeIndex.value = (activeIndex.value + 1) % props.suggestions.length;
45
+ scrollActiveIntoView();
46
+ return true;
47
+ }
48
+
49
+ if (e.key === 'ArrowUp') {
50
+ e.preventDefault();
51
+ activeIndex.value = (activeIndex.value - 1 + props.suggestions.length) % props.suggestions.length;
52
+ scrollActiveIntoView();
53
+ return true;
54
+ }
55
+
56
+ if (e.key === 'Enter' || e.key === 'Tab') {
57
+ if (props.suggestions.length > 0) {
58
+ e.preventDefault();
59
+ emit('select', props.suggestions[activeIndex.value]);
60
+ return true;
61
+ }
62
+ }
63
+
64
+ if (e.key === 'Escape') {
65
+ e.preventDefault();
66
+ emit('close');
67
+ return true;
68
+ }
69
+
70
+ return false;
71
+ }
72
+
73
+ function selectAgent(agent) {
74
+ emit('select', agent);
75
+ }
76
+
77
+ defineExpose({ onKeydown });
78
+ </script>
79
+
80
+ <template>
81
+ <div
82
+ v-if="visible && (suggestions.length > 0 || loading)"
83
+ :class="[
84
+ 'absolute z-50 w-64 max-h-48 overflow-y-auto rounded-lg border shadow-lg',
85
+ escDark ? 'border-white/10 bg-neutral-900 shadow-black/40' : 'border-gray-200 bg-white shadow-gray-200/50',
86
+ ]"
87
+ role="listbox"
88
+ aria-label="Agent suggestions"
89
+ >
90
+ <!-- Loading state -->
91
+ <div
92
+ v-if="loading && suggestions.length === 0"
93
+ :class="['px-3 py-2 text-xs', escDark ? 'text-neutral-500' : 'text-gray-400']"
94
+ >
95
+ Searching...
96
+ </div>
97
+
98
+ <!-- Suggestions list -->
99
+ <div ref="listRef">
100
+ <button
101
+ v-for="(agent, index) in suggestions"
102
+ :key="agent.id"
103
+ type="button"
104
+ role="option"
105
+ :aria-selected="index === activeIndex"
106
+ :class="[
107
+ 'flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors',
108
+ index === activeIndex
109
+ ? escDark
110
+ ? 'bg-white/[0.06] text-white'
111
+ : 'bg-blue-50 text-blue-900'
112
+ : escDark
113
+ ? 'text-neutral-300 hover:bg-white/[0.04]'
114
+ : 'text-gray-700 hover:bg-gray-50',
115
+ ]"
116
+ @click="selectAgent(agent)"
117
+ @mouseenter="activeIndex = index"
118
+ >
119
+ <!-- Avatar placeholder -->
120
+ <div
121
+ :class="[
122
+ 'flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-semibold',
123
+ escDark ? 'bg-cyan-500/20 text-cyan-400' : 'bg-blue-100 text-blue-700',
124
+ ]"
125
+ >
126
+ {{
127
+ (agent.name || '')
128
+ .split(' ')
129
+ .map((w) => w[0])
130
+ .join('')
131
+ .toUpperCase()
132
+ .slice(0, 2)
133
+ }}
134
+ </div>
135
+ <div class="min-w-0 flex-1">
136
+ <div :class="['truncate font-medium text-sm', escDark ? 'text-white' : 'text-gray-900']">
137
+ {{ agent.name }}
138
+ </div>
139
+ <div :class="['truncate text-xs', escDark ? 'text-neutral-500' : 'text-gray-500']">
140
+ {{ agent.email }}
141
+ </div>
142
+ </div>
143
+ </button>
144
+ </div>
145
+
146
+ <!-- No results -->
147
+ <div
148
+ v-if="!loading && suggestions.length === 0 && visible"
149
+ :class="['px-3 py-2 text-xs', escDark ? 'text-neutral-500' : 'text-gray-400']"
150
+ >
151
+ No agents found
152
+ </div>
153
+ </div>
154
+ </template>