@escalated-dev/escalated 0.7.0 → 0.8.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 (181) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +36 -1
  3. package/package.json +16 -7
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -55
  6. package/src/components/ActivityTimeline.vue +83 -83
  7. package/src/components/AdminDashboard.stories.js +1423 -1095
  8. package/src/components/AgentLoadIndicator.stories.js +37 -37
  9. package/src/components/AgentLoadIndicator.vue +49 -49
  10. package/src/components/AgentPerformanceCard.vue +44 -44
  11. package/src/components/ArticleEditor.vue +82 -82
  12. package/src/components/AssigneeSelect.vue +37 -37
  13. package/src/components/AttachmentList.vue +13 -1
  14. package/src/components/AuditLogEntry.vue +95 -95
  15. package/src/components/BulkActionBar.vue +166 -166
  16. package/src/components/CategoryTree.vue +89 -89
  17. package/src/components/ChartWidget.vue +46 -46
  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 -30
  24. package/src/components/CollisionWarning.vue +68 -68
  25. package/src/components/ConditionalFieldRules.vue +135 -135
  26. package/src/components/ContextPanel.vue +66 -66
  27. package/src/components/ContextPanelSection.vue +65 -65
  28. package/src/components/CustomFieldRenderer.vue +159 -159
  29. package/src/components/EscalatedLayout.vue +35 -6
  30. package/src/components/FileDropzone.vue +57 -57
  31. package/src/components/FollowButton.stories.js +30 -30
  32. package/src/components/FollowButton.vue +88 -88
  33. package/src/components/KeyboardShortcutHelp.vue +213 -213
  34. package/src/components/KnowledgePanel.vue +122 -122
  35. package/src/components/KpiCard.stories.js +32 -32
  36. package/src/components/KpiCard.vue +88 -88
  37. package/src/components/MacroDropdown.vue +150 -150
  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 -107
  43. package/src/components/PinnedNotes.vue +129 -129
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +46 -2
  46. package/src/components/PriorityBadge.stories.js +33 -33
  47. package/src/components/PriorityBadge.vue +46 -46
  48. package/src/components/ReplyComposer.vue +81 -18
  49. package/src/components/ReplyThread.vue +40 -2
  50. package/src/components/ReportChart.vue +395 -0
  51. package/src/components/ReportExportButton.vue +108 -0
  52. package/src/components/SatisfactionRating.vue +180 -180
  53. package/src/components/SaveViewDialog.vue +172 -0
  54. package/src/components/SavedViewSidebar.vue +208 -0
  55. package/src/components/ScheduleEditor.vue +104 -104
  56. package/src/components/SideConversation.vue +165 -165
  57. package/src/components/SideConversationList.vue +147 -147
  58. package/src/components/SkillTagManager.vue +83 -79
  59. package/src/components/SlaComplianceChart.vue +46 -53
  60. package/src/components/SlaTimer.stories.js +35 -35
  61. package/src/components/SlaTimer.vue +62 -62
  62. package/src/components/SnoozeButton.vue +247 -0
  63. package/src/components/StatsCard.stories.js +24 -24
  64. package/src/components/StatsCard.vue +58 -58
  65. package/src/components/StatusBadge.stories.js +53 -53
  66. package/src/components/StatusBadge.vue +78 -78
  67. package/src/components/TagSelect.vue +84 -84
  68. package/src/components/TicketFilters.vue +270 -225
  69. package/src/components/TicketLinkPanel.vue +193 -193
  70. package/src/components/TicketList.vue +655 -594
  71. package/src/components/TicketMergeDialog.vue +157 -157
  72. package/src/components/TicketSplitDialog.vue +98 -0
  73. package/src/components/TotpInput.stories.js +9 -9
  74. package/src/components/TotpInput.vue +81 -81
  75. package/src/components/TwoFactorSetup.vue +113 -113
  76. package/src/components/VariableTokenInput.vue +111 -0
  77. package/src/components/WorkflowActionConfig.vue +287 -0
  78. package/src/components/WorkflowActionList.vue +350 -0
  79. package/src/components/WorkflowConditionBuilder.vue +265 -0
  80. package/src/components/WorkflowTriggerSelector.vue +125 -0
  81. package/src/composables/useChat.js +187 -0
  82. package/src/composables/useI18n.js +30 -8
  83. package/src/composables/useKeyboardShortcuts.js +46 -46
  84. package/src/composables/useMentions.js +134 -0
  85. package/src/composables/usePluginExtensions.js +159 -159
  86. package/src/composables/usePluginHooks.js +65 -65
  87. package/src/composables/useRealtime.js +182 -0
  88. package/src/defineEscalatedPlugin.js +76 -76
  89. package/src/index.js +84 -72
  90. package/src/locales/README.md +13 -0
  91. package/src/locales/ar.json +727 -0
  92. package/src/locales/de.json +678 -430
  93. package/src/locales/en.json +717 -455
  94. package/src/locales/es.json +679 -430
  95. package/src/locales/fr.json +680 -431
  96. package/src/locales/index.js +12 -2
  97. package/src/locales/it.json +727 -0
  98. package/src/locales/ja.json +727 -0
  99. package/src/locales/ko.json +727 -0
  100. package/src/locales/nl.json +727 -0
  101. package/src/locales/pl.json +727 -0
  102. package/src/locales/pt-BR.json +727 -0
  103. package/src/locales/ru.json +727 -0
  104. package/src/locales/tr.json +727 -0
  105. package/src/locales/zh-CN.json +727 -0
  106. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  107. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  108. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  109. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  110. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  111. package/src/pages/Admin/Capacity/Index.vue +134 -134
  112. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  113. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  114. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  115. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  116. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  117. package/src/pages/Admin/Departments/Form.vue +75 -75
  118. package/src/pages/Admin/Departments/Index.vue +110 -109
  119. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  120. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  121. package/src/pages/Admin/Import/Connect.vue +0 -1
  122. package/src/pages/Admin/Import/Index.vue +1 -1
  123. package/src/pages/Admin/Import/Progress.vue +1 -1
  124. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  125. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  126. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  127. package/src/pages/Admin/Macros/Index.vue +448 -448
  128. package/src/pages/Admin/Plugins/Index.vue +327 -327
  129. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  130. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  131. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  132. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  133. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  134. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  135. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  136. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  138. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  139. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  140. package/src/pages/Admin/Reports.vue +206 -206
  141. package/src/pages/Admin/Roles/Form.vue +71 -71
  142. package/src/pages/Admin/Roles/Index.vue +122 -122
  143. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  144. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  145. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  146. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  147. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  148. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  149. package/src/pages/Admin/Settings.vue +1008 -468
  150. package/src/pages/Admin/Skills/Form.vue +193 -53
  151. package/src/pages/Admin/Skills/Index.vue +105 -94
  152. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  153. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  154. package/src/pages/Admin/Statuses/Form.vue +114 -114
  155. package/src/pages/Admin/Statuses/Index.vue +153 -153
  156. package/src/pages/Admin/Tags/Index.vue +191 -191
  157. package/src/pages/Admin/Tickets/Index.vue +126 -91
  158. package/src/pages/Admin/Tickets/Show.vue +29 -0
  159. package/src/pages/Admin/Users/Index.vue +145 -0
  160. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  161. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  162. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  163. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  164. package/src/pages/Admin/Workflows/Index.vue +235 -0
  165. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  166. package/src/pages/Agent/TicketIndex.vue +112 -88
  167. package/src/pages/Agent/TicketShow.vue +192 -60
  168. package/src/pages/Customer/Create.vue +1 -1
  169. package/src/pages/Customer/Index.vue +26 -1
  170. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  171. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  172. package/src/pages/Customer/Show.vue +69 -63
  173. package/src/pages/Guest/Create.vue +40 -15
  174. package/src/pages/Guest/Show.vue +124 -93
  175. package/src/plugin.js +26 -1
  176. package/src/utils/mentionHighlight.js +24 -0
  177. package/src/utils/sanitizeHtml.js +78 -64
  178. package/src/widget/EscalatedWidget.vue +804 -0
  179. package/src/widget/index.js +254 -0
  180. package/src/pages/Admin/Automations/Form.vue +0 -184
  181. package/src/pages/Admin/Automations/Index.vue +0 -129
@@ -2,8 +2,9 @@
2
2
  import { computed, inject, provide } from 'vue';
3
3
  import { usePage, Link } from '@inertiajs/vue3';
4
4
  import { usePluginExtensions } from '../composables/usePluginExtensions';
5
+ import ActiveChatsPanel from './ActiveChatsPanel.vue';
5
6
 
6
- const props = defineProps({
7
+ defineProps({
7
8
  title: { type: String, default: 'Support' },
8
9
  });
9
10
 
@@ -24,6 +25,11 @@ const isAgentSection = computed(() => currentUrl.value?.includes('/agent'));
24
25
  const isPanel = computed(() => isAdminSection.value || isAgentSection.value);
25
26
  const isDark = computed(() => isPanel.value && panelConfig.mode !== 'light');
26
27
  const showPoweredBy = computed(() => page.props.escalated?.show_powered_by !== false);
28
+ const kbEnabled = computed(() => page.props.escalated?.knowledge_base_enabled !== false);
29
+ const kbPublic = computed(() => page.props.escalated?.knowledge_base_public !== false);
30
+ const chatEnabled = computed(() => page.props.escalated?.chat_enabled === true);
31
+ const activeChats = computed(() => page.props.escalated?.active_chats || []);
32
+ const agentId = computed(() => page.props.auth?.user?.id);
27
33
 
28
34
  provide('esc-dark', isDark);
29
35
 
@@ -57,7 +63,7 @@ const adminLinks = computed(() => {
57
63
  position: 40,
58
64
  },
59
65
  {
60
- href: `${p}/admin/knowledge-base/articles`,
66
+ href: `${p}/admin/kb-articles`,
61
67
  label: 'Knowledge Base',
62
68
  icon: 'M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25',
63
69
  position: 45,
@@ -69,10 +75,10 @@ const adminLinks = computed(() => {
69
75
  position: 50,
70
76
  },
71
77
  {
72
- href: `${p}/admin/automations`,
73
- label: 'Automations',
74
- icon: 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182',
75
- position: 52,
78
+ href: `${p}/admin/workflows`,
79
+ label: 'Workflows',
80
+ icon: 'M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z M15 12a3 3 0 11-6 0 3 3 0 016 0z',
81
+ position: 53,
76
82
  },
77
83
  {
78
84
  href: `${p}/admin/webhooks`,
@@ -122,6 +128,12 @@ const adminLinks = computed(() => {
122
128
  icon: 'M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z',
123
129
  position: 84,
124
130
  },
131
+ {
132
+ href: `${p}/admin/users`,
133
+ label: 'Users',
134
+ 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',
135
+ position: 83,
136
+ },
125
137
  {
126
138
  href: `${p}/admin/capacity`,
127
139
  label: 'Capacity',
@@ -505,6 +517,9 @@ function isActive(href) {
505
517
  Powered by Escalated
506
518
  </a>
507
519
  </footer>
520
+
521
+ <!-- Active Chats Panel (agent layout only, when chat enabled) -->
522
+ <ActiveChatsPanel v-if="chatEnabled && agentId" :chats="activeChats" :agent-id="agentId" />
508
523
  </div>
509
524
 
510
525
  <!-- MODE 3: Customer pages — use host app layout if provided -->
@@ -514,6 +529,13 @@ function isActive(href) {
514
529
  <h2 class="text-xl font-semibold leading-tight text-gray-800">{{ title }}</h2>
515
530
  <nav class="flex items-center gap-4 text-sm">
516
531
  <Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
532
+ <Link
533
+ v-if="kbEnabled && (kbPublic || page.props.auth?.user)"
534
+ :href="`${prefix}/kb`"
535
+ class="text-gray-600 hover:text-gray-900"
536
+ >
537
+ Knowledge Base
538
+ </Link>
517
539
  <Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">
518
540
  Agent Panel
519
541
  </Link>
@@ -538,6 +560,13 @@ function isActive(href) {
538
560
  <span class="text-lg font-bold text-gray-900">{{ title }}</span>
539
561
  <div class="flex items-center gap-4 text-sm">
540
562
  <Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
563
+ <Link
564
+ v-if="kbEnabled && (kbPublic || page.props.auth?.user)"
565
+ :href="`${prefix}/kb`"
566
+ class="text-gray-600 hover:text-gray-900"
567
+ >
568
+ Knowledge Base
569
+ </Link>
541
570
  <Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">
542
571
  Agent Panel
543
572
  </Link>
@@ -1,57 +1,57 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
-
4
- const emit = defineEmits(['files']);
5
- const dark = inject(
6
- 'esc-dark',
7
- computed(() => false),
8
- );
9
-
10
- const dragging = ref(false);
11
- const fileInput = ref(null);
12
-
13
- function onDrop(e) {
14
- dragging.value = false;
15
- if (e.dataTransfer?.files?.length) {
16
- emit('files', Array.from(e.dataTransfer.files));
17
- }
18
- }
19
-
20
- function onFileSelect(e) {
21
- if (e.target.files?.length) {
22
- emit('files', Array.from(e.target.files));
23
- e.target.value = '';
24
- }
25
- }
26
-
27
- function browse() {
28
- fileInput.value?.click();
29
- }
30
- </script>
31
-
32
- <template>
33
- <div
34
- role="button"
35
- aria-label="Drop files here or click to upload"
36
- :class="[
37
- 'cursor-pointer rounded-lg border-2 border-dashed px-4 py-3 text-center text-xs transition-colors',
38
- dark
39
- ? dragging
40
- ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)]'
41
- : 'border-[var(--esc-panel-border)] hover:border-[var(--esc-panel-border-input)]'
42
- : dragging
43
- ? 'border-blue-400 bg-blue-50'
44
- : 'border-gray-300 hover:border-gray-400',
45
- ]"
46
- @dragover.prevent="dragging = true"
47
- @dragleave="dragging = false"
48
- @drop.prevent="onDrop"
49
- @click="browse"
50
- >
51
- <p :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'">
52
- Drop files here or
53
- <span :class="['font-medium', dark ? 'text-[var(--esc-panel-text)]' : 'text-blue-600']">browse</span>
54
- </p>
55
- <input ref="fileInput" type="file" multiple aria-label="Upload files" class="hidden" @change="onFileSelect" />
56
- </div>
57
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+
4
+ const emit = defineEmits(['files']);
5
+ const dark = inject(
6
+ 'esc-dark',
7
+ computed(() => false),
8
+ );
9
+
10
+ const dragging = ref(false);
11
+ const fileInput = ref(null);
12
+
13
+ function onDrop(e) {
14
+ dragging.value = false;
15
+ if (e.dataTransfer?.files?.length) {
16
+ emit('files', Array.from(e.dataTransfer.files));
17
+ }
18
+ }
19
+
20
+ function onFileSelect(e) {
21
+ if (e.target.files?.length) {
22
+ emit('files', Array.from(e.target.files));
23
+ e.target.value = '';
24
+ }
25
+ }
26
+
27
+ function browse() {
28
+ fileInput.value?.click();
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div
34
+ role="button"
35
+ aria-label="Drop files here or click to upload"
36
+ :class="[
37
+ 'cursor-pointer rounded-lg border-2 border-dashed px-4 py-3 text-center text-xs transition-colors',
38
+ dark
39
+ ? dragging
40
+ ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)]'
41
+ : 'border-[var(--esc-panel-border)] hover:border-[var(--esc-panel-border-input)]'
42
+ : dragging
43
+ ? 'border-blue-400 bg-blue-50'
44
+ : 'border-gray-300 hover:border-gray-400',
45
+ ]"
46
+ @dragover.prevent="dragging = true"
47
+ @dragleave="dragging = false"
48
+ @drop.prevent="onDrop"
49
+ @click="browse"
50
+ >
51
+ <p :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'">
52
+ Drop files here or
53
+ <span :class="['font-medium', dark ? 'text-[var(--esc-panel-text)]' : 'text-blue-600']">browse</span>
54
+ </p>
55
+ <input ref="fileInput" type="file" multiple aria-label="Upload files" class="hidden" @change="onFileSelect" />
56
+ </div>
57
+ </template>
@@ -1,30 +1,30 @@
1
- import FollowButton from './FollowButton.vue';
2
-
3
- export default {
4
- title: 'Components/FollowButton',
5
- component: FollowButton,
6
- };
7
-
8
- export const NotFollowing = {
9
- args: {
10
- action: '#',
11
- isFollowing: false,
12
- followersCount: 3,
13
- },
14
- };
15
-
16
- export const Following = {
17
- args: {
18
- action: '#',
19
- isFollowing: true,
20
- followersCount: 5,
21
- },
22
- };
23
-
24
- export const NoFollowers = {
25
- args: {
26
- action: '#',
27
- isFollowing: false,
28
- followersCount: 0,
29
- },
30
- };
1
+ import FollowButton from './FollowButton.vue';
2
+
3
+ export default {
4
+ title: 'Components/FollowButton',
5
+ component: FollowButton,
6
+ };
7
+
8
+ export const NotFollowing = {
9
+ args: {
10
+ action: '#',
11
+ isFollowing: false,
12
+ followersCount: 3,
13
+ },
14
+ };
15
+
16
+ export const Following = {
17
+ args: {
18
+ action: '#',
19
+ isFollowing: true,
20
+ followersCount: 5,
21
+ },
22
+ };
23
+
24
+ export const NoFollowers = {
25
+ args: {
26
+ action: '#',
27
+ isFollowing: false,
28
+ followersCount: 0,
29
+ },
30
+ };
@@ -1,88 +1,88 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
- import { router } from '@inertiajs/vue3';
4
-
5
- const props = defineProps({
6
- action: { type: String, required: true },
7
- isFollowing: { type: Boolean, default: false },
8
- followersCount: { type: Number, default: 0 },
9
- });
10
-
11
- const escDark = inject(
12
- 'esc-dark',
13
- computed(() => false),
14
- );
15
- const following = ref(props.isFollowing);
16
- const count = ref(props.followersCount);
17
- const processing = ref(false);
18
-
19
- function toggle() {
20
- if (processing.value) return;
21
- processing.value = true;
22
- router.post(
23
- props.action,
24
- {},
25
- {
26
- preserveScroll: true,
27
- onSuccess: () => {
28
- following.value = !following.value;
29
- count.value += following.value ? 1 : -1;
30
- },
31
- onFinish: () => {
32
- processing.value = false;
33
- },
34
- },
35
- );
36
- }
37
- </script>
38
-
39
- <template>
40
- <button
41
- :disabled="processing"
42
- :class="[
43
- 'inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
44
- escDark
45
- ? following
46
- ? 'bg-cyan-500/15 text-[var(--esc-panel-text)] ring-1 ring-[var(--esc-panel-accent)]/20 hover:bg-cyan-500/25'
47
- : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]'
48
- : following
49
- ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300 hover:bg-blue-200'
50
- : 'border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-800',
51
- processing && 'opacity-50 cursor-not-allowed',
52
- ]"
53
- :aria-pressed="following"
54
- :aria-label="following ? 'Unfollow ticket' : 'Follow ticket'"
55
- @click="toggle"
56
- >
57
- <!-- Eye icon -->
58
- <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
59
- <template v-if="following">
60
- <path
61
- stroke-linecap="round"
62
- stroke-linejoin="round"
63
- d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
64
- />
65
- <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
66
- </template>
67
- <template v-else>
68
- <path
69
- stroke-linecap="round"
70
- stroke-linejoin="round"
71
- d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
72
- />
73
- </template>
74
- </svg>
75
- {{ following ? 'Following' : 'Follow' }}
76
- <span
77
- v-if="count > 0"
78
- :class="[
79
- 'rounded-full px-1.5 py-0.5 text-xs',
80
- escDark
81
- ? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)]'
82
- : 'bg-gray-100 text-gray-500',
83
- ]"
84
- >
85
- {{ count }}
86
- </span>
87
- </button>
88
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ action: { type: String, required: true },
7
+ isFollowing: { type: Boolean, default: false },
8
+ followersCount: { type: Number, default: 0 },
9
+ });
10
+
11
+ const escDark = inject(
12
+ 'esc-dark',
13
+ computed(() => false),
14
+ );
15
+ const following = ref(props.isFollowing);
16
+ const count = ref(props.followersCount);
17
+ const processing = ref(false);
18
+
19
+ function toggle() {
20
+ if (processing.value) return;
21
+ processing.value = true;
22
+ router.post(
23
+ props.action,
24
+ {},
25
+ {
26
+ preserveScroll: true,
27
+ onSuccess: () => {
28
+ following.value = !following.value;
29
+ count.value += following.value ? 1 : -1;
30
+ },
31
+ onFinish: () => {
32
+ processing.value = false;
33
+ },
34
+ },
35
+ );
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <button
41
+ :disabled="processing"
42
+ :class="[
43
+ 'inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
44
+ escDark
45
+ ? following
46
+ ? 'bg-cyan-500/15 text-[var(--esc-panel-text)] ring-1 ring-[var(--esc-panel-accent)]/20 hover:bg-cyan-500/25'
47
+ : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]'
48
+ : following
49
+ ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300 hover:bg-blue-200'
50
+ : 'border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-800',
51
+ processing && 'opacity-50 cursor-not-allowed',
52
+ ]"
53
+ :aria-pressed="following"
54
+ :aria-label="following ? 'Unfollow ticket' : 'Follow ticket'"
55
+ @click="toggle"
56
+ >
57
+ <!-- Eye icon -->
58
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
59
+ <template v-if="following">
60
+ <path
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
64
+ />
65
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
66
+ </template>
67
+ <template v-else>
68
+ <path
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
72
+ />
73
+ </template>
74
+ </svg>
75
+ {{ following ? 'Following' : 'Follow' }}
76
+ <span
77
+ v-if="count > 0"
78
+ :class="[
79
+ 'rounded-full px-1.5 py-0.5 text-xs',
80
+ escDark
81
+ ? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)]'
82
+ : 'bg-gray-100 text-gray-500',
83
+ ]"
84
+ >
85
+ {{ count }}
86
+ </span>
87
+ </button>
88
+ </template>