/** Render note cards from application queries and read-only state. */ import { _nextGoalStep, _goalProgress, _hasItems, _standardVisualVariant, _visibleNoteTags } from '../domain/noteRules.js'; import { _hasTimeComponent, _formatRepeatLabel } from '../domain/recurrence.js'; export function createCardRendering({ COLORS, COLOR_HEX, _animateReflow, _applyMasonry, _bindCardEvents, _captureCardPositions, _customColorStyle, _dotBg, _dotIsActive, _esc, _formatDueDate, _formatReminderTag, _hasActiveReminder, _isBgImage, _isDueOverdue, _isNotesMobileMode, _linkify, _loadActiveHighlights, _renderLabelsInto, _renderQuickAdd, _safeImgSrc, _sortedVisibleNotes, _updateRailBadge, _wireTodayView, state, uiModule }) { function _renderLoadingSkeleton() { const body = document.querySelector('#notes-pane .notes-pane-body'); if (!body) return; body.innerHTML = ''; _renderLabelsInto(body); _renderQuickAdd(body); const skel = document.createElement('div'); skel.className = 'notes-skeleton'; skel.innerHTML = `
`; body.appendChild(skel); } function _renderNotes() { _updateRailBadge(); const body = document.querySelector('#notes-pane .notes-pane-body'); if (!body) return; const prevPositions = _captureCardPositions(); const activeReminderHighlights = _loadActiveHighlights(); const sorted = _sortedVisibleNotes(); let html = ''; // Today view: render a compact card listing the next-unchecked step from // each active goal. Tapping a step toggles it done (same idx-based wiring // as regular checkboxes). Tapping the title opens the goal note for full // editing. if (state.activeFilter === 'today') { body.innerHTML = ''; _renderLabelsInto(body); _renderQuickAdd(body); if (sorted.length === 0) { body.insertAdjacentHTML('beforeend', `
All caught up — no pending goal steps right now.
`); } else { let todayHtml = `
Today · one step per goal
`; for (const note of sorted) { const next = _nextGoalStep(note); if (!next) continue; const progress = _goalProgress(note).trim(); todayHtml += `
${_esc(note.title || '(untitled goal)')}
${_linkify(next.item.text || '')}
${_esc(progress)}
`; } todayHtml += `
`; body.insertAdjacentHTML('beforeend', todayHtml); } _wireTodayView(body); return; } for (const note of sorted) { if (state.editingId === note.id) continue; // skip : form is shown instead const borderColor = COLOR_HEX[note.color || ''] || 'var(--border)'; const dueFmt = _formatDueDate(note.due_date); const overdue = _isDueOverdue(note.due_date); let contentHtml = ''; if (_hasItems(note) && Array.isArray(note.items)) { // Goal notes can carry a free-form description above the step list : // todos rarely do, but the same render works for both. if (note.note_type === 'goal' && (note.content || '').trim()) { const fullText = note.content || ''; const preview = fullText.length > 300 ? fullText.slice(0, 300) + '…' : fullText; contentHtml += `
${_esc(preview)}
`; } contentHtml += '
'; // Show ALL items : the preview container is scrollable (CSS caps // its max-height + overflow-y:auto), so there's no need to truncate. for (let i = 0; i < note.items.length; i++) { const item = note.items[i]; const doneClass = item.done ? ' done' : ''; const indent = Math.min(item.indent || 0, 3); contentHtml += `
${_linkify(item.text)}
`; } contentHtml += '
'; } else { const fullText = note.content || ''; const preview = fullText.length > 600 ? fullText.slice(0, 600) + '…' : fullText; // _linkify already calls _esc internally, so URLs become clickable // anchors (used by e.g. the "remind me to reply" email deep-link). contentHtml = preview ? `
${_linkify(preview)}
` : ''; } const isBg = _isBgImage(note.color); const cc = (note.color && !isBg) ? ' note-color-' + note.color : ''; const cardStyle = isBg ? ` style="${_customColorStyle(note.color)}"` : ''; const isStandard = !note.note_type || note.note_type === 'note'; const visualClass = isStandard && !note.color ? ` note-standard note-standard-v${_standardVisualVariant(note)}` : ''; const sel = state.selectedIds.has(note.id) ? ' note-card-selected' : ''; const reminderTagHtml = note.due_date && _hasTimeComponent(note.due_date) ? `
${_esc(_formatReminderTag(note.due_date))}${note.repeat && note.repeat !== 'none' ? ' · ' + _esc(_formatRepeatLabel(note.repeat, new Date(note.due_date))) : ''}
` : ''; const noteTags = _visibleNoteTags(note); const dueBadge = dueFmt && !_hasTimeComponent(note.due_date) ? `${dueFmt}` : ''; const colorDots = COLORS.map(c => ``).join(''); const goalClass = note.note_type === 'goal' ? ' note-card-goal' : ''; const reminderGlowClass = activeReminderHighlights.has(note.id) && _hasActiveReminder(note) ? ' note-card-reminder-fired-sticky' : ''; const goalPill = note.note_type === 'goal' ? ` Goal${_goalProgress(note)} ` : ''; const drawingClass = note.note_type === 'draw' ? ' note-card-drawing' : ''; html += `
${state.selectMode ? `` : ''} ${goalPill} ${state.showingArchived ? ` ` : ` ${_hasItems(note) ? `` : ''}`}
${_esc(note.title || '')}
${dueBadge}
${_safeImgSrc(note.image_url) ? `` : ''} ${contentHtml} ${_hasItems(note) ? `
` : ''} ${reminderTagHtml} ${noteTags.length ? `
${noteTags.map(t => ``).join(' ')}
` : ''}
${colorDots}
${state.showingArchived ? ` ` : ` ${_hasItems(note) ? ` ` : ''} `}
`; } // Always render quick-add at top (collapsed unless user is typing) const existingForm = body.querySelector('.note-form'); if (existingForm && state.editingId === '__new__') { // Keep the expanded form, replace cards after it const next = [...body.children].filter(c => c !== existingForm); next.forEach(c => c.remove()); if (sorted.length === 0) { body.insertAdjacentHTML('beforeend', '
No notes ' + uiModule.emptyStateIcon('smiley') + '
'); } else { existingForm.insertAdjacentHTML('afterend', html); } } else { body.innerHTML = ''; _renderLabelsInto(body); _renderQuickAdd(body); if (sorted.length === 0) { body.insertAdjacentHTML('beforeend', '
No notes yet ' + uiModule.emptyStateIcon('smiley') + '
'); } else { body.insertAdjacentHTML('beforeend', html); } } _bindCardEvents(body); _animateReflow(prevPositions); _applyMasonry(body); } return { _renderLoadingSkeleton, _renderNotes }; }