Planer: Meine-Spells-View + Lila Personal-Farbe + Sidebar-Layout + Gantt-Scrollbar
- js/planner.js:
- Mechaniken-Card: View-Toggle 'Mechaniken' / 'Meine Spells'
- renderMySpellsHtml(): Assignments pro Mechanik gruppiert (1 Zeile statt 1 Zeile/Ability)
- initMySpells(): Toggle-Logik, Job-Dropdown persistent (localStorage), Kopieren-Button
- Personal-Mits lila (myspells-type--personal / badge-assign-personal) in beiden Views
- Legende: Personal-Eintrag ergänzt
- css/planner.css:
- .view-toggle-btns / .view-toggle-btn: Toggle-Button-Styles
- .myspells-*: Cheatsheet-Layout (Grid, Badges, Farbkodierung)
- .badge-assign-personal: Lila für Personal-Mits in Mechaniken-Übersicht
- .info-legend-dot--personal: Lila Dot in Legende
- .planner-info-panel: border-bottom statt border-top (jetzt oben in Sidebar)
- .timeline-scroll: dunkler Custom-Scrollbar (webkit + scrollbar-color)
- templates/tab-planner.php: Info-Panel über Pläne-Sektion verschoben
- templates/topbar.php: Planer-Icon 📋 -> ☰
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1a91d1af0e
commit
fce55c5bb6
152
css/planner.css
152
css/planner.css
@ -320,9 +320,10 @@
|
||||
font-size: 13px;
|
||||
color: var(--t2);
|
||||
}
|
||||
.badge-assign-buff { background: rgba(200,168,75,.08); border-color: rgba(200,168,75,.4); color: var(--gold); }
|
||||
.badge-assign-debuff { background: rgba(224,92,92,.08); border-color: rgba(224,92,92,.4); color: var(--red); }
|
||||
.badge-assign-shield { background: rgba(74,158,255,.08); border-color: rgba(74,158,255,.4); color: var(--blue); }
|
||||
.badge-assign-buff { background: rgba(200,168,75,.08); border-color: rgba(200,168,75,.4); color: var(--gold); }
|
||||
.badge-assign-debuff { background: rgba(224,92,92,.08); border-color: rgba(224,92,92,.4); color: var(--red); }
|
||||
.badge-assign-shield { background: rgba(74,158,255,.08); border-color: rgba(74,158,255,.4); color: var(--blue); }
|
||||
.badge-assign-personal { background: rgba(177,112,255,.08); border-color: rgba(177,112,255,.4); color: #dbc7ff; }
|
||||
|
||||
.badge-assign--missing-job {
|
||||
border-style: dashed;
|
||||
@ -485,9 +486,10 @@
|
||||
}
|
||||
.ability-chip:hover { background: var(--bg3); color: var(--t1); }
|
||||
|
||||
.ability-chip.badge-assign-buff.ability-chip--active { background: rgba(200,168,75,.18); border-color: rgba(200,168,75,.6); color: var(--gold); }
|
||||
.ability-chip.badge-assign-debuff.ability-chip--active { background: rgba(224,92,92,.18); border-color: rgba(224,92,92,.6); color: var(--red); }
|
||||
.ability-chip.badge-assign-shield.ability-chip--active { background: rgba(74,158,255,.18); border-color: rgba(74,158,255,.6); color: var(--blue); }
|
||||
.ability-chip.badge-assign-buff.ability-chip--active { background: rgba(200,168,75,.18); border-color: rgba(200,168,75,.6); color: var(--gold); }
|
||||
.ability-chip.badge-assign-debuff.ability-chip--active { background: rgba(224,92,92,.18); border-color: rgba(224,92,92,.6); color: var(--red); }
|
||||
.ability-chip.badge-assign-shield.ability-chip--active { background: rgba(74,158,255,.18); border-color: rgba(74,158,255,.6); color: var(--blue); }
|
||||
.ability-chip.badge-assign-personal.ability-chip--active { background: rgba(177,112,255,.18); border-color: rgba(177,112,255,.6); color: #dbc7ff; }
|
||||
|
||||
.ability-chip--other-job { opacity: 0.45; }
|
||||
|
||||
@ -526,9 +528,9 @@
|
||||
|
||||
/* ── Info Panel ──────────────────────────────────────────────────────────────── */
|
||||
.planner-info-panel {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.info-section { margin-bottom: 12px; }
|
||||
@ -560,9 +562,10 @@
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info-legend-dot--buff { background: rgba(200,168,75,.8); }
|
||||
.info-legend-dot--debuff { background: var(--red); }
|
||||
.info-legend-dot--shield { background: var(--blue); }
|
||||
.info-legend-dot--buff { background: rgba(200,168,75,.8); }
|
||||
.info-legend-dot--debuff { background: var(--red); }
|
||||
.info-legend-dot--shield { background: var(--blue); }
|
||||
.info-legend-dot--personal { background: rgba(177,112,255,.9); }
|
||||
|
||||
.info-legend-label {
|
||||
font-size: 12px;
|
||||
@ -754,6 +757,8 @@
|
||||
overflow-y: hidden;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg1);
|
||||
scrollbar-color: var(--border) var(--bg1);
|
||||
scrollbar-width: thin;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
cursor: grab;
|
||||
@ -764,6 +769,11 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.timeline-scroll::-webkit-scrollbar { height: 6px; }
|
||||
.timeline-scroll::-webkit-scrollbar-track { background: var(--bg1); }
|
||||
.timeline-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
.timeline-scroll::-webkit-scrollbar-thumb:hover { background: var(--t3); }
|
||||
|
||||
.timeline-grid {
|
||||
width: calc(180px + var(--timeline-width));
|
||||
display: grid;
|
||||
@ -1210,6 +1220,124 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── View Toggle ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.view-toggle-btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.view-toggle-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
background: transparent;
|
||||
color: var(--t2);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-b);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.view-toggle-btn:hover {
|
||||
color: var(--t1);
|
||||
border-color: var(--t3);
|
||||
}
|
||||
|
||||
.view-toggle-btn.active {
|
||||
background: var(--gold);
|
||||
border-color: var(--gold);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Meine Spells ────────────────────────────────────────────────────────── */
|
||||
|
||||
.myspells-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.myspells-controls select {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
background: var(--bg2);
|
||||
color: var(--t1);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.myspells-row {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 180px 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 4px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.myspells-abilities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.myspells-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.myspells-time {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
color: var(--gold);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.myspells-mechanic {
|
||||
color: var(--t2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.myspells-ability {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: var(--t1);
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
padding: 3px 8px 3px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.myspells-ability.myspells-type--debuff { color: var(--orange); border-color: rgba(255,140,0,0.3); }
|
||||
.myspells-ability.myspells-type--shield { color: var(--blue); border-color: rgba(88,166,255,0.3); }
|
||||
.myspells-ability.myspells-type--personal { color: #dbc7ff; border-color: rgba(177,112,255,0.4); }
|
||||
|
||||
.myspells-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.myspells-empty {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: var(--t3);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.planner-layout {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
130
js/planner.js
130
js/planner.js
@ -461,10 +461,26 @@ function renderPlanDetail(plan) {
|
||||
<div class="card">
|
||||
<div class="card-title-row">
|
||||
<div class="card-title">Mechaniken</div>
|
||||
<div class="view-toggle-btns">
|
||||
<button class="view-toggle-btn active" data-view="mechanics">Mechaniken</button>
|
||||
<button class="view-toggle-btn" data-view="myspells">★ Meine Spells</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mechanic-list">
|
||||
${renderMechanicListHtml(plan)}
|
||||
</div>
|
||||
<div id="myspells-panel" style="display:none">
|
||||
<div class="myspells-controls">
|
||||
<select id="myspells-job-select">
|
||||
<option value="">— Job wählen —</option>
|
||||
${(plan.jobComposition ?? []).filter(Boolean).filter((j, i, a) => a.indexOf(j) === i).map(j =>
|
||||
`<option value="${escHtml(j)}">${escHtml(j)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
<button id="myspells-copy-btn" class="btn btn-sm" title="Als Text kopieren">⎘ Kopieren</button>
|
||||
</div>
|
||||
<div id="myspells-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -478,10 +494,118 @@ function renderPlanDetail(plan) {
|
||||
initTimelineOptions(plan.id);
|
||||
initTimeline(plan.id);
|
||||
initMechanicClicks(plan.id);
|
||||
initMySpells(plan.id);
|
||||
renderInfoPanel(plan);
|
||||
ensureActionMetaLoaded().then(() => refreshMechanicList(plan.id));
|
||||
}
|
||||
|
||||
// ── Meine Spells ─────────────────────────────────────────────────────────────
|
||||
|
||||
function renderMySpellsHtml(plan, job) {
|
||||
if (!job) return '<div class="myspells-empty">Job auswählen um Assignments zu sehen.</div>';
|
||||
|
||||
const mechanics = visiblePlanMechanics(plan);
|
||||
const rows = [];
|
||||
for (const mechanic of mechanics) {
|
||||
const mine = (mechanic.assignments ?? []).filter(a => a.job === job);
|
||||
if (!mine.length) continue;
|
||||
rows.push({ mechanic, assignments: mine });
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return `<div class="myspells-empty">Keine Assignments für ${escHtml(job)} in diesem Plan.</div>`;
|
||||
}
|
||||
|
||||
return rows.map(({ mechanic, assignments }) => {
|
||||
const time = escHtml(fmtTimestamp(mechanic.timestamp));
|
||||
const mechName = escHtml(mechanic.name);
|
||||
const abilities = assignments.map(a => {
|
||||
const iconSrc = abilityIcon(a.ability);
|
||||
const icon = iconSrc
|
||||
? `<img src="${escHtml(iconSrc)}" class="myspells-icon" alt="">`
|
||||
: '';
|
||||
const name = escHtml(assignmentAbilityName(a, plan));
|
||||
const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(a.ability);
|
||||
const typeClass = a.buffType === 'debuff' ? 'myspells-type--debuff'
|
||||
: a.buffType === 'shield' ? 'myspells-type--shield'
|
||||
: isPersonal ? 'myspells-type--personal'
|
||||
: 'myspells-type--buff';
|
||||
return `<span class="myspells-ability ${typeClass}">${icon}${name}</span>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="myspells-row">
|
||||
<span class="myspells-time">${time}</span>
|
||||
<span class="myspells-mechanic">${mechName}</span>
|
||||
<div class="myspells-abilities">${abilities}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function mySpellsPlainText(plan, job) {
|
||||
if (!job) return '';
|
||||
const mechanics = visiblePlanMechanics(plan);
|
||||
const lines = [`Meine Spells — ${job} (${plan.name})`, '─'.repeat(36)];
|
||||
for (const mechanic of mechanics) {
|
||||
const mine = (mechanic.assignments ?? []).filter(a => a.job === job);
|
||||
if (!mine.length) continue;
|
||||
const time = fmtTimestamp(mechanic.timestamp);
|
||||
const names = mine.map(a => assignmentAbilityName(a, plan)).join(', ');
|
||||
lines.push(`${time.padEnd(6)} ${mechanic.name.padEnd(22)} → ${names}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function initMySpells(planId) {
|
||||
const viewBtns = document.querySelectorAll('.view-toggle-btn');
|
||||
const mechList = document.getElementById('mechanic-list');
|
||||
const myPanel = document.getElementById('myspells-panel');
|
||||
const jobSelect = document.getElementById('myspells-job-select');
|
||||
const myList = document.getElementById('myspells-list');
|
||||
const copyBtn = document.getElementById('myspells-copy-btn');
|
||||
if (!mechList || !myPanel || !jobSelect || !myList) return;
|
||||
|
||||
viewBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
viewBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const view = btn.dataset.view;
|
||||
mechList.style.display = view === 'mechanics' ? '' : 'none';
|
||||
myPanel.style.display = view === 'myspells' ? '' : 'none';
|
||||
if (view === 'myspells') {
|
||||
const plan = loadPlans().find(p => p.id === planId);
|
||||
if (plan) myList.innerHTML = renderMySpellsHtml(plan, jobSelect.value || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Gespeicherten Job wiederherstellen
|
||||
const savedJob = localStorage.getItem('ff14-planner-myspells-job') ?? '';
|
||||
if (savedJob && [...jobSelect.options].some(o => o.value === savedJob)) {
|
||||
jobSelect.value = savedJob;
|
||||
const plan = loadPlans().find(p => p.id === planId);
|
||||
if (plan) myList.innerHTML = renderMySpellsHtml(plan, savedJob);
|
||||
}
|
||||
|
||||
jobSelect.addEventListener('change', () => {
|
||||
localStorage.setItem('ff14-planner-myspells-job', jobSelect.value);
|
||||
const plan = loadPlans().find(p => p.id === planId);
|
||||
if (plan) myList.innerHTML = renderMySpellsHtml(plan, jobSelect.value);
|
||||
});
|
||||
|
||||
copyBtn?.addEventListener('click', () => {
|
||||
const plan = loadPlans().find(p => p.id === planId);
|
||||
if (!plan) return;
|
||||
const text = mySpellsPlainText(plan, jobSelect.value);
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
copyBtn.textContent = '✓ Kopiert';
|
||||
setTimeout(() => { copyBtn.innerHTML = '⎘ Kopieren'; }, 1800);
|
||||
}).catch(() => {
|
||||
copyBtn.textContent = '✗ Fehler';
|
||||
setTimeout(() => { copyBtn.innerHTML = '⎘ Kopieren'; }, 1800);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function avgNonTankMaxHp(plan) {
|
||||
const roster = plan.playerRoster ?? [];
|
||||
const jobComp = plan.jobComposition ?? [];
|
||||
@ -585,8 +709,9 @@ function renderMechanicListHtml(plan) {
|
||||
const assignHtml = sorted.length === 0
|
||||
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
|
||||
: sorted.map(a => {
|
||||
const cls = a.buffType === 'debuff' ? 'badge-assign-debuff'
|
||||
: a.buffType === 'shield' ? 'badge-assign-shield'
|
||||
const cls = a.buffType === 'debuff' ? 'badge-assign-debuff'
|
||||
: a.buffType === 'shield' ? 'badge-assign-shield'
|
||||
: TIMELINE_PERSONAL_ABILITIES.has(a.ability) ? 'badge-assign-personal'
|
||||
: 'badge-assign-buff';
|
||||
const isMissing = !!a.job && !activeJobSet.has(a.job);
|
||||
const icon = abilityIcon(a.ability);
|
||||
@ -1910,6 +2035,7 @@ function renderInfoPanel(plan) {
|
||||
<div class="info-legend-row"><span class="info-legend-dot info-legend-dot--buff"></span><span class="info-legend-label">Mitigation</span></div>
|
||||
<div class="info-legend-row"><span class="info-legend-dot info-legend-dot--debuff"></span><span class="info-legend-label">Debuff</span></div>
|
||||
<div class="info-legend-row"><span class="info-legend-dot info-legend-dot--shield"></span><span class="info-legend-label">Schild</span></div>
|
||||
<div class="info-legend-row"><span class="info-legend-dot info-legend-dot--personal"></span><span class="info-legend-label">Personal</span></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
<!-- Left: Plan list sidebar -->
|
||||
<div class="plan-sidebar">
|
||||
|
||||
<div id="planner-info-panel" class="planner-info-panel"></div>
|
||||
|
||||
<div class="plan-sidebar-header">
|
||||
<div class="card-title">Pläne</div>
|
||||
<button id="planner-new-folder-btn" class="btn btn-sm" title="Neuer Ordner">+ Ordner</button>
|
||||
@ -26,7 +29,6 @@
|
||||
|
||||
<div id="plan-list"></div>
|
||||
|
||||
<div id="planner-info-panel" class="planner-info-panel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Plan detail -->
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<nav class="tabs">
|
||||
<button class="tab active" data-tab="report">⚔ Report</button>
|
||||
<button class="tab" data-tab="analysis">⚖ Analyse</button>
|
||||
<button class="tab" data-tab="planner">📋 Planer</button>
|
||||
<button class="tab" data-tab="planner">☰ Planer</button>
|
||||
</nav>
|
||||
<div class="topbar-user">Token gültig bis: <?= date('Y-m-d H:i:s', $_SESSION['token_expires']) ?></div>
|
||||
</header>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user