diff --git a/css/planner.css b/css/planner.css index c590c8b..64a1b39 100644 --- a/css/planner.css +++ b/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; diff --git a/js/planner.js b/js/planner.js index 6a7c9d5..c3f9b53 100644 --- a/js/planner.js +++ b/js/planner.js @@ -461,10 +461,26 @@ function renderPlanDetail(plan) {
Mechaniken
+
+ + +
${renderMechanicListHtml(plan)}
+
`; @@ -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 '
Job auswählen um Assignments zu sehen.
'; + + 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 `
Keine Assignments für ${escHtml(job)} in diesem Plan.
`; + } + + 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 + ? `` + : ''; + 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 `${icon}${name}`; + }).join(''); + return ` +
+ ${time} + ${mechName} +
${abilities}
+
`; + }).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 ? 'Keine Zuweisung' : 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) {
Mitigation
Debuff
Schild
+
Personal
`; diff --git a/templates/tab-planner.php b/templates/tab-planner.php index 170b073..ebf2622 100644 --- a/templates/tab-planner.php +++ b/templates/tab-planner.php @@ -2,6 +2,9 @@
+ +
+
Pläne
@@ -26,7 +29,6 @@
-
diff --git a/templates/topbar.php b/templates/topbar.php index 0457f39..f6376c4 100644 --- a/templates/topbar.php +++ b/templates/topbar.php @@ -3,7 +3,7 @@
Token gültig bis: