Projektdetails und Fortschritt kundenverständlich
Der Server bereitet Commits, Tickets und Arbeitseinträge für Kunden auf (server/services/customerProjectView.js), die Rohdaten bleiben intern: - keine Co-Authored-By-/Signed-off-by-Zeilen und KI-Hinweise, keine Commit-Kürzel, Kontonamen, Git-Links oder Branch - technische Commits (Preview-Config, Merges) ausgeblendet, bekannte Automatik-Texte übersetzt, "fix"/"upload" -> "Website aktualisiert" - Ticketstatus und Arbeitsarten auf Deutsch (Open -> Eingegangen, Remote -> Online erledigt, ...), Fortschritt in 4 Schritten - Akquise-Tickets auch in den Projektdetails verborgen - /api/projects ohne Gitea-URL, Repo- und Vorlagennamen Darstellung: Auftragskarte mit Status und Fortschrittsleiste, Zeitleiste "Letzte Änderungen" nach Tagen, Arbeiten mit Farbe/Symbol je Art und Arbeitszeit; der Tab "Fortschritt" nutzt dieselben Bausteine. In der App-Ansicht öffnen sich die Projektdetails als eigene Seite mit Zurück zu "Projekte". Tests: npm test (node --test, 22 Fälle). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e54f97144c
commit
ba9916cfe6
9 files changed
+932
-138
No files matched your search
+224
-79
@@ -75,7 +75,8 @@ function fmtMinutes(min) {
|
||||
if (!n) return ''
|
||||
const h = Math.floor(n / 60)
|
||||
const m = n % 60
|
||||
return h ? `${h} h ${m} min` : `${m} min`
|
||||
if (!h) return `${m} min`
|
||||
return m ? `${h} h ${m} min` : `${h} h`
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- Projekte --- */
|
||||
@@ -162,7 +163,10 @@ function renderProjectCard(project, features) {
|
||||
<button type="button" class="kb-chip" data-view="invoices">Rechnungen</button>
|
||||
${extraChips.map((label) => `<span class="kb-chip">${escapeHtml(label)}</span>`).join('')}
|
||||
</div>
|
||||
<button type="button" class="kb-btn-outline" data-details-btn>Projektdetails anzeigen</button>
|
||||
<button type="button" class="kb-btn-outline kb-details-btn" data-details-btn aria-expanded="false">
|
||||
<span data-details-label>Projektdetails anzeigen</span>
|
||||
<svg class="kb-details-chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
|
||||
</button>
|
||||
<div class="kb-details hidden" data-details-box></div>
|
||||
</div>
|
||||
`
|
||||
@@ -170,44 +174,178 @@ function renderProjectCard(project, features) {
|
||||
const frame = li.querySelector('[data-preview-frame]')
|
||||
if (frame) setupPreviewFrame(frame)
|
||||
|
||||
// Am Desktop klappen die Details in der Karte auf; in der App-Ansicht (Handy) oeffnen sie sich
|
||||
// als eigene Seite mit Zurueck-Pfeil (openProjectPage)
|
||||
const btn = li.querySelector('[data-details-btn]')
|
||||
const label = li.querySelector('[data-details-label]')
|
||||
const box = li.querySelector('[data-details-box]')
|
||||
let loaded = false
|
||||
btn.addEventListener('click', async () => {
|
||||
const isHidden = box.classList.contains('hidden')
|
||||
box.classList.toggle('hidden', !isHidden)
|
||||
btn.textContent = isHidden ? 'Projektdetails ausblenden' : 'Projektdetails anzeigen'
|
||||
if (isHidden && !loaded) {
|
||||
btn.addEventListener('click', () => {
|
||||
if (isAppLayout()) {
|
||||
openProjectPage(project)
|
||||
return
|
||||
}
|
||||
const opening = box.classList.contains('hidden')
|
||||
box.classList.toggle('hidden', !opening)
|
||||
btn.setAttribute('aria-expanded', String(opening))
|
||||
label.textContent = opening ? 'Projektdetails ausblenden' : 'Projektdetails anzeigen'
|
||||
if (opening && !loaded) {
|
||||
loaded = true
|
||||
await loadProjectDetails(project, box)
|
||||
loadProjectDetails(project, box)
|
||||
}
|
||||
})
|
||||
|
||||
return li
|
||||
}
|
||||
|
||||
function renderCommits(git) {
|
||||
const sizeInfo = git.repo
|
||||
? `<p class="muted">Projektgröße: ${fmtBytes((git.repo.sizeKb || 0) * 1024)} · Branch: ${escapeHtml(git.repo.defaultBranch || 'main')}</p>`
|
||||
/* --------------------------------------------------------- Projektdetails --- */
|
||||
// Die Daten kommen fertig fuer Kunden aufbereitet vom Server (customerProjectView.js): keine
|
||||
// Commit-Kuerzel, Kontonamen oder KI-Hinweise, Status und Arbeitsarten auf Deutsch.
|
||||
// Dieselben Bausteine nutzt der Tab "Fortschritt" (loadCustomerActivity).
|
||||
|
||||
const PD_ICONS = {
|
||||
update: '<path d="M4 20h4L19 9l-4-4L4 16z"/><path d="M13 7l4 4"/>',
|
||||
online: '<rect x="4" y="5" width="16" height="11" rx="2"/><path d="M2 19h20"/>',
|
||||
onsite: '<path d="M12 21s7-6.2 7-11.5a7 7 0 0 0-14 0C5 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/>',
|
||||
office: '<path d="M5 21V5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v16"/><path d="M16 10h2a2 2 0 0 1 2 2v9M9 7h3M9 11h3M9 15h3M3 21h18"/>',
|
||||
work: '<path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.5 2.5-2.4-.6-.6-2.4z"/>',
|
||||
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
changes: '<path d="M3 12a9 9 0 1 0 2.6-6.4L3 8"/><path d="M3 3v5h5M12 7.5V12l3 2"/>',
|
||||
files: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
||||
}
|
||||
const ORDER_STEPS = ['Eingegangen', 'In Arbeit', 'Prüfung', 'Erledigt']
|
||||
const PD_CHANGES_SHOWN = 5
|
||||
const PD_ENTRIES_SHOWN = 4
|
||||
|
||||
function pdIcon(name, cls = 'pd-ico') {
|
||||
return `<svg class="${cls}" viewBox="0 0 24 24" aria-hidden="true">${PD_ICONS[name] || PD_ICONS.work}</svg>`
|
||||
}
|
||||
|
||||
// "2026-09-20" -> "20.09.2026" (reines Datum, ohne Zeitzonen-Umrechnung)
|
||||
function fmtIsoDay(day) {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(day || ''))
|
||||
return m ? `${m[3]}.${m[2]}.${m[1]}` : ''
|
||||
}
|
||||
|
||||
// Ueberschrift je Tag in der Zeitleiste: "Heute", "Gestern", sonst "Mittwoch, 24. September"
|
||||
function changeDayLabel(date) {
|
||||
const today = new Date()
|
||||
const dayStart = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
|
||||
const diff = Math.round((dayStart(today) - dayStart(date)) / 86400000)
|
||||
if (diff === 0) return 'Heute'
|
||||
if (diff === 1) return 'Gestern'
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
weekday: 'long', day: 'numeric', month: 'long', ...(date.getFullYear() === today.getFullYear() ? {} : { year: 'numeric' }),
|
||||
})
|
||||
}
|
||||
|
||||
// Auftrag: Nummer, Betreff, Status und Fortschrittsleiste Eingegangen -> In Arbeit -> Prüfung -> Erledigt
|
||||
function renderOrder(order) {
|
||||
const { status } = order
|
||||
const steps = status.step > 0
|
||||
? `<ol class="pd-steps" aria-label="Fortschritt des Auftrags">${ORDER_STEPS.map((label, i) => {
|
||||
const n = i + 1
|
||||
const state = n < status.step ? 'is-done' : n === status.step ? 'is-current' : ''
|
||||
return `<li class="${state}"${n === status.step ? ' aria-current="step"' : ''}>${label}</li>`
|
||||
}).join('')}</ol>`
|
||||
: ''
|
||||
if (!git.commits || !git.commits.length) {
|
||||
return `${sizeInfo}<p class="muted">Noch keine Änderungen vorhanden.</p>`
|
||||
}
|
||||
return sizeInfo + git.commits.map((c) => {
|
||||
const [title, ...body] = String(c.message || '').trim().split('\n')
|
||||
const bodyText = body.map((l) => l.trim()).filter(Boolean).join('\n')
|
||||
return `
|
||||
<div class="commit-item">
|
||||
<strong>${escapeHtml(title)}</strong>
|
||||
${bodyText ? `<div class="commit-body">${escapeHtml(bodyText)}</div>` : ''}
|
||||
<div class="commit-meta">${escapeHtml(c.shortSha)} · ${escapeHtml(c.authorName)} · ${fmtDateTime(c.date)}</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
const hint = status.tone === 'waiting'
|
||||
? '<p class="pd-order-hint">Wir brauchen noch eine Info von dir. <button type="button" class="kb-link" data-view="chat">Im Chat antworten</button></p>'
|
||||
: ''
|
||||
return `
|
||||
<div class="pd-order-head">
|
||||
<div class="pd-order-name">
|
||||
${order.number ? `<span class="pd-order-no">Auftrag ${escapeHtml(order.number)}</span>` : ''}
|
||||
<h3>${escapeHtml(order.topic || 'Dein Auftrag')}</h3>
|
||||
</div>
|
||||
<span class="pd-status">${escapeHtml(status.label)}</span>
|
||||
</div>
|
||||
${steps}${hint}`
|
||||
}
|
||||
|
||||
// Lange Listen: nur die ersten Eintraege zeigen, "3 weitere … anzeigen" klappt den Rest auf (initPdToggles)
|
||||
function pdMoreButton(total, shown, [one, many]) {
|
||||
const rest = total - shown
|
||||
if (rest <= 0) return ''
|
||||
const text = `${rest} ${rest === 1 ? one : many} anzeigen`
|
||||
return `<button type="button" class="pd-more" data-pd-more data-more-label="${escapeAttr(text)}">${escapeHtml(text)}</button>`
|
||||
}
|
||||
|
||||
function renderChange(c, extra) {
|
||||
const d = new Date(c.date)
|
||||
const time = Number.isNaN(d.getTime()) ? '' : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
|
||||
return `
|
||||
<li class="pd-change"${extra ? ' data-extra' : ''}>
|
||||
<div class="pd-change-head">
|
||||
<strong>${escapeHtml(c.title)}</strong>
|
||||
${time ? `<time datetime="${escapeAttr(c.date)}">${time}</time>` : ''}
|
||||
</div>
|
||||
${c.details?.length ? `<ul class="pd-change-details">${c.details.map((t) => `<li>${escapeHtml(t)}</li>`).join('')}</ul>` : ''}
|
||||
</li>`
|
||||
}
|
||||
|
||||
// Zeitleiste der letzten Aenderungen, nach Tagen gruppiert
|
||||
function renderChanges(changes) {
|
||||
if (!changes.length) return '<p class="pd-empty">Noch keine Änderungen an der Website.</p>'
|
||||
const groups = []
|
||||
changes.forEach((c, i) => {
|
||||
const d = new Date(c.date)
|
||||
const day = Number.isNaN(d.getTime()) ? '' : changeDayLabel(d)
|
||||
if (!groups.length || groups[groups.length - 1].day !== day) groups.push({ day, first: i, items: [] })
|
||||
groups[groups.length - 1].items.push(renderChange(c, i >= PD_CHANGES_SHOWN))
|
||||
})
|
||||
return `
|
||||
<div class="pd-collapsible">
|
||||
<div class="pd-timeline">
|
||||
${groups.map((g) => `
|
||||
<div class="pd-day"${g.first >= PD_CHANGES_SHOWN ? ' data-extra' : ''}>
|
||||
${g.day ? `<p class="pd-day-label">${escapeHtml(g.day)}</p>` : ''}
|
||||
<ul class="pd-changes">${g.items.join('')}</ul>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
${pdMoreButton(changes.length, PD_CHANGES_SHOWN, ['weitere Änderung', 'weitere Änderungen'])}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function renderWorkEntry(e, extra) {
|
||||
const meta = [fmtIsoDay(e.date), e.person].filter(Boolean).map(escapeHtml).join(' · ')
|
||||
const body = e.changes?.length
|
||||
? `<ul class="pd-entry-changes">${e.changes.map((t) => `<li>${escapeHtml(t)}</li>`).join('')}</ul>`
|
||||
: e.text ? `<p class="pd-entry-text">${escapeHtml(e.text)}</p>` : ''
|
||||
return `
|
||||
<li class="pd-entry pd-kind-${escapeAttr(e.kind)}"${extra ? ' data-extra' : ''}>
|
||||
<span class="pd-entry-ico">${pdIcon(e.kind)}</span>
|
||||
<div class="pd-entry-main">
|
||||
<div class="pd-entry-head">
|
||||
<strong>${escapeHtml(e.label)}</strong>
|
||||
${meta ? `<span class="pd-entry-meta">${meta}</span>` : ''}
|
||||
</div>
|
||||
${body}
|
||||
</div>
|
||||
${e.minutes ? `<span class="pd-time" title="Arbeitszeit">${pdIcon('clock', 'pd-time-ico')}${fmtMinutes(e.minutes)}</span>` : ''}
|
||||
</li>`
|
||||
}
|
||||
|
||||
function renderWorkEntries(entries, emptyText) {
|
||||
if (!entries.length) return `<p class="pd-empty">${emptyText}</p>`
|
||||
return `
|
||||
<div class="pd-collapsible">
|
||||
<ul class="pd-work">${entries.map((e, i) => renderWorkEntry(e, i >= PD_ENTRIES_SHOWN)).join('')}</ul>
|
||||
${pdMoreButton(entries.length, PD_ENTRIES_SHOWN, ['weiteren Eintrag', 'weitere Einträge'])}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function initPdToggles(root) {
|
||||
root.querySelectorAll('[data-pd-more]').forEach((btn) => btn.addEventListener('click', () => {
|
||||
const wrap = btn.closest('.pd-collapsible')
|
||||
const open = wrap.classList.toggle('is-open')
|
||||
btn.textContent = open ? 'Weniger anzeigen' : btn.dataset.moreLabel
|
||||
}))
|
||||
}
|
||||
|
||||
function renderFileOverview(files) {
|
||||
if (!files || !files.totalFiles) {
|
||||
return '<p class="muted">Keine Datei-Informationen verfügbar.</p>'
|
||||
return '<p class="pd-empty">Keine Datei-Informationen verfügbar.</p>'
|
||||
}
|
||||
const rows = (files.topLevel || []).map((entry) => `
|
||||
<tr>
|
||||
@@ -216,50 +354,53 @@ function renderFileOverview(files) {
|
||||
<td>${fmtBytes(entry.sizeBytes)}</td>
|
||||
</tr>`).join('')
|
||||
return `
|
||||
<p class="muted">${files.totalFiles} Dateien · ${fmtBytes(files.totalSizeBytes)}${files.truncated ? ' (Übersicht unvollständig – sehr großes Projekt)' : ''}</p>
|
||||
<table class="simple-table">
|
||||
<p class="pd-note">${files.totalFiles} Dateien · ${fmtBytes(files.totalSizeBytes)}${files.truncated ? ' (Übersicht unvollständig – sehr großes Projekt)' : ''}</p>
|
||||
<div class="kb-table-wrap"><table class="simple-table pd-files">
|
||||
<thead><tr><th>Ordner / Datei</th><th>Dateien</th><th>Größe</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`
|
||||
</table></div>`
|
||||
}
|
||||
|
||||
function renderWorkEntries(work) {
|
||||
if (!work.ticket) {
|
||||
return '<p class="muted">Für dieses Projekt gibt es noch kein Ticket.</p>'
|
||||
}
|
||||
const head = `<p class="muted">Ticket #${escapeHtml(work.ticket.woid)} „${escapeHtml(work.ticket.topic)}" · Status: ${escapeHtml(work.ticket.status)}</p>`
|
||||
if (!work.worksheets.length) {
|
||||
return `${head}<p class="muted">Noch keine dokumentierten Arbeiten.</p>`
|
||||
}
|
||||
return head + work.worksheets.map((ws) => `
|
||||
<div class="activity-item">
|
||||
<div>
|
||||
${ws.isGit ? '<span class="pill pill-git">Git-Push</span>' : `<span class="pill pill-muted">${escapeHtml(ws.serviceType || 'Arbeit')}</span>`}
|
||||
<strong> ${escapeHtml(ws.employeeName)}</strong>
|
||||
<span class="muted"> · ${fmtDateShort(ws.date)}</span>
|
||||
${ws.totalTime ? `<span class="pill pill-ok">${fmtMinutes(ws.totalTime)}</span>` : ''}
|
||||
${ws.pending ? '<span class="pill pill-muted">Zeiterfassung folgt</span>' : ''}
|
||||
</div>
|
||||
<div class="activity-ws">${escapeHtml(ws.details)}</div>
|
||||
</div>`).join('')
|
||||
function pdSection(icon, title, body) {
|
||||
return `<section class="pd-section"><h3 class="pd-title">${pdIcon(icon)}${title}</h3>${body}</section>`
|
||||
}
|
||||
|
||||
async function loadProjectDetails(project, box) {
|
||||
box.innerHTML = '<p class="muted">Lade Details…</p>'
|
||||
// stillWanted: die Handy-Seite teilt sich einen Container fuer alle Projekte; eine spaete Antwort fuer
|
||||
// ein inzwischen verlassenes Projekt darf die Seite des naechsten nicht ueberschreiben
|
||||
async function loadProjectDetails(project, box, { stillWanted = () => true } = {}) {
|
||||
box.innerHTML = '<p class="pd-empty">Details werden geladen…</p>'
|
||||
const [git, files, work] = await Promise.all([
|
||||
api(`/api/projects/${encodeURIComponent(project.id)}/git`).catch(() => null),
|
||||
api(`/api/projects/${encodeURIComponent(project.id)}/files`).catch(() => null),
|
||||
api(`/api/projects/${encodeURIComponent(project.id)}/work`).catch(() => null),
|
||||
])
|
||||
if (!stillWanted()) return
|
||||
const order = work?.order
|
||||
|
||||
box.innerHTML = `
|
||||
<h3>Letzte Änderungen</h3>
|
||||
${git ? renderCommits(git) : '<p class="muted">Git-Daten derzeit nicht verfügbar.</p>'}
|
||||
<h3>Datei-Übersicht</h3>
|
||||
${files ? renderFileOverview(files) : '<p class="muted">Datei-Übersicht derzeit nicht verfügbar.</p>'}
|
||||
<h3>Arbeiten am Projekt</h3>
|
||||
${work ? renderWorkEntries(work) : '<p class="muted">Keine Ticket-Daten verfügbar.</p>'}
|
||||
`
|
||||
${order ? `<section class="pd-order pd-tone-${escapeAttr(order.status.tone)}">${renderOrder(order)}</section>` : ''}
|
||||
${pdSection('changes', 'Letzte Änderungen', git
|
||||
? renderChanges(git.changes || [])
|
||||
: '<p class="pd-empty">Die Änderungen können gerade nicht geladen werden.</p>')}
|
||||
${order || !work ? pdSection('work', 'Arbeiten am Projekt', work
|
||||
? renderWorkEntries(work.entries || [], 'Noch keine Arbeitszeit eingetragen. Was sich an der Website geändert hat, steht unter „Letzte Änderungen“.')
|
||||
: '<p class="pd-empty">Die Arbeiten können gerade nicht geladen werden.</p>') : ''}
|
||||
${pdSection('files', 'Dateien', files ? renderFileOverview(files) : '<p class="pd-empty">Die Datei-Übersicht kann gerade nicht geladen werden.</p>')}`
|
||||
initPdToggles(box)
|
||||
}
|
||||
|
||||
// App-Ansicht: Projektdetails als Unterseite von "Projekte" (Zurueck-Pfeil, eigener Verlaufseintrag).
|
||||
// Dasselbe Projekt erneut oeffnen zeigt die schon geladenen Details.
|
||||
const projectPage = { project: null }
|
||||
|
||||
function openProjectPage(project) {
|
||||
const view = document.getElementById('view-projekt')
|
||||
if (!view) return
|
||||
const reload = projectPage.project !== project
|
||||
projectPage.project = project
|
||||
view.querySelector('.kb-head h1').textContent = project.projectName || project.subdomain || 'Projekt'
|
||||
showView('projekt')
|
||||
if (reload) loadProjectDetails(project, view.querySelector('.kb-details'), { stillWanted: () => projectPage.project === project })
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- Login-Redirects --- */
|
||||
@@ -479,6 +620,8 @@ async function loadCustomerInvoices() {
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- Aktivität --- */
|
||||
// Tab "Fortschritt": je Auftrag eine Karte mit Status, Fortschrittsleiste und Arbeiten
|
||||
// (Bausteine der Projektdetails; Texte kommen aufbereitet vom Server)
|
||||
async function loadCustomerActivity() {
|
||||
const body = document.getElementById('activity-body')
|
||||
if (!body) return
|
||||
@@ -489,23 +632,11 @@ async function loadCustomerActivity() {
|
||||
return
|
||||
}
|
||||
body.innerHTML = tickets.map((t) => `
|
||||
<div class="activity-item">
|
||||
<div>
|
||||
<strong>${escapeHtml(t.woid ? `#${t.woid} ` : '')}${escapeHtml(t.topic || '–')}</strong>
|
||||
<span class="activity-status">${escapeHtml(t.status || '–')}</span>
|
||||
<span class="muted"> · ${fmtDateShort(t.createdAt)}</span>
|
||||
</div>
|
||||
${(t.worksheets || []).map((w) => `
|
||||
<div style="margin: 6px 0 0 14px;">
|
||||
${w.isGit ? '<span class="pill pill-git">Git-Push</span>' : `<span class="pill pill-muted">${escapeHtml(w.serviceType || 'Arbeit')}</span>`}
|
||||
${w.employeeName ? ` <strong>${escapeHtml(w.employeeName)}</strong>` : ''}
|
||||
<span class="muted"> · ${fmtDateShort(w.date)}</span>
|
||||
${Number(w.totalTime) ? `<span class="pill pill-ok">${fmtMinutes(w.totalTime)}</span>` : ''}
|
||||
${w.pending ? '<span class="pill pill-muted">Zeiterfassung folgt</span>' : ''}
|
||||
${w.details ? `<div class="activity-ws">${escapeHtml(w.details)}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`).join('')
|
||||
<article class="pd-order pd-order-card pd-tone-${escapeAttr(t.status.tone)}">
|
||||
${renderOrder(t)}
|
||||
${renderWorkEntries(t.entries || [], 'Noch keine Arbeiten dokumentiert.')}
|
||||
</article>`).join('')
|
||||
initPdToggles(body)
|
||||
} catch (err) {
|
||||
body.innerHTML = `<p class="muted">Aktivität konnte nicht geladen werden: ${escapeHtml(err.message)}</p>`
|
||||
}
|
||||
@@ -1143,7 +1274,8 @@ async function loadTakeoverCard({ abo, notice = '' } = {}) {
|
||||
/* ---------------------------------------------------------- Navigation --- */
|
||||
// Ansichten im Inhaltsbereich; Hash fuer Direktlinks (z. B. aus Mails: #abo, #gutschrift).
|
||||
// "wartung" gibt es nur in der App-Ansicht (Tab), am Desktop zeigt sie das Wartungs-Abo.
|
||||
const VIEW_HASHES = { projects: '', activity: 'fortschritt', invoices: 'rechnungen', chat: 'chat', wartung: 'wartung', abo: 'abo', takeover: 'gutschrift' }
|
||||
// "projekt" (Projektdetails als eigene Seite) gibt es ebenfalls nur in der App-Ansicht.
|
||||
const VIEW_HASHES = { projects: '', activity: 'fortschritt', invoices: 'rechnungen', chat: 'chat', wartung: 'wartung', abo: 'abo', takeover: 'gutschrift', projekt: 'projekt' }
|
||||
const viewLoaders = { activity: loadCustomerActivity, chat: initChat }
|
||||
const loadedViews = new Set()
|
||||
|
||||
@@ -1291,8 +1423,9 @@ async function initDashboardPage() {
|
||||
// Gleiche Bedingung wie der Abschnitt "App-Ansicht" in kundenbereich.css (auch Handys quer)
|
||||
const appLayout = window.matchMedia('(max-width: 880px), (pointer: coarse) and (max-height: 500px)')
|
||||
const coarsePointer = window.matchMedia('(pointer: coarse)')
|
||||
// Unterseite -> Tab, zu dem sie gehoert
|
||||
const APP_PARENT = { abo: 'wartung', takeover: 'wartung' }
|
||||
// Unterseite -> Tab, zu dem sie gehoert (und Text neben dem Zurueck-Pfeil)
|
||||
const APP_PARENT = { abo: 'wartung', takeover: 'wartung', projekt: 'projects' }
|
||||
const APP_PARENT_LABEL = { wartung: 'Wartung', projects: 'Projekte' }
|
||||
const appShell = { view: 'projects', scroll: {}, fromHistory: false, sideCards: [], installPrompt: null }
|
||||
const TEXT_INPUT = 'textarea, input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=date]):not([type=button]):not([type=submit]):not([type=range]):not([type=color])'
|
||||
|
||||
@@ -1339,7 +1472,13 @@ function updateBarState() {
|
||||
|
||||
function updateAppBar() {
|
||||
const sub = isAppLayout() && Boolean(APP_PARENT[appShell.view])
|
||||
document.getElementById('kb-back')?.classList.toggle('hidden', !sub)
|
||||
const back = document.getElementById('kb-back')
|
||||
back?.classList.toggle('hidden', !sub)
|
||||
if (back && sub) {
|
||||
const label = APP_PARENT_LABEL[APP_PARENT[appShell.view]] || 'Zurück'
|
||||
back.querySelector('span').textContent = label
|
||||
back.setAttribute('aria-label', `Zurück zu ${label}`)
|
||||
}
|
||||
document.body.classList.toggle('kb-subpage', sub)
|
||||
const title = document.getElementById('kb-app-title')
|
||||
if (title) title.textContent = currentViewTitle()?.textContent.trim() || ''
|
||||
@@ -1352,6 +1491,11 @@ function onAppViewShown(name) {
|
||||
showView('abo', { updateHash: !appShell.fromHistory })
|
||||
return
|
||||
}
|
||||
// Projektdetails als Seite nur in der App-Ansicht und nur mit gewaehltem Projekt (nicht per Direktlink)
|
||||
if (name === 'projekt' && (!isAppLayout() || !projectPage.project)) {
|
||||
showView('projects', { updateHash: !appShell.fromHistory })
|
||||
return
|
||||
}
|
||||
const prev = appShell.view
|
||||
const parent = APP_PARENT[name]
|
||||
const nav = name === prev ? 'same' : parent === prev ? 'push' : APP_PARENT[prev] === name ? 'back' : 'tab'
|
||||
@@ -1368,9 +1512,9 @@ function onAppViewShown(name) {
|
||||
})
|
||||
|
||||
if (isAppLayout()) {
|
||||
// Unterseite mit eigenem Verlaufseintrag: "Zurück" (auch die Android-Geste) führt zur Wartung
|
||||
// Unterseite mit eigenem Verlaufseintrag: "Zurück" (auch die Android-Geste) führt zum Tab darüber (Wartung bzw. Projekte)
|
||||
if (nav === 'push' && !appShell.fromHistory) {
|
||||
window.history.replaceState(null, '', `#${VIEW_HASHES[parent]}`)
|
||||
window.history.replaceState(null, '', VIEW_HASHES[parent] ? `#${VIEW_HASHES[parent]}` : window.location.pathname)
|
||||
window.history.pushState({ kbSub: name }, '', `#${VIEW_HASHES[name]}`)
|
||||
}
|
||||
// Tabs merken sich ihre Scrollposition, Unterseiten beginnen oben, erneutes Tippen scrollt nach oben
|
||||
@@ -1671,6 +1815,7 @@ function initAppShell() {
|
||||
onMediaChange(appLayout, () => {
|
||||
placeSideCards()
|
||||
if (!isAppLayout() && appShell.view === 'wartung') showView('abo')
|
||||
if (!isAppLayout() && appShell.view === 'projekt') showView('projects')
|
||||
updateAppBar()
|
||||
renderInstallOffers()
|
||||
})
|
||||
|
||||
+11
-1
@@ -30,7 +30,7 @@
|
||||
<div class="kb-glow" aria-hidden="true"></div>
|
||||
|
||||
<header class="kb-header">
|
||||
<!-- App-Ansicht: Zurueck aus Unterseiten der Wartung, Seitentitel beim Scrollen (initAppShell) -->
|
||||
<!-- App-Ansicht: Zurueck aus Unterseiten (Wartung, Projektdetails; Text setzt updateAppBar), Seitentitel beim Scrollen (initAppShell) -->
|
||||
<button type="button" class="kb-back hidden" id="kb-back" aria-label="Zurück zu Wartung">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>
|
||||
<span>Wartung</span>
|
||||
@@ -137,6 +137,16 @@
|
||||
<div id="empty" class="kb-panel kb-empty hidden">Noch keine Website-Projekte zugewiesen.</div>
|
||||
</section>
|
||||
|
||||
<!-- App-Ansicht: Projektdetails als eigene Seite mit Zurueck zu "Projekte" (app.js, openProjectPage);
|
||||
am Desktop klappen die Details in der Projektkarte auf -->
|
||||
<section class="kb-view" id="view-projekt">
|
||||
<header class="kb-head">
|
||||
<h1>Projekt</h1>
|
||||
<p>Stand des Auftrags, letzte Änderungen und Dateien.</p>
|
||||
</header>
|
||||
<div class="kb-details pd-page"></div>
|
||||
</section>
|
||||
|
||||
<!-- Fortschritt -->
|
||||
<section class="kb-view" id="view-activity">
|
||||
<header class="kb-head">
|
||||
|
||||
+128
-27
@@ -235,15 +235,118 @@
|
||||
background: rgba(157, 61, 254, 0.14); border: 1px solid rgba(157, 61, 254, 0.4); cursor: pointer;
|
||||
}
|
||||
.kb-chip:hover { background: rgba(157, 61, 254, 0.24); }
|
||||
.kb-details { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--kb-border); }
|
||||
.kb-details h3 { margin: 16px 0 8px; font-size: 14px; letter-spacing: 0.02em; }
|
||||
.kb-details h3:first-child { margin-top: 0; }
|
||||
.commit-item { padding: 7px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); font-size: 14px; }
|
||||
.commit-item:last-child { border-bottom: none; }
|
||||
.commit-meta { font-size: 12px; color: var(--kb-faint); }
|
||||
.commit-body { margin: 2px 0 0 8px; font-size: 13px; color: var(--kb-muted); white-space: pre-wrap; }
|
||||
.kb-details-chev {
|
||||
width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round;
|
||||
transform: rotate(90deg); transition: transform 0.2s;
|
||||
}
|
||||
.kb-details-btn[aria-expanded='true'] .kb-details-chev { transform: rotate(-90deg); }
|
||||
.kb-details { display: grid; gap: 24px; margin-top: 18px; padding-top: 20px; border-top: 1px solid var(--kb-border); }
|
||||
|
||||
/* ------------------------------------------ Tabellen, Pills, Aktivitaet --- */
|
||||
/* --------------------------------------------------------- Projektdetails --- */
|
||||
/* Auftrag mit Fortschrittsleiste, Zeitleiste der Aenderungen, Arbeiten mit Symbol je Art.
|
||||
Dieselben Bausteine nutzt der Tab "Fortschritt" (app.js: renderOrder, renderWorkEntries). */
|
||||
.pd-tone-new { --pd-c: #d8b4fe; --pd-bg: rgba(157, 61, 254, 0.1); --pd-line: rgba(157, 61, 254, 0.4); --pd-bar: #b77bff; }
|
||||
.pd-tone-active { --pd-c: #e9d5ff; --pd-bg: rgba(157, 61, 254, 0.2); --pd-line: rgba(157, 61, 254, 0.58); --pd-bar: var(--kb-purple); }
|
||||
.pd-tone-waiting { --pd-c: #fcd34d; --pd-bg: rgba(251, 191, 36, 0.12); --pd-line: rgba(251, 191, 36, 0.45); --pd-bar: #f5b82e; }
|
||||
.pd-tone-review { --pd-c: #93c5fd; --pd-bg: rgba(59, 130, 246, 0.14); --pd-line: rgba(96, 165, 250, 0.45); --pd-bar: #4f8ff7; }
|
||||
.pd-tone-done { --pd-c: #86efac; --pd-bg: rgba(74, 222, 128, 0.09); --pd-line: rgba(74, 222, 128, 0.38); --pd-bar: #34c46a; }
|
||||
.pd-tone-paused, .pd-tone-stopped { --pd-c: var(--kb-muted); --pd-bg: rgba(255, 255, 255, 0.05); --pd-line: var(--kb-border-strong); --pd-bar: rgba(255, 255, 255, 0.3); }
|
||||
|
||||
.pd-order {
|
||||
padding: 18px 20px; border-radius: 16px; border: 1px solid var(--pd-line);
|
||||
background: linear-gradient(135deg, var(--pd-bg), rgba(255, 255, 255, 0.015) 72%), rgba(12, 12, 14, 0.6);
|
||||
}
|
||||
.pd-order-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px 14px; flex-wrap: wrap; }
|
||||
.pd-order-name { min-width: 0; }
|
||||
.pd-order-no { display: block; margin-bottom: 3px; font-size: 12px; font-weight: 600; letter-spacing: 0.04em; color: var(--kb-muted); }
|
||||
.pd-order-name h3 { margin: 0; font-size: 18px; line-height: 1.3; overflow-wrap: anywhere; }
|
||||
.pd-status {
|
||||
flex: none; display: inline-flex; align-items: center; gap: 7px; padding: 4px 11px; border-radius: 999px;
|
||||
font-size: 12.5px; font-weight: 700; color: var(--pd-c); background: var(--pd-bg); border: 1px solid var(--pd-line);
|
||||
}
|
||||
.pd-status::before { content: ''; flex: none; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
||||
.pd-tone-active .pd-status::before, .pd-tone-waiting .pd-status::before, .pd-tone-review .pd-status::before { animation: pd-pulse 1.8s ease-in-out infinite; }
|
||||
@keyframes pd-pulse { 50% { opacity: 0.3; } }
|
||||
.pd-steps { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; margin: 18px 0 0; padding: 0; list-style: none; }
|
||||
.pd-steps li { position: relative; min-width: 0; padding-top: 13px; font-size: 12px; color: var(--kb-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.pd-steps li::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 6px; border-radius: 3px; background: rgba(255, 255, 255, 0.1); }
|
||||
.pd-steps li.is-done { color: var(--kb-muted); }
|
||||
.pd-steps li.is-done::before, .pd-steps li.is-current::before { background: var(--pd-bar); }
|
||||
.pd-steps li.is-current { color: #fff; font-weight: 700; }
|
||||
.pd-order-hint { margin: 14px 0 0; font-size: 13.5px; color: var(--pd-c); }
|
||||
|
||||
.pd-section { min-width: 0; }
|
||||
.pd-title {
|
||||
display: flex; align-items: center; gap: 9px; margin: 0 0 14px; font-size: 12px; font-weight: 700;
|
||||
letter-spacing: 0.12em; text-transform: uppercase; color: var(--kb-muted);
|
||||
}
|
||||
.pd-ico { flex: none; width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.pd-title .pd-ico { color: var(--kb-lilac); }
|
||||
.pd-empty { margin: 0; font-size: 14px; color: var(--kb-muted); }
|
||||
.pd-note { margin: 0 0 8px; font-size: 13px; color: var(--kb-muted); }
|
||||
|
||||
/* Zeitleiste "Letzte Änderungen": Tage als Ueberschrift, jede Aenderung ein Punkt auf der Linie */
|
||||
.pd-timeline { position: relative; padding-left: 22px; }
|
||||
.pd-timeline::before {
|
||||
content: ''; position: absolute; left: 5px; top: 8px; bottom: 8px; width: 2px; border-radius: 1px;
|
||||
background: linear-gradient(var(--kb-purple-line), rgba(157, 61, 254, 0.06));
|
||||
}
|
||||
.pd-day + .pd-day { margin-top: 16px; }
|
||||
.pd-day-label { margin: 0 0 8px; font-size: 12px; font-weight: 700; letter-spacing: 0.02em; color: var(--kb-lilac); }
|
||||
.pd-changes { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
|
||||
.pd-change { position: relative; }
|
||||
.pd-change::before {
|
||||
content: ''; position: absolute; left: -21px; top: 6px; width: 8px; height: 8px; border-radius: 50%;
|
||||
background: #0c0c0e; border: 2px solid var(--kb-purple);
|
||||
}
|
||||
.pd-day:first-child .pd-change:first-child::before { background: var(--kb-purple); box-shadow: 0 0 0 4px rgba(157, 61, 254, 0.22); }
|
||||
.pd-change-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
||||
.pd-change-head strong { font-size: 14.5px; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.pd-change-head time { flex: none; font-size: 12px; color: var(--kb-faint); font-variant-numeric: tabular-nums; }
|
||||
.pd-change-details { margin: 4px 0 0; padding: 0 0 0 17px; font-size: 13px; line-height: 1.5; color: var(--kb-muted); }
|
||||
.pd-change-details li + li { margin-top: 3px; }
|
||||
.pd-change-details li::marker { color: var(--kb-faint); }
|
||||
|
||||
/* Lange Listen gekuerzt; "Weitere … anzeigen" klappt auf */
|
||||
.pd-collapsible:not(.is-open) [data-extra] { display: none; }
|
||||
.pd-more {
|
||||
margin-top: 14px; padding: 8px 15px; border-radius: 999px; border: 1px solid var(--kb-border-strong); background: transparent;
|
||||
color: #d4b4ff; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.pd-more:hover { background: rgba(157, 61, 254, 0.12); }
|
||||
|
||||
/* Arbeiten: Farbe und Symbol je Art (Website-Update, online, vor Ort, Buero) */
|
||||
.pd-kind-update { --pd-k: #d8b4fe; --pd-kbg: rgba(157, 61, 254, 0.18); }
|
||||
.pd-kind-online { --pd-k: #93c5fd; --pd-kbg: rgba(59, 130, 246, 0.17); }
|
||||
.pd-kind-onsite { --pd-k: #5eead4; --pd-kbg: rgba(45, 212, 191, 0.15); }
|
||||
.pd-kind-office { --pd-k: #fcd34d; --pd-kbg: rgba(251, 191, 36, 0.15); }
|
||||
.pd-kind-work { --pd-k: rgba(255, 255, 255, 0.78); --pd-kbg: rgba(255, 255, 255, 0.08); }
|
||||
.pd-work { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
|
||||
.pd-entry {
|
||||
display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 12px; align-items: start;
|
||||
padding: 12px 14px; border-radius: 14px; background: rgba(255, 255, 255, 0.025); border: 1px solid var(--kb-border);
|
||||
}
|
||||
.pd-entry-ico { width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; color: var(--pd-k); background: var(--pd-kbg); }
|
||||
.pd-entry-ico .pd-ico { width: 19px; height: 19px; }
|
||||
.pd-entry-main { min-width: 0; }
|
||||
.pd-entry-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-height: 22px; }
|
||||
.pd-entry-head strong { font-size: 14.5px; color: var(--pd-k); }
|
||||
.pd-entry-meta { font-size: 12.5px; color: var(--kb-faint); }
|
||||
.pd-entry-text { margin: 4px 0 0; font-size: 13.5px; line-height: 1.5; color: rgba(255, 255, 255, 0.82); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.pd-entry-changes { margin: 4px 0 0; padding-left: 17px; font-size: 13.5px; line-height: 1.5; color: rgba(255, 255, 255, 0.82); }
|
||||
.pd-entry-changes li::marker { color: var(--pd-k); }
|
||||
.pd-time {
|
||||
display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 999px; white-space: nowrap;
|
||||
font-size: 12px; font-weight: 700; background: rgba(255, 255, 255, 0.07); border: 1px solid var(--kb-border-strong);
|
||||
}
|
||||
.pd-time-ico { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; opacity: 0.75; }
|
||||
|
||||
/* Tab "Fortschritt": je Auftrag eine Karte in der Farbe seines Status */
|
||||
#activity-body:has(.pd-order-card) { display: grid; gap: 16px; padding: 0; background: none; border: none; }
|
||||
.pd-order-card > .pd-collapsible, .pd-order-card > .pd-empty { margin-top: 18px; }
|
||||
.pd-order-card .pd-entry { background: rgba(0, 0, 0, 0.28); }
|
||||
|
||||
/* ------------------------------------------------- Tabellen und Pills --- */
|
||||
.simple-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
.simple-table th, .simple-table td { padding: 10px 10px; text-align: left; border-bottom: 1px solid var(--kb-border); }
|
||||
.simple-table th { font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--kb-faint); }
|
||||
@@ -256,13 +359,7 @@
|
||||
.pill-warn { color: #d8b4fe; background: rgba(157, 61, 254, 0.16); border-color: rgba(157, 61, 254, 0.5); }
|
||||
.pill-red { color: #fca5a5; background: rgba(248, 113, 113, 0.12); border-color: rgba(248, 113, 113, 0.45); }
|
||||
.pill-muted { color: var(--kb-muted); background: rgba(255, 255, 255, 0.06); border-color: var(--kb-border-strong); }
|
||||
.pill-git { color: #fdba74; background: rgba(251, 146, 60, 0.12); border-color: rgba(251, 146, 60, 0.45); }
|
||||
.pill-amber { color: #fcd34d; background: rgba(251, 191, 36, 0.12); border-color: rgba(251, 191, 36, 0.45); }
|
||||
.activity-item { padding: 14px 0; border-bottom: 1px solid var(--kb-border); }
|
||||
.activity-item:first-child { padding-top: 0; }
|
||||
.activity-item:last-child { border-bottom: none; padding-bottom: 0; }
|
||||
.activity-status { display: inline-block; margin-left: 6px; padding: 1px 9px; border-radius: 999px; font-size: 12px; background: rgba(255, 255, 255, 0.06); border: 1px solid var(--kb-border-strong); }
|
||||
.activity-ws { margin: 4px 0 4px 14px; font-size: 13px; color: rgba(255, 255, 255, 0.78); white-space: pre-wrap; }
|
||||
|
||||
/* ------------------------------------------------------------------- Chat --- */
|
||||
.chat-box { display: flex; flex-direction: column; height: 520px; }
|
||||
@@ -554,18 +651,22 @@ button.kb-avatar:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
||||
.kb-project-top h2 { font-size: 19px; }
|
||||
.kb-open { right: 10px; bottom: 10px; padding: 7px 12px; font-size: 12.5px; }
|
||||
.kb-chip { padding: 7px 12px; font-size: 13px; }
|
||||
.kb-project [data-details-btn] { width: 100%; justify-content: center; padding: 12px 16px; font-size: 14px; }
|
||||
/* Projektdetails: der Knopf oeffnet eine eigene Seite (Pfeil nach rechts wie eine Listenzeile) */
|
||||
.kb-project [data-details-btn] { width: 100%; justify-content: space-between; padding: 14px 16px 14px 18px; font-size: 15px; }
|
||||
.kb-project .kb-details-chev { transform: none; width: 18px; height: 18px; opacity: 0.7; }
|
||||
.kb-details .simple-table { font-size: 13px; }
|
||||
|
||||
/* Fortschritt als Zeitleiste */
|
||||
#activity-body .activity-item { position: relative; padding: 0 0 18px 22px; border-bottom: none; }
|
||||
#activity-body .activity-item::before {
|
||||
content: ''; position: absolute; left: 0; top: 7px; width: 9px; height: 9px; border-radius: 50%;
|
||||
background: var(--kb-purple); box-shadow: 0 0 0 3px rgba(157, 61, 254, 0.22);
|
||||
}
|
||||
#activity-body .activity-item::after { content: ''; position: absolute; left: 4px; top: 22px; bottom: 2px; width: 1px; background: var(--kb-border-strong); }
|
||||
#activity-body .activity-item:last-child { padding-bottom: 0; }
|
||||
#activity-body .activity-item:last-child::after { display: none; }
|
||||
.kb-details.pd-page { margin: 0; padding: 0; border-top: none; gap: 26px; }
|
||||
.pd-order { padding: 16px; border-radius: 18px; }
|
||||
.pd-order-name h3 { font-size: 17px; }
|
||||
.pd-steps { gap: 5px; }
|
||||
.pd-steps li { font-size: 11.5px; letter-spacing: -0.01em; }
|
||||
.pd-change-head strong { font-size: 15px; }
|
||||
.pd-change-details { font-size: 13.5px; }
|
||||
.pd-entry { grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; padding: 12px; border-radius: 16px; }
|
||||
.pd-entry .pd-time { grid-column: 2; justify-self: start; margin-top: 6px; }
|
||||
.pd-entry-text, .pd-entry-changes { font-size: 14px; }
|
||||
.pd-more { width: 100%; padding: 12px 14px; font-size: 14px; }
|
||||
.pd-order-hint .kb-link { display: inline-block; padding: 6px 0; }
|
||||
|
||||
/* Rechnungen: jede Zeile als Karte */
|
||||
#invoices-body:has(.kb-invoices) { padding: 0; background: none; border: none; }
|
||||
@@ -689,6 +790,6 @@ button.kb-avatar:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kb-shell.dashboard-content-reveal, .kb-view.active, .kb-sheet[open] .kb-sheet-panel, #loading:not(.hidden) { animation: none !important; }
|
||||
.kb-header, .kb-nav, .kb-logo, .kb-app-title, .kb-sheet-panel { transition: none !important; }
|
||||
.kb-shell.dashboard-content-reveal, .kb-view.active, .kb-sheet[open] .kb-sheet-panel, #loading:not(.hidden), .pd-status::before { animation: none !important; }
|
||||
.kb-header, .kb-nav, .kb-logo, .kb-app-title, .kb-sheet-panel, .kb-details-chev { transition: none !important; }
|
||||
}
|
||||
Reference in new issue
Block a user