@escalated-dev/escalated 0.8.0 → 0.11.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 (124) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +388 -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 +628 -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/Automations/Form.vue +280 -0
  80. package/src/pages/Admin/Automations/Index.vue +230 -0
  81. package/src/pages/Admin/Newsletters/Compose.vue +192 -0
  82. package/src/pages/Admin/Newsletters/Edit.vue +24 -0
  83. package/src/pages/Admin/Newsletters/Index.vue +141 -0
  84. package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
  85. package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
  86. package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
  87. package/src/pages/Admin/Newsletters/Settings.vue +85 -0
  88. package/src/pages/Admin/Newsletters/Show.vue +140 -0
  89. package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
  90. package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
  91. package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
  92. package/src/pages/Admin/Newsletters/usePreview.js +44 -0
  93. package/src/pages/Admin/Plugins/Index.vue +10 -2
  94. package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
  95. package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
  96. package/src/pages/Admin/Reports/Cohorts.vue +180 -180
  97. package/src/pages/Admin/Reports/Comparison.vue +141 -141
  98. package/src/pages/Admin/Reports/Dashboard.vue +226 -226
  99. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
  100. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
  101. package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
  102. package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
  103. package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
  104. package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
  105. package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
  106. package/src/pages/Admin/Settings.vue +1008 -1008
  107. package/src/pages/Admin/Skills/Form.vue +30 -10
  108. package/src/pages/Admin/Tickets/Index.vue +126 -126
  109. package/src/pages/Admin/Tickets/Show.vue +254 -249
  110. package/src/pages/Admin/Users/Index.vue +145 -145
  111. package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
  112. package/src/pages/Admin/Workflows/Builder.vue +207 -207
  113. package/src/pages/Admin/Workflows/Form.vue +24 -0
  114. package/src/pages/Admin/Workflows/Index.vue +235 -235
  115. package/src/pages/Admin/Workflows/Logs.vue +286 -286
  116. package/src/pages/Agent/TicketIndex.vue +112 -112
  117. package/src/pages/Agent/TicketShow.vue +547 -494
  118. package/src/pages/Customer/Index.vue +56 -56
  119. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
  120. package/src/pages/Plugin/Page.vue +61 -0
  121. package/src/plugin.js +175 -175
  122. package/src/utils/mentionHighlight.js +24 -24
  123. package/src/widget/EscalatedWidget.vue +804 -804
  124. package/src/widget/index.js +254 -254
@@ -7,6 +7,7 @@ import AssigneeSelect from './AssigneeSelect.vue';
7
7
  import TagSelect from './TagSelect.vue';
8
8
  import TicketTypeSelector from './TicketTypeSelector.vue';
9
9
  import ActivityTimeline from './ActivityTimeline.vue';
10
+ import TicketSubjects from './TicketSubjects.vue';
10
11
 
11
12
  defineProps({
12
13
  ticket: { type: Object, required: true },
@@ -85,6 +86,9 @@ function renderStars(rating) {
85
86
  </dl>
86
87
  </div>
87
88
 
89
+ <!-- Related host-app entities this ticket is about (Project, Customer, …) -->
90
+ <TicketSubjects :subjects="ticket.subjects || []" />
91
+
88
92
  <!-- Satisfaction Rating (read-only) -->
89
93
  <div v-if="ticket.satisfaction_rating" :class="cardClass">
90
94
  <h3 :class="['mb-3 text-sm font-semibold', escDark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900']">
@@ -1,98 +1,98 @@
1
- <script setup>
2
- import { ref } from 'vue';
3
- import { router } from '@inertiajs/vue3';
4
-
5
- const props = defineProps({
6
- ticketReference: { type: String, required: true },
7
- replyId: { type: [Number, String], required: true },
8
- replyAuthor: { type: String, default: '' },
9
- replyExcerpt: { type: String, default: '' },
10
- });
11
-
12
- const emit = defineEmits(['close']);
13
-
14
- const subject = ref('');
15
- const splitting = ref(false);
16
-
17
- function confirmSplit() {
18
- if (splitting.value) return;
19
- splitting.value = true;
20
-
21
- const payload = { reply_id: props.replyId };
22
- if (subject.value.trim()) {
23
- payload.subject = subject.value.trim();
24
- }
25
-
26
- router.post(route('escalated.admin.tickets.split', props.ticketReference), payload, {
27
- onFinish: () => {
28
- splitting.value = false;
29
- },
30
- onError: () => {
31
- splitting.value = false;
32
- },
33
- });
34
- }
35
- </script>
36
-
37
- <template>
38
- <Teleport to="body">
39
- <div
40
- class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
41
- @click.self="emit('close')"
42
- >
43
- <div class="w-full max-w-lg rounded-xl border border-white/[0.06] bg-neutral-900 p-6 shadow-2xl">
44
- <div class="mb-4 flex items-center justify-between">
45
- <h3 class="text-lg font-semibold text-neutral-200">Split to New Ticket</h3>
46
- <button class="text-neutral-500 hover:text-neutral-300" @click="emit('close')">
47
- <svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
48
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
49
- </svg>
50
- </button>
51
- </div>
52
-
53
- <p class="mb-4 text-sm text-neutral-400">
54
- This will create a new ticket from the selected reply
55
- <span v-if="replyAuthor" class="text-neutral-300">by {{ replyAuthor }}</span
56
- >. The new ticket will be linked to
57
- <span class="font-mono text-neutral-300">{{ ticketReference }}</span
58
- >.
59
- </p>
60
-
61
- <!-- Reply excerpt preview -->
62
- <div v-if="replyExcerpt" class="mb-4 rounded-lg border border-white/[0.06] bg-white/[0.02] p-3">
63
- <p class="text-xs font-semibold uppercase tracking-wider text-neutral-500">Reply content</p>
64
- <p class="mt-1 line-clamp-3 text-sm text-neutral-300">{{ replyExcerpt }}</p>
65
- </div>
66
-
67
- <label class="mb-1 block text-sm font-medium text-neutral-400">
68
- Subject <span class="text-neutral-600">(optional override)</span>
69
- </label>
70
- <input
71
- v-model="subject"
72
- type="text"
73
- placeholder="Leave empty to auto-generate from source ticket"
74
- 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"
75
- />
76
-
77
- <div class="mt-5 flex justify-end gap-3">
78
- <button
79
- class="rounded-lg border border-white/10 bg-white/[0.03] px-4 py-2 text-sm font-medium text-neutral-300 transition-colors hover:bg-white/[0.06]"
80
- @click="emit('close')"
81
- >
82
- Cancel
83
- </button>
84
- <button
85
- :disabled="splitting"
86
- :class="[
87
- 'rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-4 py-2 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400',
88
- splitting && 'cursor-not-allowed opacity-40',
89
- ]"
90
- @click="confirmSplit"
91
- >
92
- {{ splitting ? 'Splitting...' : 'Split to New Ticket' }}
93
- </button>
94
- </div>
95
- </div>
96
- </div>
97
- </Teleport>
98
- </template>
1
+ <script setup>
2
+ import { ref } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ ticketReference: { type: String, required: true },
7
+ replyId: { type: [Number, String], required: true },
8
+ replyAuthor: { type: String, default: '' },
9
+ replyExcerpt: { type: String, default: '' },
10
+ });
11
+
12
+ const emit = defineEmits(['close']);
13
+
14
+ const subject = ref('');
15
+ const splitting = ref(false);
16
+
17
+ function confirmSplit() {
18
+ if (splitting.value) return;
19
+ splitting.value = true;
20
+
21
+ const payload = { reply_id: props.replyId };
22
+ if (subject.value.trim()) {
23
+ payload.subject = subject.value.trim();
24
+ }
25
+
26
+ router.post(route('escalated.admin.tickets.split', props.ticketReference), payload, {
27
+ onFinish: () => {
28
+ splitting.value = false;
29
+ },
30
+ onError: () => {
31
+ splitting.value = false;
32
+ },
33
+ });
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <Teleport to="body">
39
+ <div
40
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
41
+ @click.self="emit('close')"
42
+ >
43
+ <div class="w-full max-w-lg rounded-xl border border-white/[0.06] bg-neutral-900 p-6 shadow-2xl">
44
+ <div class="mb-4 flex items-center justify-between">
45
+ <h3 class="text-lg font-semibold text-neutral-200">Split to New Ticket</h3>
46
+ <button class="text-neutral-500 hover:text-neutral-300" @click="emit('close')">
47
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
48
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
49
+ </svg>
50
+ </button>
51
+ </div>
52
+
53
+ <p class="mb-4 text-sm text-neutral-400">
54
+ This will create a new ticket from the selected reply
55
+ <span v-if="replyAuthor" class="text-neutral-300">by {{ replyAuthor }}</span
56
+ >. The new ticket will be linked to
57
+ <span class="font-mono text-neutral-300">{{ ticketReference }}</span
58
+ >.
59
+ </p>
60
+
61
+ <!-- Reply excerpt preview -->
62
+ <div v-if="replyExcerpt" class="mb-4 rounded-lg border border-white/[0.06] bg-white/[0.02] p-3">
63
+ <p class="text-xs font-semibold uppercase tracking-wider text-neutral-500">Reply content</p>
64
+ <p class="mt-1 line-clamp-3 text-sm text-neutral-300">{{ replyExcerpt }}</p>
65
+ </div>
66
+
67
+ <label class="mb-1 block text-sm font-medium text-neutral-400">
68
+ Subject <span class="text-neutral-600">(optional override)</span>
69
+ </label>
70
+ <input
71
+ v-model="subject"
72
+ type="text"
73
+ placeholder="Leave empty to auto-generate from source ticket"
74
+ 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"
75
+ />
76
+
77
+ <div class="mt-5 flex justify-end gap-3">
78
+ <button
79
+ class="rounded-lg border border-white/10 bg-white/[0.03] px-4 py-2 text-sm font-medium text-neutral-300 transition-colors hover:bg-white/[0.06]"
80
+ @click="emit('close')"
81
+ >
82
+ Cancel
83
+ </button>
84
+ <button
85
+ :disabled="splitting"
86
+ :class="[
87
+ 'rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-4 py-2 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400',
88
+ splitting && 'cursor-not-allowed opacity-40',
89
+ ]"
90
+ @click="confirmSplit"
91
+ >
92
+ {{ splitting ? 'Splitting...' : 'Split to New Ticket' }}
93
+ </button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </Teleport>
98
+ </template>
@@ -0,0 +1,63 @@
1
+ import TicketSubjects from './TicketSubjects.vue';
2
+
3
+ export default {
4
+ title: 'Components/TicketSubjects',
5
+ component: TicketSubjects,
6
+ };
7
+
8
+ export const Single = {
9
+ args: {
10
+ subjects: [
11
+ {
12
+ type: 'App\\Models\\Project',
13
+ id: '42',
14
+ role: 'project',
15
+ title: 'Acme Website Redesign',
16
+ subtitle: 'Project · Acme Corp',
17
+ url: 'https://app.example.com/projects/42',
18
+ color: '#2563eb',
19
+ icon: 'folder',
20
+ },
21
+ ],
22
+ },
23
+ };
24
+
25
+ export const Multiple = {
26
+ args: {
27
+ subjects: [
28
+ {
29
+ type: 'App\\Models\\Project',
30
+ id: '42',
31
+ role: 'project',
32
+ title: 'Acme Website Redesign',
33
+ subtitle: 'Project · Acme Corp',
34
+ url: 'https://app.example.com/projects/42',
35
+ color: '#2563eb',
36
+ icon: 'folder',
37
+ },
38
+ {
39
+ type: 'App\\Models\\Customer',
40
+ id: '7',
41
+ role: 'account',
42
+ title: 'Acme Corp',
43
+ subtitle: 'Enterprise · 320 seats',
44
+ url: 'https://app.example.com/customers/7',
45
+ color: '#059669',
46
+ icon: 'building',
47
+ },
48
+ {
49
+ type: 'App\\Models\\Asset',
50
+ id: 'srv-19',
51
+ title: 'web-prod-19',
52
+ subtitle: 'Server · us-east-1',
53
+ url: null,
54
+ color: null,
55
+ icon: null,
56
+ },
57
+ ],
58
+ },
59
+ };
60
+
61
+ export const Empty = {
62
+ args: { subjects: [] },
63
+ };
@@ -0,0 +1,107 @@
1
+ <script setup>
2
+ import { inject, computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ subjects: { type: Array, default: () => [] },
6
+ });
7
+
8
+ const escDark = inject(
9
+ 'esc-dark',
10
+ computed(() => false),
11
+ );
12
+
13
+ const cardClass = computed(() =>
14
+ escDark.value
15
+ ? 'rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4'
16
+ : 'rounded-lg border border-gray-200 bg-white p-4',
17
+ );
18
+
19
+ const hasSubjects = computed(() => Array.isArray(props.subjects) && props.subjects.length > 0);
20
+
21
+ function initials(subject) {
22
+ const text = (subject.title || '').trim();
23
+ if (!text) return '?';
24
+ return text
25
+ .split(/\s+/)
26
+ .slice(0, 2)
27
+ .map((w) => w.charAt(0).toUpperCase())
28
+ .join('');
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div v-if="hasSubjects" :class="cardClass">
34
+ <h3 :class="['mb-3 text-sm font-semibold', escDark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900']">
35
+ Related
36
+ </h3>
37
+ <ul class="space-y-2">
38
+ <li v-for="(subject, i) in subjects" :key="`${subject.type}:${subject.id}:${i}`">
39
+ <component
40
+ :is="subject.url ? 'a' : 'div'"
41
+ :href="subject.url || undefined"
42
+ :target="subject.url ? '_blank' : undefined"
43
+ :rel="subject.url ? 'noopener noreferrer' : undefined"
44
+ :class="[
45
+ 'flex items-center gap-3 rounded-md px-2 py-1.5 text-sm transition',
46
+ subject.url ? 'cursor-pointer' : '',
47
+ subject.url
48
+ ? escDark
49
+ ? 'hover:bg-[var(--esc-panel-hover,rgba(255,255,255,0.05))]'
50
+ : 'hover:bg-gray-50'
51
+ : '',
52
+ ]"
53
+ >
54
+ <span
55
+ class="flex h-8 w-8 flex-none items-center justify-center rounded-md text-xs font-semibold text-white"
56
+ :style="{ backgroundColor: subject.color || '#6b7280' }"
57
+ :title="subject.icon || undefined"
58
+ aria-hidden="true"
59
+ >
60
+ {{ initials(subject) }}
61
+ </span>
62
+ <span class="min-w-0 flex-1">
63
+ <span
64
+ :class="[
65
+ 'block truncate font-medium',
66
+ escDark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900',
67
+ ]"
68
+ >
69
+ {{ subject.title }}
70
+ </span>
71
+ <span
72
+ v-if="subject.subtitle"
73
+ :class="[
74
+ 'block truncate text-xs',
75
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
76
+ ]"
77
+ >
78
+ {{ subject.subtitle }}
79
+ </span>
80
+ </span>
81
+ <span
82
+ v-if="subject.role"
83
+ :class="['flex-none text-xs', escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400']"
84
+ >
85
+ {{ subject.role }}
86
+ </span>
87
+ <svg
88
+ v-if="subject.url"
89
+ class="h-3.5 w-3.5 flex-none"
90
+ :class="escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400'"
91
+ fill="none"
92
+ viewBox="0 0 24 24"
93
+ stroke="currentColor"
94
+ stroke-width="2"
95
+ aria-hidden="true"
96
+ >
97
+ <path
98
+ stroke-linecap="round"
99
+ stroke-linejoin="round"
100
+ d="M14 5h5v5m0-5L10 14m-1 5H6a2 2 0 01-2-2V8"
101
+ />
102
+ </svg>
103
+ </component>
104
+ </li>
105
+ </ul>
106
+ </div>
107
+ </template>
@@ -2,9 +2,8 @@
2
2
  import { ref } from 'vue';
3
3
  import TotpInput from './TotpInput.vue';
4
4
 
5
- const props = defineProps({
5
+ defineProps({
6
6
  qrUri: { type: String, default: '' },
7
- recoveryCodes: { type: Array, default: () => [] },
8
7
  });
9
8
 
10
9
  const emit = defineEmits(['confirm', 'cancel']);
@@ -24,12 +23,6 @@ function confirmCode() {
24
23
  }
25
24
  error.value = '';
26
25
  emit('confirm', code.value);
27
- step.value = 3;
28
- }
29
-
30
- function copyRecoveryCodes() {
31
- const text = props.recoveryCodes.join('\n');
32
- navigator.clipboard?.writeText(text);
33
26
  }
34
27
  </script>
35
28
 
@@ -80,34 +73,5 @@ function copyRecoveryCodes() {
80
73
  </button>
81
74
  </div>
82
75
  </div>
83
-
84
- <!-- Step 3: Recovery Codes -->
85
- <div v-if="step === 3">
86
- <h3 class="mb-4 text-sm font-semibold text-white">Step 3: Save Recovery Codes</h3>
87
- <p class="mb-4 text-sm text-neutral-400">
88
- Save these recovery codes in a safe place. Each code can only be used once to access your account if you
89
- lose your authenticator device.
90
- </p>
91
- <div class="rounded-lg border border-white/[0.06] bg-neutral-950 p-4">
92
- <div class="grid grid-cols-2 gap-2">
93
- <code
94
- v-for="(rc, idx) in recoveryCodes"
95
- :key="idx"
96
- class="rounded bg-neutral-900 px-3 py-1.5 text-center text-sm font-mono text-neutral-200"
97
- >
98
- {{ rc }}
99
- </code>
100
- </div>
101
- </div>
102
- <div class="mt-4 flex gap-3">
103
- <button
104
- type="button"
105
- class="rounded-lg border border-white/10 px-4 py-2 text-sm text-neutral-400 hover:text-neutral-200"
106
- @click="copyRecoveryCodes"
107
- >
108
- Copy Codes
109
- </button>
110
- </div>
111
- </div>
112
76
  </div>
113
77
  </template>