@escalated-dev/escalated 0.11.7 → 0.11.9
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.
- package/LICENSE +1 -1
- package/README.md +10 -1
- package/package.json +1 -1
- package/pages.json +1 -1
- package/src/components/AuditLogEntry.vue +15 -9
- package/src/components/ChatQueue.vue +1 -1
- package/src/components/EscalatedLayout.vue +224 -38
- package/src/components/PeriodSelector.vue +1 -1
- package/src/components/PermissionMatrix.vue +1 -1
- package/src/components/QuickFilters.vue +5 -2
- package/src/components/TicketList.vue +2 -2
- package/src/components/admin/newsletters/DeliveriesTable.vue +29 -27
- package/src/components/admin/newsletters/ListMemberTable.vue +30 -28
- package/src/pages/Admin/ApiTokens/Index.vue +1 -1
- package/src/pages/Admin/Automations/Form.vue +10 -2
- package/src/pages/Admin/Automations/Index.vue +1 -1
- package/src/pages/Admin/BusinessHours/Index.vue +1 -1
- package/src/pages/Admin/Capacity/Index.vue +1 -1
- package/src/pages/Admin/CustomFields/Index.vue +1 -1
- package/src/pages/Admin/CustomObjects/Index.vue +1 -1
- package/src/pages/Admin/CustomObjects/Records.vue +19 -12
- package/src/pages/Admin/Departments/Index.vue +1 -1
- package/src/pages/Admin/EscalationRules/Index.vue +1 -1
- package/src/pages/Admin/Import/Index.vue +1 -1
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +3 -3
- package/src/pages/Admin/Macros/Index.vue +1 -1
- package/src/pages/Admin/Newsletters/Index.vue +31 -29
- package/src/pages/Admin/Newsletters/Lists/Index.vue +22 -20
- package/src/pages/Admin/Reports/AgentMetrics.vue +1 -1
- package/src/pages/Admin/Reports/AgentRanking.vue +3 -3
- package/src/pages/Admin/Reports/Cohorts.vue +7 -5
- package/src/pages/Admin/Reports/Comparison.vue +3 -3
- package/src/pages/Admin/Reports/Dashboard.vue +7 -5
- package/src/pages/Admin/Reports/ResolutionTimes.vue +54 -46
- package/src/pages/Admin/Reports/ResponseTimes.vue +54 -46
- package/src/pages/Admin/Reports/SlaReport.vue +70 -68
- package/src/pages/Admin/Reports/SlaTrends.vue +2 -2
- package/src/pages/Admin/Reports.vue +1 -1
- package/src/pages/Admin/Roles/Index.vue +1 -1
- package/src/pages/Admin/Settings/EmailSettings.vue +1 -1
- package/src/pages/Admin/Skills/Index.vue +1 -1
- package/src/pages/Admin/SlaPolicies/Index.vue +1 -1
- package/src/pages/Admin/Statuses/Index.vue +1 -1
- package/src/pages/Admin/Tags/Index.vue +1 -1
- package/src/pages/Admin/Users/Index.vue +1 -1
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -1
- package/src/pages/Admin/Webhooks/Index.vue +1 -1
- package/src/pages/Admin/Workflows/Index.vue +1 -1
- package/src/pages/Admin/Workflows/Logs.vue +1 -1
- package/src/pages/Agent/Dashboard.vue +1 -1
- package/src/plugin.js +19 -0
- package/src/utils/scrollAffordance.js +87 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
<div align="center">
|
|
24
24
|
|
|
25
25
|
[](https://github.com/escalated-dev/escalated/actions/workflows/run-tests.yml)[](https://www.npmjs.com/package/@escalated-dev/escalated)
|
|
26
|
+
[](https://www.npmjs.com/package/@escalated-dev/escalated)
|
|
26
27
|
[](https://vuejs.org/)
|
|
27
28
|
[](https://opensource.org/licenses/MIT)
|
|
29
|
+
[](https://hits.sh/github.com/escalated-dev/escalated/)
|
|
28
30
|
|
|
29
31
|
</div>
|
|
30
32
|
|
|
@@ -155,6 +157,13 @@ The `theme` option sets CSS custom properties you can reference in your own styl
|
|
|
155
157
|
| `--esc-radius-lg` | auto-scaled | Border radius for cards and panels |
|
|
156
158
|
| `--esc-font-family` | inherit | Font family override |
|
|
157
159
|
|
|
160
|
+
### Admin and Agent Panels
|
|
161
|
+
|
|
162
|
+
`theme.panel` colours the admin and agent panels: navigation, header, logo
|
|
163
|
+
tile, links and surfaces, in dark or light mode. Both panels collapse their
|
|
164
|
+
navigation behind a menu button below 1024px. See
|
|
165
|
+
[docs/theming.md](docs/theming.md) for every token.
|
|
166
|
+
|
|
158
167
|
### Framework Examples
|
|
159
168
|
|
|
160
169
|
**Laravel** (Inertia + Vue 3):
|
|
@@ -385,4 +394,4 @@ This is the shared frontend for the Escalated support ticket system. Host framew
|
|
|
385
394
|
|
|
386
395
|
## License
|
|
387
396
|
|
|
388
|
-
MIT
|
|
397
|
+
MIT - Copyright (c) Escalated.dev. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
package/pages.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "Generated by scripts/generate-page-manifest.js from src/pages. Every name here resolves to a component; a name a backend renders that is not here is a blank screen. `props` is what each component reads -- a backend sending something else renders the screen with no content in it.",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.9",
|
|
4
4
|
"pages": [
|
|
5
5
|
"Escalated/Admin/ApiTokens/Index",
|
|
6
6
|
"Escalated/Admin/AuditLog/Index",
|
|
@@ -46,30 +46,36 @@ const formattedDate = computed(() => {
|
|
|
46
46
|
|
|
47
47
|
<template>
|
|
48
48
|
<div
|
|
49
|
-
class="flex gap-3 rounded-lg border border-
|
|
49
|
+
class="flex gap-3 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] px-4 py-3 transition-colors hover:bg-[var(--esc-panel-hover)]"
|
|
50
50
|
>
|
|
51
51
|
<!-- Avatar -->
|
|
52
52
|
<div
|
|
53
|
-
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-xs font-semibold text-
|
|
53
|
+
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-xs font-semibold text-[var(--esc-panel-text-tertiary)]"
|
|
54
54
|
>
|
|
55
55
|
{{ userInitial }}
|
|
56
56
|
</div>
|
|
57
57
|
|
|
58
58
|
<div class="min-w-0 flex-1">
|
|
59
|
-
<div class="flex items-center gap-2 text-sm">
|
|
60
|
-
<span class="font-medium text-
|
|
59
|
+
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
|
60
|
+
<span class="font-medium text-[var(--esc-panel-text-secondary)]">{{ log.user?.name || 'System' }}</span>
|
|
61
61
|
<span :class="['inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium', actionColor]">
|
|
62
62
|
{{ log.action }}
|
|
63
63
|
</span>
|
|
64
|
-
<span class="text-
|
|
65
|
-
<span class="text-
|
|
66
|
-
<span class="ml-auto text-xs text-
|
|
64
|
+
<span class="text-[var(--esc-panel-text-tertiary)]">{{ resourceName }}</span>
|
|
65
|
+
<span class="text-[var(--esc-panel-text-muted)]">#{{ log.auditable_id }}</span>
|
|
66
|
+
<span class="ml-auto whitespace-nowrap text-xs text-[var(--esc-panel-text-muted)]">{{
|
|
67
|
+
formattedDate
|
|
68
|
+
}}</span>
|
|
67
69
|
</div>
|
|
68
70
|
|
|
69
71
|
<!-- Changed fields diff -->
|
|
70
72
|
<div v-if="changedFields.length" class="mt-2 space-y-1">
|
|
71
|
-
<div
|
|
72
|
-
|
|
73
|
+
<div
|
|
74
|
+
v-for="change in changedFields"
|
|
75
|
+
:key="change.field"
|
|
76
|
+
class="flex flex-wrap items-center gap-2 text-xs"
|
|
77
|
+
>
|
|
78
|
+
<span class="font-medium text-[var(--esc-panel-text-tertiary)]">{{ change.field }}:</span>
|
|
73
79
|
<span class="text-rose-400/70 line-through">{{
|
|
74
80
|
typeof change.old === 'object' ? JSON.stringify(change.old) : change.old
|
|
75
81
|
}}</span>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, inject, provide } from 'vue';
|
|
2
|
+
import { computed, inject, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
|
|
3
3
|
import { usePage, Link } from '@inertiajs/vue3';
|
|
4
4
|
import { usePluginExtensions } from '../composables/usePluginExtensions';
|
|
5
5
|
import { usePermissions } from '../composables/usePermissions';
|
|
6
6
|
import { useI18n } from '../composables/useI18n';
|
|
7
|
+
import { watchScrollAffordance } from '../utils/scrollAffordance';
|
|
7
8
|
import ActiveChatsPanel from './ActiveChatsPanel.vue';
|
|
8
9
|
|
|
9
10
|
defineProps({
|
|
@@ -225,6 +226,8 @@ const agentLinks = computed(() => [
|
|
|
225
226
|
{
|
|
226
227
|
href: `${prefix.value}/agent`,
|
|
227
228
|
label: 'Dashboard',
|
|
229
|
+
// Every agent URL starts with this one; only the dashboard itself is it.
|
|
230
|
+
exact: true,
|
|
228
231
|
icon: 'M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25',
|
|
229
232
|
},
|
|
230
233
|
{
|
|
@@ -237,26 +240,77 @@ const agentLinks = computed(() => [
|
|
|
237
240
|
const userName = computed(() => page.props.auth?.user?.name || 'User');
|
|
238
241
|
const userInitial = computed(() => userName.value.charAt(0).toUpperCase());
|
|
239
242
|
|
|
240
|
-
function isActive(href) {
|
|
243
|
+
function isActive(href, exact = false) {
|
|
241
244
|
if (!currentUrl.value) return false;
|
|
242
|
-
|
|
245
|
+
const path = currentUrl.value.split(/[?#]/)[0];
|
|
246
|
+
if (exact) return path === href;
|
|
247
|
+
return path === href || path.startsWith(href + '/');
|
|
243
248
|
}
|
|
249
|
+
|
|
250
|
+
// Below the `lg` breakpoint the admin sidebar and the agent nav links fold
|
|
251
|
+
// away behind a menu button. It starts closed on the server and the client
|
|
252
|
+
// alike, so SSR markup hydrates without a mismatch; CSS alone decides whether
|
|
253
|
+
// the navigation shows at a given width.
|
|
254
|
+
const navOpen = ref(false);
|
|
255
|
+
function toggleNav() {
|
|
256
|
+
navOpen.value = !navOpen.value;
|
|
257
|
+
}
|
|
258
|
+
function closeNav() {
|
|
259
|
+
navOpen.value = false;
|
|
260
|
+
}
|
|
261
|
+
watch(currentUrl, closeNav);
|
|
262
|
+
|
|
263
|
+
function onKeydown(event) {
|
|
264
|
+
if (event.key === 'Escape' && navOpen.value) closeNav();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Tables scroll sideways inside `.esc-table-scroll`; mark the edges that have
|
|
268
|
+
// more content past them so the scroll is visible before anyone tries it.
|
|
269
|
+
const contentRoot = ref(null);
|
|
270
|
+
let stopScrollAffordance = () => {};
|
|
271
|
+
|
|
272
|
+
onMounted(() => {
|
|
273
|
+
window.addEventListener('keydown', onKeydown);
|
|
274
|
+
stopScrollAffordance = watchScrollAffordance(contentRoot.value);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
onBeforeUnmount(() => {
|
|
278
|
+
window.removeEventListener('keydown', onKeydown);
|
|
279
|
+
stopScrollAffordance();
|
|
280
|
+
});
|
|
244
281
|
</script>
|
|
245
282
|
|
|
246
283
|
<template>
|
|
247
|
-
<!-- MODE 1: Admin —
|
|
284
|
+
<!-- MODE 1: Admin — sidebar layout; the sidebar folds into a drawer below lg -->
|
|
248
285
|
<div
|
|
249
286
|
v-if="isAdminSection"
|
|
250
287
|
class="flex min-h-screen bg-[var(--esc-panel-bg)]"
|
|
251
288
|
:style="{ colorScheme: panelConfig.mode === 'light' ? 'light' : 'dark' }"
|
|
252
289
|
>
|
|
290
|
+
<!-- Drawer backdrop (narrow screens only) -->
|
|
291
|
+
<div
|
|
292
|
+
v-if="navOpen"
|
|
293
|
+
class="fixed inset-0 z-30 bg-black/40 lg:hidden"
|
|
294
|
+
aria-hidden="true"
|
|
295
|
+
data-testid="esc-nav-backdrop"
|
|
296
|
+
@click="closeNav"
|
|
297
|
+
></div>
|
|
298
|
+
|
|
253
299
|
<!-- Sidebar -->
|
|
254
300
|
<aside
|
|
255
|
-
|
|
301
|
+
id="esc-admin-sidebar"
|
|
302
|
+
:class="[
|
|
303
|
+
'fixed inset-y-0 left-0 z-40 flex w-64 max-w-[85vw] flex-col border-r border-[var(--esc-panel-border)] bg-[var(--esc-panel-sidebar-bg)] transition-[transform,visibility] duration-200 lg:visible lg:translate-x-0 lg:shadow-none',
|
|
304
|
+
navOpen ? 'visible translate-x-0 shadow-2xl' : 'invisible -translate-x-full',
|
|
305
|
+
]"
|
|
306
|
+
data-testid="esc-admin-sidebar"
|
|
256
307
|
>
|
|
257
308
|
<!-- Logo -->
|
|
258
309
|
<div class="flex h-16 items-center gap-3 px-5">
|
|
259
|
-
<div
|
|
310
|
+
<div
|
|
311
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--esc-panel-logo-tile-bg,var(--esc-panel-border-input))] text-[var(--esc-panel-logo-tile-fg,var(--esc-panel-text))]"
|
|
312
|
+
data-testid="esc-logo-tile"
|
|
313
|
+
>
|
|
260
314
|
<img
|
|
261
315
|
v-if="typeof panelConfig.logo === 'string'"
|
|
262
316
|
:src="panelConfig.logo"
|
|
@@ -288,7 +342,7 @@ function isActive(href) {
|
|
|
288
342
|
</g>
|
|
289
343
|
</svg>
|
|
290
344
|
</div>
|
|
291
|
-
<div>
|
|
345
|
+
<div class="min-w-0">
|
|
292
346
|
<span class="text-sm font-bold text-[var(--esc-panel-text)] tracking-wide">{{
|
|
293
347
|
panelConfig.appName
|
|
294
348
|
}}</span>
|
|
@@ -297,6 +351,17 @@ function isActive(href) {
|
|
|
297
351
|
>ADMIN</span
|
|
298
352
|
>
|
|
299
353
|
</div>
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
class="-mr-2 ml-auto rounded-lg p-2 text-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text)] lg:hidden"
|
|
357
|
+
aria-label="Close navigation"
|
|
358
|
+
data-testid="esc-nav-close"
|
|
359
|
+
@click="closeNav"
|
|
360
|
+
>
|
|
361
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
362
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
363
|
+
</svg>
|
|
364
|
+
</button>
|
|
300
365
|
</div>
|
|
301
366
|
|
|
302
367
|
<!-- Nav -->
|
|
@@ -309,15 +374,16 @@ function isActive(href) {
|
|
|
309
374
|
:class="[
|
|
310
375
|
'group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] font-medium transition-all',
|
|
311
376
|
isActive(link.href)
|
|
312
|
-
? 'bg-[var(--esc-panel-active)] text-[var(--esc-panel-text)]'
|
|
377
|
+
? 'bg-[var(--esc-panel-active)] text-[var(--esc-panel-active-text,var(--esc-panel-text))]'
|
|
313
378
|
: 'text-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
|
|
314
379
|
]"
|
|
380
|
+
@click="closeNav"
|
|
315
381
|
>
|
|
316
382
|
<svg
|
|
317
383
|
:class="[
|
|
318
384
|
'h-[18px] w-[18px] shrink-0',
|
|
319
385
|
isActive(link.href)
|
|
320
|
-
? 'text-[var(--esc-panel-text)]'
|
|
386
|
+
? 'text-[var(--esc-panel-active-text,var(--esc-panel-text))]'
|
|
321
387
|
: 'text-[var(--esc-panel-text-muted)] group-hover:text-[var(--esc-panel-text-tertiary)]',
|
|
322
388
|
]"
|
|
323
389
|
fill="none"
|
|
@@ -342,7 +408,7 @@ function isActive(href) {
|
|
|
342
408
|
<Link
|
|
343
409
|
v-if="isAgent"
|
|
344
410
|
:href="`${prefix}/agent`"
|
|
345
|
-
class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-[var(--esc-panel-text-muted)] transition-colors hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
411
|
+
class="flex items-center gap-2.5 whitespace-nowrap rounded-lg px-3 py-2 text-[13px] text-[var(--esc-panel-text-muted)] transition-colors hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
346
412
|
>
|
|
347
413
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
348
414
|
<path
|
|
@@ -355,7 +421,7 @@ function isActive(href) {
|
|
|
355
421
|
</Link>
|
|
356
422
|
<Link
|
|
357
423
|
:href="prefix"
|
|
358
|
-
class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-[var(--esc-panel-text-muted)] transition-colors hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
424
|
+
class="flex items-center gap-2.5 whitespace-nowrap rounded-lg px-3 py-2 text-[13px] text-[var(--esc-panel-text-muted)] transition-colors hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
359
425
|
>
|
|
360
426
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
361
427
|
<path
|
|
@@ -370,11 +436,11 @@ function isActive(href) {
|
|
|
370
436
|
<!-- User -->
|
|
371
437
|
<div class="mt-2 flex items-center gap-3 rounded-lg bg-[var(--esc-panel-hover)] px-3 py-2.5">
|
|
372
438
|
<div
|
|
373
|
-
class="flex h-7 w-7 items-center justify-center rounded-md bg-[var(--esc-panel-active)] text-xs font-semibold text-[var(--esc-panel-text-tertiary)]"
|
|
439
|
+
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-[var(--esc-panel-active)] text-xs font-semibold text-[var(--esc-panel-text-tertiary)]"
|
|
374
440
|
>
|
|
375
441
|
{{ userInitial }}
|
|
376
442
|
</div>
|
|
377
|
-
<span class="text-sm text-[var(--esc-panel-text-tertiary)]">{{ userName }}</span>
|
|
443
|
+
<span class="truncate text-sm text-[var(--esc-panel-text-tertiary)]">{{ userName }}</span>
|
|
378
444
|
</div>
|
|
379
445
|
</div>
|
|
380
446
|
|
|
@@ -401,23 +467,42 @@ function isActive(href) {
|
|
|
401
467
|
</aside>
|
|
402
468
|
|
|
403
469
|
<!-- Main content -->
|
|
404
|
-
<div class="flex flex-1 flex-col pl-64">
|
|
470
|
+
<div class="flex min-w-0 flex-1 flex-col lg:pl-64">
|
|
405
471
|
<!-- Top bar -->
|
|
406
472
|
<header
|
|
407
473
|
role="banner"
|
|
408
|
-
class="sticky top-0 z-20 flex h-14 items-center border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-topbar-bg)] px-
|
|
474
|
+
class="sticky top-0 z-20 flex h-14 items-center gap-3 border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-header-bg,var(--esc-panel-topbar-bg))] px-4 backdrop-blur-xl sm:px-6"
|
|
409
475
|
>
|
|
410
|
-
<
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
class="-ml-2 rounded-lg p-2 text-[var(--esc-panel-header-text,var(--esc-panel-text))] hover:bg-[var(--esc-panel-hover)] lg:hidden"
|
|
479
|
+
aria-controls="esc-admin-sidebar"
|
|
480
|
+
:aria-expanded="navOpen ? 'true' : 'false'"
|
|
481
|
+
aria-label="Open navigation"
|
|
482
|
+
data-testid="esc-nav-toggle"
|
|
483
|
+
@click="toggleNav"
|
|
484
|
+
>
|
|
485
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
486
|
+
<path
|
|
487
|
+
stroke-linecap="round"
|
|
488
|
+
stroke-linejoin="round"
|
|
489
|
+
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
|
490
|
+
/>
|
|
491
|
+
</svg>
|
|
492
|
+
</button>
|
|
493
|
+
<h1 class="truncate text-sm font-semibold text-[var(--esc-panel-header-text,var(--esc-panel-text))]">
|
|
494
|
+
{{ title }}
|
|
495
|
+
</h1>
|
|
411
496
|
</header>
|
|
412
497
|
|
|
413
498
|
<!-- Page content -->
|
|
414
|
-
<main role="main" class="flex-1 p-6">
|
|
499
|
+
<main ref="contentRoot" role="main" class="min-w-0 flex-1 p-4 sm:p-6">
|
|
415
500
|
<slot />
|
|
416
501
|
</main>
|
|
417
502
|
</div>
|
|
418
503
|
</div>
|
|
419
504
|
|
|
420
|
-
<!-- MODE 2: Agent —
|
|
505
|
+
<!-- MODE 2: Agent — top-nav layout; the links fold behind a menu button below lg -->
|
|
421
506
|
<div
|
|
422
507
|
v-else-if="isAgentSection"
|
|
423
508
|
class="min-h-screen bg-[var(--esc-panel-bg)]"
|
|
@@ -425,12 +510,16 @@ function isActive(href) {
|
|
|
425
510
|
>
|
|
426
511
|
<!-- Top nav -->
|
|
427
512
|
<nav
|
|
428
|
-
|
|
513
|
+
aria-label="Agent navigation"
|
|
514
|
+
class="sticky top-0 z-30 border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-header-bg,var(--esc-panel-sidebar-bg))] backdrop-blur-xl"
|
|
429
515
|
>
|
|
430
|
-
<div class="mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
|
516
|
+
<div class="mx-auto flex h-14 max-w-7xl items-center justify-between gap-3 px-4 sm:px-6 lg:px-8">
|
|
431
517
|
<!-- Left: branding -->
|
|
432
|
-
<div class="flex items-center gap-3">
|
|
433
|
-
<div
|
|
518
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
519
|
+
<div
|
|
520
|
+
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--esc-panel-logo-tile-bg,var(--esc-panel-border-input))] text-[var(--esc-panel-logo-tile-fg,var(--esc-panel-text))]"
|
|
521
|
+
data-testid="esc-logo-tile"
|
|
522
|
+
>
|
|
434
523
|
<img
|
|
435
524
|
v-if="typeof panelConfig.logo === 'string'"
|
|
436
525
|
:src="panelConfig.logo"
|
|
@@ -462,22 +551,23 @@ function isActive(href) {
|
|
|
462
551
|
</g>
|
|
463
552
|
</svg>
|
|
464
553
|
</div>
|
|
465
|
-
<span
|
|
466
|
-
|
|
467
|
-
|
|
554
|
+
<span
|
|
555
|
+
class="truncate text-sm font-bold tracking-wide text-[var(--esc-panel-header-text,var(--esc-panel-text))]"
|
|
556
|
+
>{{ panelConfig.appName }}</span
|
|
557
|
+
>
|
|
468
558
|
</div>
|
|
469
559
|
|
|
470
|
-
<!-- Center: nav links -->
|
|
471
|
-
<div class="
|
|
560
|
+
<!-- Center: nav links (lg and up) -->
|
|
561
|
+
<div class="hidden items-center gap-1 lg:flex">
|
|
472
562
|
<Link
|
|
473
563
|
v-for="link in agentLinks"
|
|
474
564
|
:key="link.href"
|
|
475
565
|
:href="link.href"
|
|
476
|
-
:aria-current="isActive(link.href) ? 'page' : undefined"
|
|
566
|
+
:aria-current="isActive(link.href, link.exact) ? 'page' : undefined"
|
|
477
567
|
:class="[
|
|
478
|
-
'flex items-center gap-2 rounded-lg px-3.5 py-1.5 text-[13px] font-medium transition-all',
|
|
479
|
-
isActive(link.href)
|
|
480
|
-
? 'bg-[var(--esc-panel-active)] text-[var(--esc-panel-text)]'
|
|
568
|
+
'flex items-center gap-2 whitespace-nowrap rounded-lg px-3.5 py-1.5 text-[13px] font-medium transition-all',
|
|
569
|
+
isActive(link.href, link.exact)
|
|
570
|
+
? 'bg-[var(--esc-panel-active)] text-[var(--esc-panel-active-text,var(--esc-panel-text))]'
|
|
481
571
|
: 'text-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
|
|
482
572
|
]"
|
|
483
573
|
>
|
|
@@ -488,36 +578,110 @@ function isActive(href) {
|
|
|
488
578
|
</Link>
|
|
489
579
|
</div>
|
|
490
580
|
|
|
491
|
-
<!-- Right: user + links -->
|
|
492
|
-
<div class="
|
|
581
|
+
<!-- Right: user + links (lg and up) -->
|
|
582
|
+
<div class="hidden items-center gap-3 lg:flex">
|
|
493
583
|
<Link
|
|
494
584
|
v-if="isAdmin"
|
|
495
585
|
:href="`${prefix}/admin/reports`"
|
|
496
|
-
class="text-[13px] text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-text)]"
|
|
586
|
+
class="whitespace-nowrap text-[13px] text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-text)]"
|
|
497
587
|
>
|
|
498
588
|
Admin
|
|
499
589
|
</Link>
|
|
500
590
|
<Link
|
|
501
591
|
:href="prefix"
|
|
502
|
-
class="text-[13px] text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-text)]"
|
|
592
|
+
class="whitespace-nowrap text-[13px] text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-text)]"
|
|
503
593
|
>
|
|
504
594
|
Back to App
|
|
505
595
|
</Link>
|
|
506
596
|
<div class="ml-1 h-5 w-px bg-[var(--esc-panel-active)]"></div>
|
|
507
597
|
<div class="flex items-center gap-2">
|
|
508
598
|
<div
|
|
509
|
-
class="flex h-7 w-7 items-center justify-center rounded-md bg-[var(--esc-panel-active)] text-[10px] font-semibold text-[var(--esc-panel-text-tertiary)]"
|
|
599
|
+
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-[var(--esc-panel-active)] text-[10px] font-semibold text-[var(--esc-panel-text-tertiary)]"
|
|
510
600
|
>
|
|
511
601
|
{{ userInitial }}
|
|
512
602
|
</div>
|
|
513
|
-
<span class="text-[13px] text-[var(--esc-panel-text-tertiary)]">{{
|
|
603
|
+
<span class="max-w-[12rem] truncate text-[13px] text-[var(--esc-panel-text-tertiary)]">{{
|
|
604
|
+
userName
|
|
605
|
+
}}</span>
|
|
514
606
|
</div>
|
|
515
607
|
</div>
|
|
608
|
+
|
|
609
|
+
<!-- Menu button (below lg) -->
|
|
610
|
+
<button
|
|
611
|
+
type="button"
|
|
612
|
+
class="-mr-2 rounded-lg p-2 text-[var(--esc-panel-header-text,var(--esc-panel-text))] hover:bg-[var(--esc-panel-hover)] lg:hidden"
|
|
613
|
+
aria-controls="esc-agent-menu"
|
|
614
|
+
:aria-expanded="navOpen ? 'true' : 'false'"
|
|
615
|
+
:aria-label="navOpen ? 'Close navigation' : 'Open navigation'"
|
|
616
|
+
data-testid="esc-nav-toggle"
|
|
617
|
+
@click="toggleNav"
|
|
618
|
+
>
|
|
619
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
620
|
+
<path v-if="navOpen" stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
621
|
+
<path
|
|
622
|
+
v-else
|
|
623
|
+
stroke-linecap="round"
|
|
624
|
+
stroke-linejoin="round"
|
|
625
|
+
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
|
626
|
+
/>
|
|
627
|
+
</svg>
|
|
628
|
+
</button>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<!-- Collapsed menu (below lg) -->
|
|
632
|
+
<div
|
|
633
|
+
v-if="navOpen"
|
|
634
|
+
id="esc-agent-menu"
|
|
635
|
+
class="space-y-1 border-t border-[var(--esc-panel-border)] px-4 py-3 lg:hidden"
|
|
636
|
+
data-testid="esc-agent-menu"
|
|
637
|
+
>
|
|
638
|
+
<Link
|
|
639
|
+
v-for="link in agentLinks"
|
|
640
|
+
:key="link.href"
|
|
641
|
+
:href="link.href"
|
|
642
|
+
:aria-current="isActive(link.href, link.exact) ? 'page' : undefined"
|
|
643
|
+
:class="[
|
|
644
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium',
|
|
645
|
+
isActive(link.href, link.exact)
|
|
646
|
+
? 'bg-[var(--esc-panel-active)] text-[var(--esc-panel-active-text,var(--esc-panel-text))]'
|
|
647
|
+
: 'text-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
|
|
648
|
+
]"
|
|
649
|
+
@click="closeNav"
|
|
650
|
+
>
|
|
651
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
652
|
+
<path stroke-linecap="round" stroke-linejoin="round" :d="link.icon" />
|
|
653
|
+
</svg>
|
|
654
|
+
{{ link.label }}
|
|
655
|
+
</Link>
|
|
656
|
+
<div class="my-2 h-px bg-[var(--esc-panel-border)]"></div>
|
|
657
|
+
<Link
|
|
658
|
+
v-if="isAdmin"
|
|
659
|
+
:href="`${prefix}/admin/reports`"
|
|
660
|
+
class="block rounded-lg px-3 py-2 text-sm text-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text)]"
|
|
661
|
+
@click="closeNav"
|
|
662
|
+
>
|
|
663
|
+
Admin
|
|
664
|
+
</Link>
|
|
665
|
+
<Link
|
|
666
|
+
:href="prefix"
|
|
667
|
+
class="block rounded-lg px-3 py-2 text-sm text-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text)]"
|
|
668
|
+
@click="closeNav"
|
|
669
|
+
>
|
|
670
|
+
Back to App
|
|
671
|
+
</Link>
|
|
672
|
+
<div class="flex items-center gap-2 px-3 py-2">
|
|
673
|
+
<div
|
|
674
|
+
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-[var(--esc-panel-active)] text-[10px] font-semibold text-[var(--esc-panel-text-tertiary)]"
|
|
675
|
+
>
|
|
676
|
+
{{ userInitial }}
|
|
677
|
+
</div>
|
|
678
|
+
<span class="truncate text-sm text-[var(--esc-panel-text-tertiary)]">{{ userName }}</span>
|
|
679
|
+
</div>
|
|
516
680
|
</div>
|
|
517
681
|
</nav>
|
|
518
682
|
|
|
519
683
|
<!-- Page content -->
|
|
520
|
-
<main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
684
|
+
<main ref="contentRoot" class="mx-auto min-w-0 max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
521
685
|
<slot />
|
|
522
686
|
</main>
|
|
523
687
|
|
|
@@ -626,3 +790,25 @@ function isActive(href) {
|
|
|
626
790
|
</footer>
|
|
627
791
|
</div>
|
|
628
792
|
</template>
|
|
793
|
+
|
|
794
|
+
<style>
|
|
795
|
+
/* Tables in the panels scroll sideways inside this container rather than
|
|
796
|
+
widening the page. A shadow on an edge says there is more past it; the
|
|
797
|
+
layout keeps data-esc-overflow current (see utils/scrollAffordance.js). */
|
|
798
|
+
.esc-table-scroll {
|
|
799
|
+
overflow-x: auto;
|
|
800
|
+
-webkit-overflow-scrolling: touch;
|
|
801
|
+
scrollbar-width: thin;
|
|
802
|
+
}
|
|
803
|
+
.esc-table-scroll[data-esc-overflow='end'] {
|
|
804
|
+
box-shadow: inset -14px 0 12px -12px rgb(0 0 0 / 0.3);
|
|
805
|
+
}
|
|
806
|
+
.esc-table-scroll[data-esc-overflow='start'] {
|
|
807
|
+
box-shadow: inset 14px 0 12px -12px rgb(0 0 0 / 0.3);
|
|
808
|
+
}
|
|
809
|
+
.esc-table-scroll[data-esc-overflow='start end'] {
|
|
810
|
+
box-shadow:
|
|
811
|
+
inset 14px 0 12px -12px rgb(0 0 0 / 0.3),
|
|
812
|
+
inset -14px 0 12px -12px rgb(0 0 0 / 0.3);
|
|
813
|
+
}
|
|
814
|
+
</style>
|
|
@@ -56,7 +56,7 @@ function isGroupAllChecked(group) {
|
|
|
56
56
|
</script>
|
|
57
57
|
|
|
58
58
|
<template>
|
|
59
|
-
<div class="
|
|
59
|
+
<div class="esc-table-scroll rounded-xl border border-white/[0.06] bg-neutral-900/60">
|
|
60
60
|
<table class="min-w-full divide-y divide-white/[0.06]">
|
|
61
61
|
<thead>
|
|
62
62
|
<tr class="bg-white/[0.02]">
|
|
@@ -33,7 +33,10 @@ function selectTab(tab) {
|
|
|
33
33
|
|
|
34
34
|
<template>
|
|
35
35
|
<div
|
|
36
|
-
:class="[
|
|
36
|
+
:class="[
|
|
37
|
+
'flex items-center gap-0 overflow-x-auto border-b',
|
|
38
|
+
escDark ? 'border-white/[0.08]' : 'border-gray-200',
|
|
39
|
+
]"
|
|
37
40
|
role="tablist"
|
|
38
41
|
aria-label="Ticket view tabs"
|
|
39
42
|
>
|
|
@@ -43,7 +46,7 @@ function selectTab(tab) {
|
|
|
43
46
|
role="tab"
|
|
44
47
|
:aria-selected="activeTab === tab.key"
|
|
45
48
|
:class="[
|
|
46
|
-
'relative px-4 py-2.5 text-sm font-medium transition-colors whitespace-nowrap',
|
|
49
|
+
'relative shrink-0 px-4 py-2.5 text-sm font-medium transition-colors whitespace-nowrap',
|
|
47
50
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-0',
|
|
48
51
|
activeTab === tab.key
|
|
49
52
|
? escDark
|
|
@@ -204,7 +204,7 @@ const colCount = computed(() => {
|
|
|
204
204
|
|
|
205
205
|
<template>
|
|
206
206
|
<!-- Dark mode -->
|
|
207
|
-
<div v-if="escDark" class="
|
|
207
|
+
<div v-if="escDark" class="esc-table-scroll rounded-xl border border-white/[0.06] bg-neutral-900/60">
|
|
208
208
|
<table class="min-w-full divide-y divide-white/[0.06]">
|
|
209
209
|
<thead>
|
|
210
210
|
<tr class="bg-white/[0.02]">
|
|
@@ -430,7 +430,7 @@ const colCount = computed(() => {
|
|
|
430
430
|
</div>
|
|
431
431
|
|
|
432
432
|
<!-- Light mode -->
|
|
433
|
-
<div v-else class="
|
|
433
|
+
<div v-else class="esc-table-scroll rounded-lg border border-gray-200 bg-white">
|
|
434
434
|
<table class="min-w-full divide-y divide-gray-200">
|
|
435
435
|
<thead class="bg-gray-50">
|
|
436
436
|
<tr>
|