@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
@@ -1,41 +1,57 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
-
4
- const emit = defineEmits(['files']);
5
- const dark = inject('esc-dark', computed(() => false));
6
-
7
- const dragging = ref(false);
8
- const fileInput = ref(null);
9
-
10
- function onDrop(e) {
11
- dragging.value = false;
12
- if (e.dataTransfer?.files?.length) {
13
- emit('files', Array.from(e.dataTransfer.files));
14
- }
15
- }
16
-
17
- function onFileSelect(e) {
18
- if (e.target.files?.length) {
19
- emit('files', Array.from(e.target.files));
20
- e.target.value = '';
21
- }
22
- }
23
-
24
- function browse() {
25
- fileInput.value?.click();
26
- }
27
- </script>
28
-
29
- <template>
30
- <div @dragover.prevent="dragging = true" @dragleave="dragging = false" @drop.prevent="onDrop"
31
- :class="['cursor-pointer rounded-lg border-2 border-dashed px-4 py-3 text-center text-xs transition-colors',
32
- dark
33
- ? (dragging ? 'border-white/20 bg-white/[0.04]' : 'border-white/[0.08] hover:border-white/[0.12]')
34
- : (dragging ? 'border-blue-400 bg-blue-50' : 'border-gray-300 hover:border-gray-400')]"
35
- @click="browse">
36
- <p :class="dark ? 'text-neutral-500' : 'text-gray-500'">
37
- Drop files here or <span :class="['font-medium', dark ? 'text-white' : 'text-blue-600']">browse</span>
38
- </p>
39
- <input ref="fileInput" type="file" multiple class="hidden" @change="onFileSelect" />
40
- </div>
41
- </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>
@@ -0,0 +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,63 +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('esc-dark', computed(() => false));
12
- const following = ref(props.isFollowing);
13
- const count = ref(props.followersCount);
14
- const processing = ref(false);
15
-
16
- function toggle() {
17
- if (processing.value) return;
18
- processing.value = true;
19
- router.post(props.action, {}, {
20
- preserveScroll: true,
21
- onSuccess: () => {
22
- following.value = !following.value;
23
- count.value += following.value ? 1 : -1;
24
- },
25
- onFinish: () => {
26
- processing.value = false;
27
- },
28
- });
29
- }
30
- </script>
31
-
32
- <template>
33
- <button @click="toggle"
34
- :disabled="processing"
35
- :class="['inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
36
- escDark
37
- ? (following
38
- ? 'bg-cyan-500/15 text-white ring-1 ring-cyan-500/20 hover:bg-cyan-500/25'
39
- : 'border border-white/10 bg-white/[0.03] text-neutral-400 hover:bg-white/[0.06] hover:text-neutral-200')
40
- : (following
41
- ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300 hover:bg-blue-200'
42
- : 'border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-800'),
43
- processing && 'opacity-50 cursor-not-allowed']">
44
- <!-- Eye icon -->
45
- <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
46
- <template v-if="following">
47
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
48
- <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
49
- </template>
50
- <template v-else>
51
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
52
- </template>
53
- </svg>
54
- {{ following ? 'Following' : 'Follow' }}
55
- <span v-if="count > 0"
56
- :class="['rounded-full px-1.5 py-0.5 text-xs',
57
- escDark
58
- ? 'bg-white/[0.06] text-neutral-400'
59
- : 'bg-gray-100 text-gray-500']">
60
- {{ count }}
61
- </span>
62
- </button>
63
- </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>
@@ -1,123 +1,213 @@
1
- <script setup>
2
- import { computed, inject } from 'vue';
3
-
4
- const props = defineProps({
5
- show: { type: Boolean, default: false },
6
- context: { type: String, default: 'list' },
7
- });
8
-
9
- const emit = defineEmits(['update:show']);
10
- const escDark = inject('esc-dark', computed(() => false));
11
-
12
- const shortcutGroups = [
13
- {
14
- title: 'List View',
15
- contexts: ['list'],
16
- shortcuts: [
17
- { keys: ['j'], description: 'Move down' },
18
- { keys: ['k'], description: 'Move up' },
19
- { keys: ['x'], description: 'Toggle select' },
20
- { keys: ['Enter'], description: 'Open ticket' },
21
- ],
22
- },
23
- {
24
- title: 'Detail View',
25
- contexts: ['detail'],
26
- shortcuts: [
27
- { keys: ['r'], description: 'Reply' },
28
- { keys: ['n'], description: 'Internal note' },
29
- { keys: ['s'], description: 'Change status' },
30
- { keys: ['p'], description: 'Change priority' },
31
- { keys: ['f'], description: 'Follow/unfollow' },
32
- ],
33
- },
34
- {
35
- title: 'Global',
36
- contexts: ['list', 'detail'],
37
- shortcuts: [
38
- { keys: ['?'], description: 'Show this help' },
39
- ],
40
- },
41
- ];
42
-
43
- const filteredGroups = computed(() =>
44
- shortcutGroups.filter(g => g.contexts.includes(props.context))
45
- );
46
-
47
- function closeModal() {
48
- emit('update:show', false);
49
- }
50
- </script>
51
-
52
- <template>
53
- <Teleport to="body">
54
- <Transition enter-active-class="transition duration-200 ease-out" enter-from-class="opacity-0" enter-to-class="opacity-100"
55
- leave-active-class="transition duration-150 ease-in" leave-from-class="opacity-100" leave-to-class="opacity-0">
56
- <div v-if="show" class="fixed inset-0 z-[100] flex items-center justify-center" @keydown.escape="closeModal">
57
- <!-- Backdrop -->
58
- <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="closeModal"></div>
59
-
60
- <!-- Modal -->
61
- <div :class="['relative z-10 w-full max-w-lg overflow-hidden rounded-2xl border shadow-2xl',
62
- escDark ? 'border-white/[0.06] bg-neutral-900' : 'border-gray-200 bg-white']">
63
- <!-- Header -->
64
- <div :class="['flex items-center justify-between border-b px-6 py-4',
65
- escDark ? 'border-white/[0.06]' : 'border-gray-200']">
66
- <div class="flex items-center gap-3">
67
- <svg :class="['h-5 w-5', escDark ? 'text-neutral-400' : 'text-gray-500']"
68
- fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
69
- <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
70
- </svg>
71
- <h2 :class="['text-base font-semibold', escDark ? 'text-white' : 'text-gray-900']">
72
- Keyboard Shortcuts
73
- </h2>
74
- </div>
75
- <button @click="closeModal"
76
- :class="['rounded-lg p-1.5 transition-colors',
77
- escDark ? 'text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300' : 'text-gray-400 hover:bg-gray-100 hover:text-gray-600']">
78
- <svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
79
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
80
- </svg>
81
- </button>
82
- </div>
83
-
84
- <!-- Body -->
85
- <div class="max-h-[60vh] overflow-y-auto px-6 py-4">
86
- <div v-for="(group, gi) in filteredGroups" :key="gi"
87
- :class="['pb-4', gi < filteredGroups.length - 1 && 'mb-4 border-b',
88
- escDark ? 'border-white/[0.06]' : 'border-gray-100']">
89
- <h3 :class="['mb-3 text-xs font-semibold uppercase tracking-wider',
90
- escDark ? 'text-neutral-500' : 'text-gray-500']">
91
- {{ group.title }}
92
- </h3>
93
- <div class="space-y-2">
94
- <div v-for="(shortcut, si) in group.shortcuts" :key="si"
95
- class="flex items-center justify-between">
96
- <span :class="['text-sm', escDark ? 'text-neutral-300' : 'text-gray-700']">
97
- {{ shortcut.description }}
98
- </span>
99
- <div class="flex items-center gap-1">
100
- <kbd v-for="(key, ki) in shortcut.keys" :key="ki"
101
- :class="['inline-flex min-w-[24px] items-center justify-center rounded-md border px-2 py-0.5 text-xs font-semibold',
102
- escDark
103
- ? 'border-white/10 bg-white/[0.06] text-neutral-300'
104
- : 'border-gray-300 bg-gray-100 text-gray-700']">
105
- {{ key }}
106
- </kbd>
107
- </div>
108
- </div>
109
- </div>
110
- </div>
111
- </div>
112
-
113
- <!-- Footer -->
114
- <div :class="['border-t px-6 py-3 text-center text-xs',
115
- escDark ? 'border-white/[0.06] text-neutral-600' : 'border-gray-100 text-gray-400']">
116
- Press <kbd :class="['mx-1 inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-semibold',
117
- escDark ? 'border-white/10 bg-white/[0.06] text-neutral-400' : 'border-gray-300 bg-gray-100 text-gray-600']">Esc</kbd> to close
118
- </div>
119
- </div>
120
- </div>
121
- </Transition>
122
- </Teleport>
123
- </template>
1
+ <script setup>
2
+ import { computed, inject } from 'vue';
3
+
4
+ const props = defineProps({
5
+ show: { type: Boolean, default: false },
6
+ context: { type: String, default: 'list' },
7
+ });
8
+
9
+ const emit = defineEmits(['update:show']);
10
+ const escDark = inject(
11
+ 'esc-dark',
12
+ computed(() => false),
13
+ );
14
+
15
+ const shortcutGroups = [
16
+ {
17
+ title: 'List View',
18
+ contexts: ['list'],
19
+ shortcuts: [
20
+ { keys: ['j'], description: 'Move down' },
21
+ { keys: ['k'], description: 'Move up' },
22
+ { keys: ['x'], description: 'Toggle select' },
23
+ { keys: ['Enter'], description: 'Open ticket' },
24
+ ],
25
+ },
26
+ {
27
+ title: 'Detail View',
28
+ contexts: ['detail'],
29
+ shortcuts: [
30
+ { keys: ['r'], description: 'Reply' },
31
+ { keys: ['n'], description: 'Internal note' },
32
+ { keys: ['s'], description: 'Change status' },
33
+ { keys: ['p'], description: 'Change priority' },
34
+ { keys: ['f'], description: 'Follow/unfollow' },
35
+ ],
36
+ },
37
+ {
38
+ title: 'Global',
39
+ contexts: ['list', 'detail'],
40
+ shortcuts: [{ keys: ['?'], description: 'Show this help' }],
41
+ },
42
+ ];
43
+
44
+ const filteredGroups = computed(() => shortcutGroups.filter((g) => g.contexts.includes(props.context)));
45
+
46
+ function closeModal() {
47
+ emit('update:show', false);
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <Teleport to="body">
53
+ <Transition
54
+ enter-active-class="transition duration-200 ease-out"
55
+ enter-from-class="opacity-0"
56
+ enter-to-class="opacity-100"
57
+ leave-active-class="transition duration-150 ease-in"
58
+ leave-from-class="opacity-100"
59
+ leave-to-class="opacity-0"
60
+ >
61
+ <div
62
+ v-if="show"
63
+ class="fixed inset-0 z-[100] flex items-center justify-center"
64
+ @keydown.escape="closeModal"
65
+ >
66
+ <!-- Backdrop -->
67
+ <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="closeModal"></div>
68
+
69
+ <!-- Modal -->
70
+ <div
71
+ role="dialog"
72
+ aria-modal="true"
73
+ aria-label="Keyboard shortcuts"
74
+ :class="[
75
+ 'relative z-10 w-full max-w-lg overflow-hidden rounded-2xl border shadow-2xl',
76
+ escDark
77
+ ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]'
78
+ : 'border-gray-200 bg-white',
79
+ ]"
80
+ >
81
+ <!-- Header -->
82
+ <div
83
+ :class="[
84
+ 'flex items-center justify-between border-b px-6 py-4',
85
+ escDark ? 'border-[var(--esc-panel-border)]' : 'border-gray-200',
86
+ ]"
87
+ >
88
+ <div class="flex items-center gap-3">
89
+ <svg
90
+ :class="[
91
+ 'h-5 w-5',
92
+ escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-500',
93
+ ]"
94
+ fill="none"
95
+ stroke="currentColor"
96
+ stroke-width="1.5"
97
+ viewBox="0 0 24 24"
98
+ >
99
+ <path
100
+ stroke-linecap="round"
101
+ stroke-linejoin="round"
102
+ d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"
103
+ />
104
+ </svg>
105
+ <h2
106
+ :class="[
107
+ 'text-base font-semibold',
108
+ escDark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900',
109
+ ]"
110
+ >
111
+ Keyboard Shortcuts
112
+ </h2>
113
+ </div>
114
+ <button
115
+ aria-label="Close"
116
+ :class="[
117
+ 'rounded-lg p-1.5 transition-colors',
118
+ escDark
119
+ ? 'text-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]'
120
+ : 'text-gray-400 hover:bg-gray-100 hover:text-gray-600',
121
+ ]"
122
+ @click="closeModal"
123
+ >
124
+ <svg
125
+ class="h-5 w-5"
126
+ fill="none"
127
+ stroke="currentColor"
128
+ stroke-width="1.5"
129
+ viewBox="0 0 24 24"
130
+ >
131
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
132
+ </svg>
133
+ </button>
134
+ </div>
135
+
136
+ <!-- Body -->
137
+ <div class="max-h-[60vh] overflow-y-auto px-6 py-4">
138
+ <div
139
+ v-for="(group, gi) in filteredGroups"
140
+ :key="gi"
141
+ :class="[
142
+ 'pb-4',
143
+ gi < filteredGroups.length - 1 && 'mb-4 border-b',
144
+ escDark ? 'border-[var(--esc-panel-border)]' : 'border-gray-100',
145
+ ]"
146
+ >
147
+ <h3
148
+ :class="[
149
+ 'mb-3 text-xs font-semibold uppercase tracking-wider',
150
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
151
+ ]"
152
+ >
153
+ {{ group.title }}
154
+ </h3>
155
+ <div class="space-y-2">
156
+ <div
157
+ v-for="(shortcut, si) in group.shortcuts"
158
+ :key="si"
159
+ class="flex items-center justify-between"
160
+ >
161
+ <span
162
+ :class="[
163
+ 'text-sm',
164
+ escDark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-700',
165
+ ]"
166
+ >
167
+ {{ shortcut.description }}
168
+ </span>
169
+ <div class="flex items-center gap-1">
170
+ <kbd
171
+ v-for="(key, ki) in shortcut.keys"
172
+ :key="ki"
173
+ :class="[
174
+ 'inline-flex min-w-[24px] items-center justify-center rounded-md border px-2 py-0.5 text-xs font-semibold',
175
+ escDark
176
+ ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-secondary)]'
177
+ : 'border-gray-300 bg-gray-100 text-gray-700',
178
+ ]"
179
+ >
180
+ {{ key }}
181
+ </kbd>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- Footer -->
189
+ <div
190
+ :class="[
191
+ 'border-t px-6 py-3 text-center text-xs',
192
+ escDark
193
+ ? 'border-[var(--esc-panel-border)] text-[var(--esc-panel-text-muted)]'
194
+ : 'border-gray-100 text-gray-400',
195
+ ]"
196
+ >
197
+ Press
198
+ <kbd
199
+ :class="[
200
+ 'mx-1 inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-semibold',
201
+ escDark
202
+ ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)]'
203
+ : 'border-gray-300 bg-gray-100 text-gray-600',
204
+ ]"
205
+ >Esc</kbd
206
+ >
207
+ to close
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </Transition>
212
+ </Teleport>
213
+ </template>