Add evidence submission, 3-state offline tasks, parent comments, and retake policies
- Evidence on assignments: photo upload + notes + student self-note (needs help / not sure) - Evidence required/optional/none flag per item; grading queue shows evidence with lightbox thumbnail - Parent comment field when grading; shown to student on graded items - Offline tasks: 3-state status (not started → in progress → done) with inline evidence form - Retake policy per item: latest / highest / average — gradebook computes effective score accordingly - Grading queue expanded to include lessons and practices (not just assignments) when evidence present Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
02909e1edc
commit
a7de24b1da
7 files changed
+351
-58
No files matched your search
@@ -120,6 +120,22 @@
|
||||
<label>Prerequisite item (must be done first)
|
||||
<select id="ie-prereq"><option value="">None</option></select>
|
||||
</label>
|
||||
<div class="inline-form">
|
||||
<label>Evidence from student
|
||||
<select id="ie-evidence-mode">
|
||||
<option value="none">None</option>
|
||||
<option value="optional">Optional</option>
|
||||
<option value="required">Required</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Retake score policy
|
||||
<select id="ie-retake-policy">
|
||||
<option value="latest">Latest attempt</option>
|
||||
<option value="highest">Highest score</option>
|
||||
<option value="average">Average of all</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Scan a page: visible for lesson + quiz types -->
|
||||
<div id="ie-scan-section" class="scan-card" hidden>
|
||||
@@ -189,6 +205,14 @@
|
||||
<h1>Grading Queue</h1>
|
||||
<div id="grading-rows"></div>
|
||||
<p id="grading-empty" class="hint" hidden>Nothing waiting to be graded 🎉</p>
|
||||
<!-- Evidence lightbox -->
|
||||
<div id="evidence-lightbox" class="modal" hidden>
|
||||
<div class="modal-box">
|
||||
<h3>Evidence</h3>
|
||||
<div id="evidence-lightbox-body"></div>
|
||||
<button id="evidence-lightbox-close" class="secondary">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gradebook -->
|
||||
|
||||
+61
-14
@@ -305,6 +305,8 @@ async function openItemEditor(unitId, itemId) {
|
||||
$('#ie-due-date').value = item.due_date || '';
|
||||
$('#ie-allow-retakes').checked = !!item.allow_retakes;
|
||||
$('#ie-prereq').value = item.prereq_item_id || '';
|
||||
$('#ie-evidence-mode').value = item.evidence_mode || 'none';
|
||||
$('#ie-retake-policy').value = item.retake_policy || 'latest';
|
||||
if (item.type === 'spelling_practice' || item.type === 'spelling_test') $('#ie-list').value = item.ref_id;
|
||||
if (item.type === 'flashcards') $('#ie-deck').value = item.ref_id;
|
||||
if (item.type === 'quiz') (item.questions || []).forEach(addQuestionRow);
|
||||
@@ -324,6 +326,8 @@ async function openItemEditor(unitId, itemId) {
|
||||
$('#ie-due-date').value = '';
|
||||
$('#ie-allow-retakes').checked = false;
|
||||
$('#ie-prereq').value = '';
|
||||
$('#ie-evidence-mode').value = 'none';
|
||||
$('#ie-retake-policy').value = 'latest';
|
||||
$('#ie-delete').hidden = true;
|
||||
}
|
||||
updateItemFieldVisibility();
|
||||
@@ -488,6 +492,8 @@ $('#item-form').addEventListener('submit', async (e) => {
|
||||
dueDate: $('#ie-due-date').value || null,
|
||||
allowRetakes: $('#ie-allow-retakes').checked,
|
||||
prereqItemId: prereqVal ? Number(prereqVal) : null,
|
||||
evidenceMode: $('#ie-evidence-mode').value,
|
||||
retakePolicy: $('#ie-retake-policy').value,
|
||||
};
|
||||
if (type === 'quiz') {
|
||||
body.questions = Array.from(document.querySelectorAll('.quiz-q-row')).map((row) => ({
|
||||
@@ -683,30 +689,71 @@ async function printWeekReport(studentId, start) {
|
||||
// Grading queue
|
||||
// ============================================================
|
||||
|
||||
$('#evidence-lightbox-close').addEventListener('click', () => { $('#evidence-lightbox').hidden = true; });
|
||||
|
||||
function showEvidenceLightbox(notes, photo) {
|
||||
let html = '';
|
||||
if (photo) html += `<img src="${photo}" style="max-width:100%;border-radius:8px;margin-bottom:.75rem">`;
|
||||
if (notes) html += `<p style="white-space:pre-wrap">${esc(notes)}</p>`;
|
||||
$('#evidence-lightbox-body').innerHTML = html || '<p class="hint">No evidence content.</p>';
|
||||
$('#evidence-lightbox').hidden = false;
|
||||
}
|
||||
|
||||
const STUDENT_NOTE_LABEL = { needs_help: '🙋 Needs help', not_sure: '🤔 Not sure' };
|
||||
|
||||
async function loadGrading() {
|
||||
const rows = await api('/api/grading-queue');
|
||||
$('#grading-empty').hidden = rows.length > 0;
|
||||
$('#grading-rows').innerHTML = rows
|
||||
.map(
|
||||
(r) => `<div class="item-row">
|
||||
<span>${esc(r.emoji)}</span>
|
||||
<strong class="grow">${esc(r.studentName)}</strong>
|
||||
<span>${esc(r.itemTitle)}</span>
|
||||
<span class="hint">${esc(r.courseName)} · ${esc(r.unitName)}</span>
|
||||
<input type="number" class="grade-input" min="0" max="${r.points_possible}" placeholder="/ ${r.points_possible}" data-sub="${r.submissionId}">
|
||||
<button data-save-grade="${r.submissionId}">Save</button>
|
||||
</div>`
|
||||
)
|
||||
.map((r) => {
|
||||
const hasEvidence = r.evidence_notes || r.evidence_photo;
|
||||
const noteTag = r.student_note ? `<span class="badge-tag">${STUDENT_NOTE_LABEL[r.student_note] || r.student_note}</span>` : '';
|
||||
const evidenceBtn = hasEvidence
|
||||
? `<button class="secondary small" data-evidence-notes="${esc(r.evidence_notes || '')}" data-evidence-photo="${r.evidence_photo ? 'yes' : ''}" data-sub-id="${r.submissionId}">📎 Evidence</button>`
|
||||
: '';
|
||||
return `<div class="grading-card">
|
||||
<div class="grading-header">
|
||||
<span>${esc(r.emoji)}</span>
|
||||
<strong>${esc(r.studentName)}</strong>
|
||||
<span class="grow">${esc(r.itemTitle)}</span>
|
||||
<span class="hint">${esc(r.courseName)} · ${esc(r.unitName)}</span>
|
||||
${noteTag}
|
||||
</div>
|
||||
${evidenceBtn}
|
||||
<div class="grading-footer">
|
||||
<input type="number" class="grade-input" min="0" max="${r.points_possible}" placeholder="Score / ${r.points_possible || '?'}" data-sub="${r.submissionId}">
|
||||
<input type="text" class="comment-input" placeholder="Parent comment (optional)" data-comment-sub="${r.submissionId}">
|
||||
<button data-save-grade="${r.submissionId}">Save grade</button>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
document.querySelectorAll('[data-save-grade]').forEach((btn) =>
|
||||
document.querySelectorAll('[data-evidence-notes]').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
// photo may be truncated in the attribute — fetch full evidence via API
|
||||
const subId = btn.dataset.subId;
|
||||
let photo = null;
|
||||
if (btn.dataset.evidencePhoto === 'yes') {
|
||||
const full = await fetch(`/api/grading-queue/${subId}/evidence`, { headers: { 'x-pin': parentPin } });
|
||||
if (full.ok) { const d = await full.json(); photo = d.evidence_photo; }
|
||||
}
|
||||
showEvidenceLightbox(btn.dataset.evidenceNotes || null, photo);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-save-grade]').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const input = document.querySelector(`.grade-input[data-sub="${btn.dataset.saveGrade}"]`);
|
||||
const commentInput = document.querySelector(`.comment-input[data-comment-sub="${btn.dataset.saveGrade}"]`);
|
||||
if (input.value === '') return;
|
||||
await api(`/api/submissions/${btn.dataset.saveGrade}/grade`, { method: 'PUT', body: { score: Number(input.value) } });
|
||||
await api(`/api/submissions/${btn.dataset.saveGrade}/grade`, {
|
||||
method: 'PUT',
|
||||
body: { score: Number(input.value), parentComment: commentInput?.value || '' },
|
||||
});
|
||||
loadGrading();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
+133
-12
@@ -54,6 +54,46 @@ document.querySelectorAll('[data-back]').forEach((btn) =>
|
||||
})
|
||||
);
|
||||
|
||||
// ---------- evidence photo helpers ----------
|
||||
|
||||
let evidencePhotoBase64 = null;
|
||||
|
||||
function compressPhoto(file, maxDim = 1200, quality = 0.75) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = () => reject(new Error('Could not read file'));
|
||||
reader.onload = () => {
|
||||
const img = new Image();
|
||||
img.onerror = () => reject(new Error('Could not read image'));
|
||||
img.onload = () => {
|
||||
let { width, height } = img;
|
||||
if (width > maxDim || height > maxDim) {
|
||||
const scale = maxDim / Math.max(width, height);
|
||||
width = Math.round(width * scale);
|
||||
height = Math.round(height * scale);
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width; canvas.height = height;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
|
||||
resolve(canvas.toDataURL('image/jpeg', quality));
|
||||
};
|
||||
img.src = reader.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
$('#assignment-photo-btn').addEventListener('click', () => $('#assignment-photo-input').click());
|
||||
$('#assignment-photo-input').addEventListener('change', async () => {
|
||||
const file = $('#assignment-photo-input').files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const dataUrl = await compressPhoto(file);
|
||||
evidencePhotoBase64 = dataUrl;
|
||||
$('#assignment-photo-status').textContent = '✅ Photo ready';
|
||||
} catch { $('#assignment-photo-status').textContent = '❌ Could not load photo'; }
|
||||
});
|
||||
|
||||
// ---------- text to speech (spelling module) ----------
|
||||
|
||||
function speak(text, rate = 0.85) {
|
||||
@@ -145,16 +185,38 @@ async function openKid(id, tab = 'today') {
|
||||
switchTab(tab);
|
||||
}
|
||||
|
||||
const OFFLINE_STATUS_ICON = { not_started: '⬜', in_progress: '🔄', done: '✅' };
|
||||
|
||||
function renderToday(tasks) {
|
||||
$('#today-empty').hidden = tasks.length > 0;
|
||||
$('#today-list').innerHTML = tasks
|
||||
.map((t) => {
|
||||
if (!t.itemId) {
|
||||
return `<button class="today-row offline" data-schedule-id="${t.id}">
|
||||
<span class="row-check">${t.done ? '✅' : '⬜'}</span>
|
||||
<span class="row-title">${esc(t.offlineTitle)}</span>
|
||||
<span class="row-badge">📌 Offline</span>
|
||||
</button>`;
|
||||
const st = t.offlineStatus || (t.done ? 'done' : 'not_started');
|
||||
return `<div class="today-row-offline" data-schedule-id="${t.id}">
|
||||
<div class="offline-main">
|
||||
<span class="row-check">${OFFLINE_STATUS_ICON[st] || '⬜'}</span>
|
||||
<span class="row-title">${esc(t.offlineTitle)}</span>
|
||||
<span class="row-badge">📌${t.hasEvidence ? ' 📎' : ''}</span>
|
||||
</div>
|
||||
<div class="offline-controls">
|
||||
<button class="offline-status-btn ${st === 'not_started' ? 'active' : ''}" data-sid="${t.id}" data-status="not_started">Not started</button>
|
||||
<button class="offline-status-btn ${st === 'in_progress' ? 'active' : ''}" data-sid="${t.id}" data-status="in_progress">In progress</button>
|
||||
<button class="offline-status-btn ${st === 'done' ? 'active' : ''}" data-sid="${t.id}" data-status="done">Done ✓</button>
|
||||
</div>
|
||||
<details class="offline-evidence-form">
|
||||
<summary>📎 Add notes / photo</summary>
|
||||
<div class="evidence-inner">
|
||||
<textarea class="offline-notes" placeholder="Notes…" rows="2"></textarea>
|
||||
<div class="evidence-photo-row">
|
||||
<button type="button" class="secondary small offline-photo-btn">📷 Photo</button>
|
||||
<input type="file" class="offline-photo-input" accept="image/*" capture="environment" hidden>
|
||||
<span class="offline-photo-status hint"></span>
|
||||
</div>
|
||||
<button type="button" class="secondary small offline-evidence-save" data-sid="${t.id}">Save evidence</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>`;
|
||||
}
|
||||
const badge = statusBadge(t.type, t.subStatus, t.score, t.points_possible, t.done);
|
||||
return `<button class="today-row" data-item-id="${t.itemId}" data-item-type="${t.type}">
|
||||
@@ -165,13 +227,43 @@ function renderToday(tasks) {
|
||||
})
|
||||
.join('');
|
||||
|
||||
document.querySelectorAll('.today-row.offline').forEach((row) =>
|
||||
row.addEventListener('click', async () => {
|
||||
const done = row.querySelector('.row-check').textContent.trim() !== '✅';
|
||||
await api(`/api/schedule/${row.dataset.scheduleId}/done`, { method: 'POST', body: { done } });
|
||||
// Offline status buttons
|
||||
document.querySelectorAll('.offline-status-btn').forEach((btn) =>
|
||||
btn.addEventListener('click', async () => {
|
||||
await api(`/api/schedule/${btn.dataset.sid}/done`, { method: 'POST', body: { status: btn.dataset.status } });
|
||||
openKid(currentStudent.id, 'today');
|
||||
})
|
||||
);
|
||||
|
||||
// Offline evidence photo picker
|
||||
document.querySelectorAll('.today-row-offline').forEach((row) => {
|
||||
const photoBtn = row.querySelector('.offline-photo-btn');
|
||||
const photoInput = row.querySelector('.offline-photo-input');
|
||||
const photoStatus = row.querySelector('.offline-photo-status');
|
||||
photoBtn.addEventListener('click', () => photoInput.click());
|
||||
photoInput.addEventListener('change', async () => {
|
||||
const file = photoInput.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
photoInput._dataUrl = await compressPhoto(file);
|
||||
photoStatus.textContent = '✅ Ready';
|
||||
} catch { photoStatus.textContent = '❌ Error'; }
|
||||
});
|
||||
});
|
||||
|
||||
// Offline evidence save
|
||||
document.querySelectorAll('.offline-evidence-save').forEach((btn) =>
|
||||
btn.addEventListener('click', async () => {
|
||||
const row = btn.closest('.today-row-offline');
|
||||
const notes = row.querySelector('.offline-notes').value || null;
|
||||
const photoInput = row.querySelector('.offline-photo-input');
|
||||
const photo = photoInput._dataUrl || null;
|
||||
await api(`/api/schedule/${btn.dataset.sid}/done`, { method: 'POST', body: { evidenceNotes: notes, evidencePhoto: photo } });
|
||||
btn.textContent = '✅ Saved';
|
||||
setTimeout(() => openKid(currentStudent.id, 'today'), 600);
|
||||
})
|
||||
);
|
||||
|
||||
document.querySelectorAll('.today-row:not(.offline)').forEach((row) =>
|
||||
row.addEventListener('click', () => {
|
||||
backTarget = () => openKid(currentStudent.id, 'today');
|
||||
@@ -293,6 +385,12 @@ async function openLesson(itemId) {
|
||||
// ---------- assignment ----------
|
||||
|
||||
async function openAssignment(itemId) {
|
||||
evidencePhotoBase64 = null;
|
||||
$('#assignment-photo-status').textContent = '';
|
||||
$('#assignment-photo-input').value = '';
|
||||
$('#assignment-evidence-notes').value = '';
|
||||
$('#assignment-student-note').value = '';
|
||||
|
||||
const item = await api(`/api/items/${itemId}?studentId=${currentStudent.id}`);
|
||||
$('#assignment-kicker').textContent = `${item.course_name} · ${item.unit_name}`;
|
||||
$('#assignment-title').textContent = item.title;
|
||||
@@ -301,15 +399,30 @@ async function openAssignment(itemId) {
|
||||
|
||||
const btn = $('#assignment-done-btn');
|
||||
const status = $('#assignment-status');
|
||||
const commentEl = $('#assignment-comment');
|
||||
const evidenceSection = $('#assignment-evidence-section');
|
||||
const needsEvidence = item.evidence_mode === 'required' || item.evidence_mode === 'optional';
|
||||
|
||||
// Show parent comment if graded
|
||||
if (item.submission?.parent_comment) {
|
||||
commentEl.hidden = false;
|
||||
commentEl.innerHTML = `<strong>Parent feedback:</strong> ${esc(item.submission.parent_comment)}`;
|
||||
} else {
|
||||
commentEl.hidden = true;
|
||||
}
|
||||
|
||||
if (item.submission && item.submission.status === 'graded') {
|
||||
status.hidden = false;
|
||||
status.className = 'status-banner good';
|
||||
status.textContent = `🌟 Graded: ${item.submission.score} / ${item.submission.points_possible}`;
|
||||
evidenceSection.hidden = !needsEvidence || !item.allow_retakes;
|
||||
if (item.allow_retakes) {
|
||||
btn.hidden = false;
|
||||
btn.textContent = '🔁 Retake';
|
||||
btn.onclick = async () => {
|
||||
await api(`/api/items/${itemId}/complete`, { method: 'POST', body: { studentId: currentStudent.id, date: todayStr() } });
|
||||
const body = { studentId: currentStudent.id, date: todayStr() };
|
||||
if (needsEvidence) { body.evidenceNotes = $('#assignment-evidence-notes').value || null; body.evidencePhoto = evidencePhotoBase64; body.studentNote = $('#assignment-student-note').value || null; }
|
||||
await api(`/api/items/${itemId}/complete`, { method: 'POST', body });
|
||||
openAssignment(itemId);
|
||||
};
|
||||
} else {
|
||||
@@ -317,15 +430,23 @@ async function openAssignment(itemId) {
|
||||
}
|
||||
} else if (item.submission && item.submission.status === 'done') {
|
||||
btn.hidden = true;
|
||||
evidenceSection.hidden = true;
|
||||
status.hidden = false;
|
||||
status.className = 'status-banner';
|
||||
status.textContent = `⏳ Turned in — waiting for a parent to grade it.`;
|
||||
} else {
|
||||
btn.hidden = false;
|
||||
btn.textContent = 'Mark as done ✓';
|
||||
btn.textContent = item.evidence_mode === 'required' ? 'Submit with evidence ✓' : 'Mark as done ✓';
|
||||
status.hidden = true;
|
||||
evidenceSection.hidden = !needsEvidence;
|
||||
btn.onclick = async () => {
|
||||
await api(`/api/items/${itemId}/complete`, { method: 'POST', body: { studentId: currentStudent.id, date: todayStr() } });
|
||||
if (item.evidence_mode === 'required' && !$('#assignment-evidence-notes').value && !evidencePhotoBase64) {
|
||||
status.hidden = false; status.className = 'status-banner bad'; status.textContent = 'Evidence required — add notes or a photo first.';
|
||||
return;
|
||||
}
|
||||
const body = { studentId: currentStudent.id, date: todayStr() };
|
||||
if (needsEvidence) { body.evidenceNotes = $('#assignment-evidence-notes').value || null; body.evidencePhoto = evidencePhotoBase64; body.studentNote = $('#assignment-student-note').value || null; }
|
||||
await api(`/api/items/${itemId}/complete`, { method: 'POST', body });
|
||||
openAssignment(itemId);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,8 +73,29 @@
|
||||
<h1 id="assignment-title"></h1>
|
||||
<p id="assignment-points" class="points-label"></p>
|
||||
<div id="assignment-body" class="lesson-body"></div>
|
||||
<div id="assignment-evidence-section" hidden>
|
||||
<details id="assignment-evidence-details" class="evidence-form">
|
||||
<summary>📎 Add evidence (photo or notes)</summary>
|
||||
<div class="evidence-inner">
|
||||
<textarea id="assignment-evidence-notes" placeholder="Notes about your work…" rows="3"></textarea>
|
||||
<div class="evidence-photo-row">
|
||||
<button type="button" id="assignment-photo-btn" class="secondary small">📷 Add photo</button>
|
||||
<input type="file" id="assignment-photo-input" accept="image/*" capture="environment" hidden>
|
||||
<span id="assignment-photo-status" class="hint"></span>
|
||||
</div>
|
||||
<label class="checkbox-label evidence-note-label">
|
||||
<select id="assignment-student-note">
|
||||
<option value="">I felt good about this</option>
|
||||
<option value="needs_help">🙋 I need help with this</option>
|
||||
<option value="not_sure">🤔 I did it but I'm not sure</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<button id="assignment-done-btn" class="check-btn">Mark as done ✓</button>
|
||||
<div id="assignment-status" class="status-banner" hidden></div>
|
||||
<div id="assignment-comment" class="parent-comment" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -416,3 +416,31 @@ td.overdue, .overdue { background: var(--bad-bg) !important; }
|
||||
}
|
||||
.modal-box h3 { margin-bottom: 1rem; }
|
||||
.modal-box .secondary { margin-top: 1rem; }
|
||||
|
||||
/* ---------- evidence forms ---------- */
|
||||
.evidence-form { margin: 0.75rem 0; border: 1.5px dashed var(--accent); border-radius: 10px; padding: 0.75rem; }
|
||||
.evidence-form summary { cursor: pointer; color: var(--accent); font-weight: 600; }
|
||||
.evidence-inner { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem; }
|
||||
.evidence-inner textarea { width: 100%; border: 1.5px solid #d8d2c0; border-radius: 8px; padding: 0.5rem; font-family: inherit; font-size: 1rem; resize: vertical; }
|
||||
.evidence-photo-row { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.evidence-note-label select { width: 100%; padding: 0.4rem 0.6rem; border: 1.5px solid #d8d2c0; border-radius: 8px; font-family: inherit; }
|
||||
.parent-comment { background: var(--good-bg); border-radius: 10px; padding: 0.75rem 1rem; margin: 0.75rem 0; color: var(--good); }
|
||||
.status-banner.bad { background: var(--bad-bg); color: var(--bad); border-radius: 10px; padding: 0.75rem 1rem; margin: 0.75rem 0; }
|
||||
|
||||
/* ---------- offline 3-state tasks ---------- */
|
||||
.today-row-offline { background: var(--card); border: 1.5px solid #e2dccf; border-radius: 14px; padding: 0.75rem 1rem; margin-bottom: 0.5rem; text-align: left; }
|
||||
.offline-main { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.offline-main .row-title { flex: 1; font-weight: 600; }
|
||||
.offline-controls { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
|
||||
.offline-status-btn { padding: 0.3rem 0.75rem; border: 1.5px solid var(--muted); border-radius: 20px; background: transparent; font-size: 0.85rem; cursor: pointer; }
|
||||
.offline-status-btn.active { border-color: var(--accent); background: var(--accent); color: #fff; }
|
||||
.offline-evidence-form { border: none; padding: 0; }
|
||||
.offline-evidence-form summary { font-size: 0.85rem; color: var(--muted); cursor: pointer; }
|
||||
|
||||
/* ---------- grading queue cards ---------- */
|
||||
.grading-card { background: var(--card); border: 1.5px solid #e2dccf; border-radius: 14px; padding: 1rem; margin-bottom: 0.75rem; }
|
||||
.grading-header { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.6rem; }
|
||||
.grading-header .grow { flex: 1; font-weight: 600; }
|
||||
.grading-footer { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-top: 0.6rem; }
|
||||
.comment-input { flex: 1; min-width: 140px; padding: 0.4rem 0.6rem; border: 1.5px solid #d8d2c0; border-radius: 8px; font-family: inherit; }
|
||||
.badge-tag { background: var(--gold); color: #333; border-radius: 20px; padding: 0.2rem 0.6rem; font-size: 0.8rem; font-weight: 600; }
|
||||
Reference in new issue
Block a user