Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
179 lines
5.9 KiB
JavaScript
179 lines
5.9 KiB
JavaScript
// ── tim — time calculator plugin ──
|
|
|
|
const $ = id => document.getElementById(id);
|
|
const h = window.hyprcrd;
|
|
const overlay = $('tim-overlay');
|
|
|
|
function show() { overlay?.classList.remove('hidden'); $('tim-dur-start')?.focus(); }
|
|
function hide() { overlay?.classList.add('hidden'); }
|
|
function toggle() { overlay?.classList.contains('hidden') ? show() : hide(); }
|
|
|
|
function pad4(n) { return String(n).padStart(4, '0'); }
|
|
|
|
// Parse HHMM string → total minutes since midnight, or null on invalid
|
|
function parseHHMM(s) {
|
|
if (!s) return null;
|
|
s = String(s).trim().replace(':', '').padStart(4, '0');
|
|
if (!/^\d{3,4}$/.test(s)) return null;
|
|
const h = parseInt(s.slice(0, -2));
|
|
const m = parseInt(s.slice(-2));
|
|
if (m >= 60) return null;
|
|
return h * 60 + m;
|
|
}
|
|
|
|
function minutesToHHMM(mins) {
|
|
mins = ((mins % (24 * 60)) + 24 * 60) % (24 * 60);
|
|
return pad4(Math.floor(mins / 60) * 100 + (mins % 60));
|
|
}
|
|
|
|
function formatDuration(mins) {
|
|
if (mins < 0) mins += 24 * 60;
|
|
const h = Math.floor(mins / 60), m = mins % 60;
|
|
return `${h}h ${String(m).padStart(2, '0')}m`;
|
|
}
|
|
|
|
// ── Tab switching ──
|
|
|
|
document.querySelectorAll('.tim-tab').forEach(t => {
|
|
t.addEventListener('click', () => {
|
|
document.querySelectorAll('.tim-tab').forEach(x => x.classList.toggle('active', x === t));
|
|
document.querySelectorAll('.tim-tab-body').forEach(p => {
|
|
p.classList.toggle('hidden', p.dataset.timPanel !== t.dataset.timTab);
|
|
});
|
|
const firstInput = document.querySelector(`[data-tim-panel="${t.dataset.timTab}"] input`);
|
|
firstInput?.focus();
|
|
});
|
|
});
|
|
|
|
// ── Duration tab ──
|
|
|
|
function calcDuration() {
|
|
const res = $('tim-dur-result');
|
|
const s = parseHHMM($('tim-dur-start').value);
|
|
const e = parseHHMM($('tim-dur-end').value);
|
|
if (s == null || e == null) {
|
|
res.className = 'tim-result';
|
|
res.textContent = '';
|
|
return;
|
|
}
|
|
const diff = e - s;
|
|
res.className = 'tim-result ok';
|
|
res.textContent = formatDuration(diff);
|
|
}
|
|
|
|
// ── Add tab ──
|
|
|
|
function calcAdd() {
|
|
const res = $('tim-add-result');
|
|
const t = parseHHMM($('tim-add-time').value);
|
|
const d = parseHHMM($('tim-add-dur').value);
|
|
if (t == null || d == null) {
|
|
res.className = 'tim-result';
|
|
res.textContent = '';
|
|
return;
|
|
}
|
|
res.className = 'tim-result ok';
|
|
res.textContent = minutesToHHMM(t + d);
|
|
}
|
|
|
|
// ── Workday tab ──
|
|
|
|
function calcWorkday() {
|
|
const res = $('tim-wd-result');
|
|
const s = parseHHMM($('tim-wd-start').value);
|
|
const bf = parseHHMM($('tim-wd-break-from').value);
|
|
const bb = parseHHMM($('tim-wd-break-back').value);
|
|
const target = parseHHMM($('tim-wd-target').value) ?? 480;
|
|
|
|
if (s == null || bf == null || bb == null) {
|
|
res.className = 'tim-result';
|
|
res.textContent = '';
|
|
return;
|
|
}
|
|
const workedBeforeBreak = bf - s;
|
|
if (workedBeforeBreak < 0) {
|
|
res.className = 'tim-result err';
|
|
res.textContent = 'Break must be after start';
|
|
return;
|
|
}
|
|
const remaining = target - workedBeforeBreak;
|
|
if (remaining < 0) {
|
|
res.className = 'tim-result ok';
|
|
res.textContent = 'Target already reached at ' + minutesToHHMM(s + target);
|
|
return;
|
|
}
|
|
const endOfDay = (bb + remaining) % (24 * 60);
|
|
res.className = 'tim-result ok';
|
|
res.textContent = `End at ${minutesToHHMM(endOfDay)} (${formatDuration(remaining)} left after break)`;
|
|
}
|
|
|
|
// Single delegated input handler on the overlay — routes to the right
|
|
// calc by input id. Robust against DOM order / re-renders.
|
|
overlay?.addEventListener('input', e => {
|
|
const id = e.target?.id || '';
|
|
if (id.startsWith('tim-dur-')) calcDuration();
|
|
else if (id.startsWith('tim-add-')) calcAdd();
|
|
else if (id.startsWith('tim-wd-')) calcWorkday();
|
|
});
|
|
|
|
// Focus → select existing text so typing replaces it.
|
|
overlay?.addEventListener('focusin', e => {
|
|
if (e.target.tagName === 'INPUT' && e.target.value) e.target.select();
|
|
});
|
|
|
|
// Tab cycles within the currently active tab panel's inputs.
|
|
overlay?.addEventListener('keydown', e => {
|
|
if (e.key !== 'Tab') return;
|
|
const panel = e.target.closest('.tim-tab-body:not(.hidden)');
|
|
if (!panel) return;
|
|
const inputs = [...panel.querySelectorAll('input')];
|
|
if (!inputs.length) return;
|
|
const idx = inputs.indexOf(e.target);
|
|
if (idx === -1) return;
|
|
e.preventDefault();
|
|
const next = e.shiftKey
|
|
? (idx - 1 + inputs.length) % inputs.length
|
|
: (idx + 1) % inputs.length;
|
|
inputs[next].focus();
|
|
});
|
|
|
|
// ── Global bindings ──
|
|
|
|
$('tim-close')?.addEventListener('click', hide);
|
|
overlay?.addEventListener('click', e => { if (e.target === overlay) hide(); });
|
|
|
|
window.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape' && !overlay?.classList.contains('hidden')) {
|
|
e.preventDefault(); e.stopPropagation();
|
|
hide();
|
|
}
|
|
}, { capture: true });
|
|
|
|
// Expose toggle for terminal / shortcuts
|
|
window._timToggle = toggle;
|
|
|
|
// Register toolbar command — the md-toolbar delegated click handler in
|
|
// editor.js routes clicks on [data-md="tim"] through this.
|
|
h?.editor?.registerCommand('tim', () => toggle());
|
|
|
|
function injectToolbarButton() {
|
|
const toolbar = document.getElementById('md-toolbar');
|
|
if (!toolbar) return;
|
|
if (toolbar.querySelector('[data-md="tim"]')) return;
|
|
const btn = document.createElement('button');
|
|
btn.setAttribute('data-md', 'tim');
|
|
btn.setAttribute('data-help', 'tim — time calculator\nDuration, add, workday end.\nShortcut: I');
|
|
btn.className = 'toolbar-btn';
|
|
btn.innerHTML = '<i class="ph ph-clock text-sm"></i>';
|
|
const spacer = toolbar.querySelector('.flex-1');
|
|
if (spacer) toolbar.insertBefore(btn, spacer);
|
|
else toolbar.appendChild(btn);
|
|
}
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', injectToolbarButton);
|
|
} else {
|
|
injectToolbarButton();
|
|
}
|
|
|
|
h?.registerPlugin({ name: 'tim', version: '1.0.0' });
|