Files
servicedesk/src/resources/css/app.css
Kacper 0b06687ea1 v1.1.2
- Real-time updates (Laravel Reverb): live operator queue, live ticket
  chat/detail updates for operator and client, periodic fallback refresh
  with a visible countdown as a backstop for dropped websocket connections.
- SLA automation rules (Admin > Automatyzacja SLA): act on a ticket after
  N minutes of customer silence (change priority/status/team/assignee),
  evaluated every 15 minutes, reusing TicketService's own setters so
  automated changes get the same history/notification/broadcast a manual
  change would.
- New notification: every operator on a matching team gets notified when
  a new ticket lands in one of their subcategories.
- BookStack knowledge-base sidebar now also shown on the client's own
  ticket view (previously operator-only); suggestions everywhere now load
  in after first paint instead of blocking it.
- Client ticket view: shows assigned operator + team; page widened to
  match the operator's.
- Notification bell shows unread only; read notifications disappear
  instead of just dimming.
- Stats dashboard: sectioned layout, new breakdowns (by subcategory, CSAT
  by team/operator, top clients, client x subcategory cross-tab).
- Mobile: nav dropdowns (theme/notifications/profile) now expand full
  width instead of overflowing off-screen below 640px.
- Fixed two bugs that silently disabled all real-time updates (missing
  CSRF header on Echo's private-channel auth; a script-load-order race
  that could miss the livewire:init event) and the mariadb healthcheck
  (world-writable credentials file on this stack's NFS mount).
- Assorted test-suite fixes (roles virtual attribute needs the roles
  table seeded; a few missing seeds/wrong assertions found along the way).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 20:43:05 +02:00

475 lines
18 KiB
CSS

@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../views/*.blade.php';
@source '../views/**/*.blade.php';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
:root {
--color-accent: #7c6fd6;
/* Shades derive from --color-accent so an admin-picked brand color
(App\Support\Settings::accentColor(), overridden in layouts/app.blade.php)
re-themes buttons/tags/etc. everywhere without listing each tint by hand. */
--color-accent-100: color-mix(in srgb, var(--color-accent) 14%, white);
--color-accent-200: color-mix(in srgb, var(--color-accent) 26%, white);
--color-accent-700: color-mix(in srgb, var(--color-accent) 82%, black);
--color-accent-800: color-mix(in srgb, var(--color-accent) 64%, black);
--color-accent-2: #5aa9a3;
--color-accent-2-200: #d9efed;
--color-accent-2-700: #3d7f79;
--color-neutral-200: #e7e8ee;
--color-neutral-700: #545766;
--color-neutral-900: #1b1c26;
--color-danger: #d9534f;
--color-success: #3fa66e;
--color-warning: #d99a3f;
}
[data-theme='dark'] {
--color-bg: #161826;
--color-surface: #232532;
--color-text: #e9e9ed;
--color-divider: color-mix(in srgb, #e9e9ed 16%, transparent);
--shadow-sm: 0 0 0 1px #3f424d;
--shadow-md: 0 0 0 1px #595d6c, 0 6px 18px rgba(0, 0, 0, 0.55);
--shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0, 0, 0, 0.65);
}
[data-theme='light'] {
--color-bg: #f3f5fe;
--color-surface: #ffffff;
--color-text: #23252d;
--color-divider: color-mix(in srgb, #23252d 14%, transparent);
--shadow-sm: 0 0 0 1px #cfd3e5;
--shadow-md: 0 0 0 1px #cfd3e5, 0 6px 18px rgba(41, 43, 49, 0.1);
--shadow-lg: 0 0 0 1px #b2b6ca, 0 16px 40px rgba(41, 43, 49, 0.16);
}
[data-theme='light'] .tag-accent { background: var(--color-accent-200); color: var(--color-accent-700); }
[data-theme='light'] .tag-accent-2 { background: var(--color-accent-2-200); color: var(--color-accent-2-700); }
[data-theme='light'] .tag-neutral { background: var(--color-neutral-200); color: var(--color-neutral-700); }
[data-theme='light'] .dialog-backdrop { background: color-mix(in srgb, var(--color-neutral-900) 35%, transparent); }
[data-theme='light'] .login-notice-info { background: color-mix(in srgb, var(--color-accent) 14%, white); color: var(--color-accent-700); }
[data-theme='light'] .login-notice-warning { background: color-mix(in srgb, var(--color-warning) 20%, white); color: color-mix(in srgb, var(--color-warning) 80%, black); }
[data-theme='light'] .login-notice-success { background: color-mix(in srgb, var(--color-success) 18%, white); color: color-mix(in srgb, var(--color-success) 75%, black); }
[data-theme='light'] .login-notice-danger { background: color-mix(in srgb, var(--color-danger) 16%, white); color: color-mix(in srgb, var(--color-danger) 80%, black); }
* { box-sizing: border-box; }
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans);
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: var(--color-divider); border-radius: 8px; }
[x-cloak] { display: none !important; }
@layer components {
.nav {
display: flex;
align-items: center;
gap: 16px;
}
.nav-brand {
font-weight: 600;
font-size: 15px;
margin-right: auto;
}
.nav a {
color: var(--color-accent);
font-size: 13px;
text-decoration: none;
}
.nav a:hover { text-decoration: underline; }
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
padding: 8px 14px;
border-radius: 8px;
border: 1px solid transparent;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s, opacity 0.15s;
}
.btn-primary { background: var(--color-accent); color: #fff; }
.btn-primary:hover { background: var(--color-accent-700); }
.btn-secondary { background: transparent; border-color: var(--color-divider); color: var(--color-text); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 6%, transparent); }
.btn-ghost { background: transparent; color: var(--color-accent); border: none; }
.btn-ghost:hover { text-decoration: underline; }
.btn-icon { padding: 8px; width: 34px; height: 34px; }
.btn-block { width: 100%; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.card {
display: flex;
flex-direction: column;
background: var(--color-surface);
border: 1px solid var(--color-divider);
border-radius: 10px;
}
.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.elev-lg { box-shadow: var(--shadow-lg); }
.card-kicker { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.card-title { font-weight: 600; font-size: 14px; }
.card-body { font-size: 13px; color: color-mix(in srgb, var(--color-text) 75%, transparent); margin: 0; }
.card-meta { font-size: 12px; color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: 12.5px; font-weight: 500; }
.input {
font-family: inherit;
font-size: 13.5px;
padding: 8px 10px;
border-radius: 7px;
border: 1px solid var(--color-divider);
background: var(--color-bg);
color: var(--color-text);
width: 100%;
}
.input:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }
textarea.input { min-height: 90px; resize: vertical; }
.radio {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
cursor: pointer;
}
.tag {
display: inline-flex;
align-items: center;
font-size: 11px;
letter-spacing: 0.02em;
padding: 3px 10px;
border-radius: 6px;
}
.tag-accent { background: color-mix(in srgb, var(--color-accent) 20%, transparent); color: var(--color-accent); }
.tag-accent-2 { background: color-mix(in srgb, var(--color-accent-2) 20%, transparent); color: var(--color-accent-2); }
.tag-neutral { background: color-mix(in srgb, var(--color-text) 12%, transparent); color: color-mix(in srgb, var(--color-text) 70%, transparent); }
.tag-outline { background: transparent; border: 1px solid var(--color-divider); color: var(--color-text); }
.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.hr { height: 1px; background: var(--color-divider); }
.table { width: 100%; border-collapse: collapse; border-top: 1px solid var(--color-divider); }
.table th, .table td { text-align: left; padding: 10px 12px; font-size: 13px; border-bottom: 1px solid var(--color-divider); }
.table th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.dialog-backdrop {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, black 55%, transparent);
padding: 20px;
}
.dialog {
display: flex;
flex-direction: column;
gap: 14px;
width: 100%;
background: var(--color-surface);
border-radius: 12px;
box-shadow: var(--shadow-lg);
padding: 24px;
}
.dialog-title { font-size: 16px; font-weight: 600; }
.dialog-body { font-size: 13.5px; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
.seg { display: inline-flex; border: 1px solid var(--color-divider); border-radius: 8px; overflow: hidden; }
.seg-opt {
display: flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
padding: 6px 12px;
cursor: pointer;
}
.seg-opt:has(input:checked) { background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent); }
.seg-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.theme-toggle-option {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
text-align: left;
border: none;
background: transparent;
color: var(--color-text);
font-family: inherit;
font-size: 12.5px;
padding: 8px 12px;
cursor: pointer;
}
.theme-toggle-option:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
}
/* Unlayered (not inside @layer components above) on purpose: it shares the
.ql-editor class with Quill's own CSS (see the login-notice component's
comment) to inherit alignment/heading/blockquote/code-block formatting,
and Tailwind's @layer'd rules always lose to unlayered ones — including
Quill's CDN stylesheet — regardless of source order, so anything meant to
override .ql-editor here (like the padding reset) has to be unlayered too. */
.login-notice {
padding: 0 14px;
border-radius: 8px;
font-size: 13px;
line-height: 1.5;
height: auto;
min-height: 0;
overflow: visible;
}
.login-notice > *:first-child { margin-top: 0; }
.login-notice > *:last-child { margin-bottom: 0; }
.login-notice-info { background: color-mix(in srgb, var(--color-accent) 20%, transparent); color: var(--color-accent); }
.login-notice-warning { background: color-mix(in srgb, var(--color-warning) 20%, transparent); color: var(--color-warning); }
.login-notice-success { background: color-mix(in srgb, var(--color-success) 20%, transparent); color: var(--color-success); }
.login-notice-danger { background: color-mix(in srgb, var(--color-danger) 20%, transparent); color: var(--color-danger); }
/* ---- Responsive layout (phones/tablets) ---- */
.side-rail {
width: 230px;
flex: none;
display: flex;
flex-direction: column;
border-right: 1px solid var(--color-divider);
transition: width 0.15s;
}
.side-rail-scroll {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
min-height: 0;
overflow: auto;
}
.side-rail-toggle {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
flex: none;
padding: 7px 10px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-family: inherit;
color: color-mix(in srgb, var(--color-text) 60%, transparent);
}
.side-rail-toggle:hover { background: color-mix(in srgb, var(--color-text) 6%, transparent); color: var(--color-text); }
@media (min-width: 781px) {
/* Desktop: collapse down to an icons-only rail; hovering it while
collapsed peeks it back open without changing the saved preference. */
.side-rail-collapsed { width: 60px; }
.side-rail-collapsed .side-rail-label,
.side-rail-collapsed .side-rail-group-label,
.side-rail-collapsed .side-rail-count,
.side-rail-collapsed .side-rail-toggle-label { display: none; }
.side-rail-collapsed .side-rail-item,
.side-rail-collapsed .side-rail-toggle { justify-content: center; }
.side-rail-collapsed:hover { width: 230px; }
.side-rail-collapsed:hover .side-rail-label,
.side-rail-collapsed:hover .side-rail-group-label,
.side-rail-collapsed:hover .side-rail-count,
.side-rail-collapsed:hover .side-rail-toggle-label { display: initial; }
.side-rail-collapsed:hover .side-rail-item,
.side-rail-collapsed:hover .side-rail-toggle { justify-content: flex-start; }
}
.table-wrap { width: 100%; overflow-x: auto; }
.queue-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
.queue-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.queue-actions-pair { display: flex; gap: 8px; }
.queue-filters { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; align-items: center; }
.queue-filters-search { width: 220px; }
.queue-filters-columns { position: relative; margin-left: auto; }
.queue-filters-saved { position: relative; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.spin { display: inline-block; animation: spin 0.8s linear infinite; }
.main-col { flex: 1; min-width: 320px; }
.aside-col { width: 300px; flex: none; }
/* Opt-in wider sidebar (operator ticket view, which also shows BookStack
suggestions) — at least 50% wider than the default .aside-col. */
.aside-col-wide { width: 460px; }
.wizard-step { width: 80px; flex: none; }
.wizard-connector { width: 56px; flex: none; }
@media (max-width: 780px) {
.app-split { flex-direction: column; }
.side-rail {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--color-divider);
}
/* Mobile: collapse hides the whole list, leaving just the toggle bar. */
.side-rail-collapsed { width: 100%; }
.side-rail-collapsed .side-rail-scroll { display: none; }
}
@media (max-width: 640px) {
.page-pad { padding: 16px !important; }
.nav { padding-left: 14px !important; padding-right: 14px !important; gap: 10px; }
.nav-panel-label { display: none; }
/* Theme/notifications/profile dropdowns are anchored (position:absolute)
to their own small trigger button by default, which overflows off the
edge of narrow screens once their fixed width no longer fits between
the button and the viewport edge. Dropping the wrapper's own
positioning context makes .nav itself (already position:relative) the
containing block instead, so left/right:0 spans the whole navbar
width rather than the button's. */
.nav-dropdown-wrap { position: static !important; }
.nav-dropdown {
left: 0 !important;
right: 0 !important;
width: auto !important;
min-width: 0 !important;
margin-top: 8px !important;
}
.profile-menu-name { display: none; }
.main-col { min-width: 0; }
.aside-col { width: 100%; }
.aside-col-wide { width: 100%; }
.wizard-step { width: 58px; }
.wizard-connector { width: 22px; }
.dialog { padding: 18px; }
/* Queue toolbar/filters: a clean vertical stack of full-width controls
instead of a wrapped row of odd-sized chips. */
.queue-toolbar { flex-direction: column; align-items: stretch; }
.queue-actions { margin-left: 0; flex-direction: column; }
.queue-actions .btn { width: 100%; }
.queue-actions-pair { width: 100%; }
.queue-actions-pair .btn { flex: 1; width: auto; }
.queue-filters { flex-direction: column; align-items: stretch; }
.queue-filters-search { width: 100%; }
.queue-filters select.input { width: 100% !important; }
.queue-filters-columns { margin-left: 0; }
.queue-filters-columns .btn { width: 100%; }
/* Wide data tables (opted in via .table-cards-mobile) render as a
stacked list of cards instead of scrolling sideways — every visible
column becomes a labeled row, so nothing is hidden off-screen. */
.table-cards-mobile { border-top: none; }
.table-cards-mobile thead {
position: absolute;
width: 1px; height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.table-cards-mobile, .table-cards-mobile tbody, .table-cards-mobile tr, .table-cards-mobile td {
display: block;
width: 100%;
}
.table-cards-mobile tr {
position: relative;
border: 1px solid var(--color-divider);
border-radius: 10px;
padding: 10px 40px 10px 12px;
margin-bottom: 10px;
}
.table-cards-mobile tr:hover { background: none; }
.table-cards-mobile td {
border-bottom: none;
padding: 3px 0;
}
.table-cards-mobile td[data-label]::before {
content: attr(data-label);
display: block;
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: color-mix(in srgb, var(--color-text) 50%, transparent);
}
.table-cards-mobile td.td-title { font-weight: 600; font-size: 14.5px; }
.table-cards-mobile td.td-select {
position: absolute;
top: 10px;
right: 10px;
width: auto;
padding: 0;
}
}
/* ---- Operator stats (Panel > Statystyki): stat tiles + hand-rolled bar/column charts ---- */
.stat-tile {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 16px;
background: var(--color-surface);
border: 1px solid var(--color-divider);
border-radius: 10px;
}
.stat-tile-label { font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase; color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.stat-tile-value { font-size: 24px; font-weight: 600; line-height: 1.15; font-variant-numeric: proportional-nums; }
.stat-tile-meta { font-size: 12px; color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.bar-row { display: flex; align-items: center; gap: 10px; }
.bar-row + .bar-row { margin-top: 6px; }
.bar-row-label {
flex: 0 0 152px;
font-size: 12.5px;
color: var(--color-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bar-track {
flex: 1;
height: 14px;
border-radius: 7px;
background: color-mix(in srgb, var(--color-text) 8%, transparent);
overflow: hidden;
}
.bar-fill { height: 100%; border-radius: 0 7px 7px 0; min-width: 3px; }
.bar-row-value {
flex: 0 0 32px;
text-align: right;
font-size: 12.5px;
font-variant-numeric: tabular-nums;
color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.trend-chart { display: flex; flex-direction: column; gap: 4px; }
.trend-cols { display: flex; align-items: flex-end; gap: 2px; height: 84px; }
.trend-col-track { flex: 1; display: flex; align-items: flex-end; height: 100%; min-width: 2px; }
.trend-col { width: 100%; border-radius: 3px 3px 0 0; min-height: 2px; }
.trend-x-labels { display: flex; gap: 2px; }
.trend-x-label { flex: 1; font-size: 10px; text-align: center; color: color-mix(in srgb, var(--color-text) 45%, transparent); white-space: nowrap; }