diff --git a/apps/total-recall/app.js b/apps/total-recall/app.js index 55fdade..338a527 100644 --- a/apps/total-recall/app.js +++ b/apps/total-recall/app.js @@ -8,6 +8,20 @@ const saveButton = document.querySelector('[data-save]'); const clearButton = document.querySelector('[data-clear]'); const statusEl = document.querySelector('[data-status]'); + const deckSelect = document.querySelector('[data-deck-select]'); + const addDeckButton = document.querySelector('[data-add-deck]'); + const deckStatsEl = document.querySelector('[data-deck-stats]'); + const deckNameInput = document.querySelector('[data-deck-name-input]'); + const renameDeckButton = document.querySelector('[data-rename-deck]'); + const deleteDeckButton = document.querySelector('[data-delete-deck]'); + const deckListEl = document.querySelector('[data-deck-list]'); + const activeDeckNameEl = document.querySelector('[data-active-deck-name]'); + const browserPanel = document.querySelector('[data-browser-panel]'); + const browserScopeSelect = document.querySelector('[data-browser-scope]'); + const browserSearchInput = document.querySelector('[data-browser-search]'); + const browserCountEl = document.querySelector('[data-browser-count]'); + const browserResultsEl = document.querySelector('[data-browser-results]'); + const browserEmptyEl = document.querySelector('[data-browser-empty]'); if ( !notesInput || @@ -18,22 +32,43 @@ !shuffleButton || !saveButton || !clearButton || - !statusEl + !statusEl || + !deckSelect || + !addDeckButton || + !deckStatsEl || + !deckNameInput || + !renameDeckButton || + !deleteDeckButton || + !deckListEl || + !activeDeckNameEl || + !browserPanel || + !browserScopeSelect || + !browserSearchInput || + !browserCountEl || + !browserResultsEl || + !browserEmptyEl ) { return; } - const STORAGE_KEY = 'total-recall-notes'; - const STATE_KEY = 'total-recall-state'; + const STORAGE_KEYS = { + collection: 'total-recall-deck-collection', + legacyNotes: 'total-recall-notes', + legacyState: 'total-recall-state' + }; + const DEFAULT_NOTES = [ "Why don't scientists trust atoms? Because they make up everything.", "I told my computer I needed a break, and it said 'No problem — I'll go to sleep.'", 'Why did the scarecrow get a promotion? He was outstanding in his field.' ].join('\n\n'); - let entries = []; - let deckOrder = []; - let index = 0; + const DEFAULT_DECK_NAME = 'Starter deck'; + + let decks = []; + let activeDeckId = ''; + let browserQuery = ''; + let browserScope = 'deck'; function setStatus(message) { statusEl.textContent = message; @@ -75,6 +110,14 @@ .filter((entry) => entry.length > 0); } + function createSequentialOrder(length) { + const order = []; + for (let i = 0; i < length; i += 1) { + order.push(i); + } + return order; + } + function shuffle(list) { const copy = list.slice(); for (let i = copy.length - 1; i > 0; i -= 1) { @@ -84,10 +127,6 @@ return copy; } - function createSequentialDeck() { - return entries.map((_, entryIndex) => entryIndex); - } - function clampIndex(value, length) { if (length <= 0) { return 0; @@ -110,8 +149,8 @@ return integer; } - function sanitizeStoredDeck(order) { - if (!Array.isArray(order) || order.length !== entries.length) { + function sanitizeOrder(order, length) { + if (!Array.isArray(order) || order.length !== length) { return null; } @@ -120,7 +159,7 @@ for (let i = 0; i < order.length; i += 1) { const value = Number(order[i]); - if (!Number.isInteger(value) || value < 0 || value >= entries.length || seen.has(value)) { + if (!Number.isInteger(value) || value < 0 || value >= length || seen.has(value)) { return null; } seen.add(value); @@ -130,49 +169,205 @@ return sanitized; } - function loadState() { - const raw = safeGetItem(STATE_KEY); - if (typeof raw !== 'string' || raw.length === 0) { + function sanitizeState(state, length) { + if (!length) { + return { order: [], index: 0 }; + } + + if (!state || typeof state !== 'object') { + return { order: createSequentialOrder(length), index: 0 }; + } + + const rawOrder = Array.isArray(state.order) ? state.order : state.deck; + const order = sanitizeOrder(rawOrder, length) || createSequentialOrder(length); + const index = clampIndex(state.index, order.length); + return { order, index }; + } + + function createDeckId() { + return `deck-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + } + + function createDeck(name, notes) { + const deckName = typeof name === 'string' && name.trim() ? name.trim() : DEFAULT_DECK_NAME; + const safeNotes = typeof notes === 'string' ? notes : ''; + const entries = parseNotes(safeNotes); + const state = sanitizeState(null, entries.length); + return { + id: createDeckId(), + name: deckName, + notes: safeNotes, + entries, + state + }; + } + + function sanitizeDeck(rawDeck) { + if (!rawDeck || typeof rawDeck !== 'object') { + return null; + } + + const id = typeof rawDeck.id === 'string' && rawDeck.id.trim() ? rawDeck.id.trim() : createDeckId(); + const name = typeof rawDeck.name === 'string' && rawDeck.name.trim() ? rawDeck.name.trim() : DEFAULT_DECK_NAME; + const notes = typeof rawDeck.notes === 'string' ? rawDeck.notes : ''; + const entries = parseNotes(notes); + const state = sanitizeState(rawDeck.state, entries.length); + return { id, name, notes, entries, state }; + } + + function loadLegacyState() { + const raw = safeGetItem(STORAGE_KEYS.legacyState); + if (typeof raw !== 'string' || !raw.length) { return null; } try { return JSON.parse(raw); } catch (error) { - console.error('Failed to parse saved deck state', error); // eslint-disable-line no-console + console.error('Failed to parse legacy deck state', error); // eslint-disable-line no-console return null; } } - function saveState() { - if (!entries.length || deckOrder.length !== entries.length) { - safeRemoveItem(STATE_KEY); - return; + function migrateLegacyCollection() { + const legacyNotes = safeGetItem(STORAGE_KEYS.legacyNotes); + const notes = typeof legacyNotes === 'string' && legacyNotes.length ? legacyNotes : DEFAULT_NOTES; + const deck = createDeck(DEFAULT_DECK_NAME, notes); + const legacyState = loadLegacyState(); + deck.state = sanitizeState(legacyState, deck.entries.length); + + // Clean up legacy keys so the new structure is the source of truth. + safeRemoveItem(STORAGE_KEYS.legacyNotes); + safeRemoveItem(STORAGE_KEYS.legacyState); + + return { + decks: [deck], + activeDeckId: deck.id + }; + } + + function loadCollection() { + const raw = safeGetItem(STORAGE_KEYS.collection); + if (typeof raw === 'string' && raw.length) { + try { + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.decks)) { + return migrateLegacyCollection(); + } + + const sanitizedDecks = parsed.decks + .map((deck) => sanitizeDeck(deck)) + .filter(Boolean); + + if (!sanitizedDecks.length) { + return migrateLegacyCollection(); + } + + let nextActive = typeof parsed.activeDeckId === 'string' ? parsed.activeDeckId : ''; + if (!sanitizedDecks.some((deck) => deck.id === nextActive)) { + nextActive = sanitizedDecks[0].id; + } + + return { + decks: sanitizedDecks, + activeDeckId: nextActive + }; + } catch (error) { + console.error('Failed to parse stored deck collection', error); // eslint-disable-line no-console + return migrateLegacyCollection(); + } } - const payload = JSON.stringify({ deck: deckOrder, index }); - safeSetItem(STATE_KEY, payload); + return migrateLegacyCollection(); + } + + function saveCollection() { + const payload = { + decks: decks.map((deck) => ({ + id: deck.id, + name: deck.name, + notes: deck.notes, + state: { + order: deck.state.order.slice(), + index: deck.state.index + } + })), + activeDeckId + }; + + safeSetItem(STORAGE_KEYS.collection, JSON.stringify(payload)); } - function ensureDeck() { - if (!entries.length) { - deckOrder = []; - index = 0; + function getActiveDeck() { + return decks.find((deck) => deck.id === activeDeckId) || null; + } + + function ensureDeckState(deck) { + if (!deck) { + return; + } + + const length = deck.entries.length; + if (!length) { + deck.state = { order: [], index: 0 }; + return; + } + + const sanitizedOrder = sanitizeOrder(deck.state.order, length); + if (!sanitizedOrder) { + deck.state = { order: createSequentialOrder(length), index: 0 }; return; } - const expectedLength = entries.length; - if (deckOrder.length !== expectedLength) { - deckOrder = createSequentialDeck(); - index = 0; - saveState(); + deck.state = { + order: sanitizedOrder, + index: clampIndex(deck.state.index, sanitizedOrder.length) + }; + } + + function updateDeckEntries(deck, rawNotes) { + if (!deck) { return; } - if (index < 0 || index >= deckOrder.length) { - index = clampIndex(index, deckOrder.length); - saveState(); + const notes = typeof rawNotes === 'string' ? rawNotes : ''; + const entries = parseNotes(notes); + deck.notes = notes; + deck.entries = entries; + deck.state = sanitizeState(deck.state, entries.length); + } + + function syncActiveDeckNotesFromInput() { + const deck = getActiveDeck(); + if (!deck) { + return false; + } + + const raw = notesInput.value; + if (raw === deck.notes) { + return false; + } + + updateDeckEntries(deck, raw); + saveCollection(); + return true; + } + + function generateDeckName() { + const base = 'New deck'; + if (!decks.some((deck) => deck.name === base)) { + return base; + } + + let counter = 2; + while (decks.some((deck) => deck.name === `${base} ${counter}`)) { + counter += 1; } + return `${base} ${counter}`; + } + + function formatCount(count, singular) { + return count === 1 ? `1 ${singular}` : `${count} ${singular}s`; } function renderEmptyState() { @@ -184,142 +379,560 @@ shuffleButton.disabled = true; } - function render() { - if (!entries.length) { + function renderActiveDeck() { + const deck = getActiveDeck(); + if (!deck) { renderEmptyState(); + activeDeckNameEl.textContent = 'No deck'; return; } - ensureDeck(); + ensureDeckState(deck); + + activeDeckNameEl.textContent = deck.name; - if (!deckOrder.length) { + if (!deck.entries.length || !deck.state.order.length) { renderEmptyState(); return; } - const currentEntryIndex = deckOrder[index]; - const current = entries[currentEntryIndex]; + const currentEntryIndex = deck.state.order[deck.state.index]; + const current = deck.entries[currentEntryIndex]; currentEntryEl.textContent = current; currentEntryEl.classList.remove('is-empty'); - counterEl.textContent = `${index + 1} of ${deckOrder.length}`; - const disableNav = deckOrder.length <= 1; + counterEl.textContent = `${deck.state.index + 1} of ${deck.state.order.length}`; + const disableNav = deck.state.order.length <= 1; prevButton.disabled = disableNav; nextButton.disabled = disableNav; - shuffleButton.disabled = deckOrder.length <= 1; + shuffleButton.disabled = deck.state.order.length <= 1; + } + + function renderDeckSelect() { + const previousValue = deckSelect.value; + deckSelect.innerHTML = ''; + const fragment = document.createDocumentFragment(); + + decks.forEach((deck) => { + const option = document.createElement('option'); + option.value = deck.id; + option.textContent = `${deck.name} (${formatCount(deck.entries.length, 'card')})`; + fragment.appendChild(option); + }); + + deckSelect.appendChild(fragment); + if (decks.some((deck) => deck.id === previousValue)) { + deckSelect.value = previousValue; + } else { + deckSelect.value = activeDeckId; + } + } + function renderDeckStats() { + const totalDecks = decks.length; + const totalCards = decks.reduce((sum, deck) => sum + deck.entries.length, 0); + const decksText = formatCount(totalDecks, 'deck'); + const cardsText = formatCount(totalCards, 'card'); + deckStatsEl.textContent = `${decksText} · ${cardsText} total`; } - function showNext() { - if (!entries.length) { + function renderDeckList() { + deckListEl.innerHTML = ''; + + if (!decks.length) { + const empty = document.createElement('p'); + empty.className = 'browser__empty'; + empty.textContent = 'No decks yet. Add one above to get started.'; + deckListEl.appendChild(empty); return; } - ensureDeck(); - if (!deckOrder.length) { + + const fragment = document.createDocumentFragment(); + + decks.forEach((deck) => { + ensureDeckState(deck); + const item = document.createElement('article'); + item.className = 'deck-list__item'; + + const textContainer = document.createElement('div'); + textContainer.className = 'deck-list__text'; + + const title = document.createElement('h3'); + title.className = 'deck-list__title'; + title.textContent = deck.name; + if (deck.id === activeDeckId) { + const badge = document.createElement('span'); + badge.className = 'deck-list__badge'; + badge.textContent = 'Active'; + title.appendChild(document.createTextNode(' ')); + title.appendChild(badge); + } + + const meta = document.createElement('p'); + meta.className = 'deck-list__meta'; + if (!deck.entries.length) { + meta.textContent = 'No cards yet.'; + } else { + const progressIndex = deck.state.index + 1; + meta.textContent = `${formatCount(deck.entries.length, 'card')} · Next card ${progressIndex} of ${deck.state.order.length}`; + } + + textContainer.appendChild(title); + textContainer.appendChild(meta); + + const actions = document.createElement('div'); + actions.className = 'deck-list__actions'; + + if (deck.id !== activeDeckId) { + const openButton = document.createElement('button'); + openButton.type = 'button'; + openButton.className = 'control-button control-button--secondary'; + openButton.textContent = 'Switch to deck'; + openButton.dataset.action = 'activate'; + openButton.dataset.deckId = deck.id; + actions.appendChild(openButton); + } + + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'control-button control-button--danger'; + deleteButton.textContent = 'Delete'; + deleteButton.dataset.action = 'delete'; + deleteButton.dataset.deckId = deck.id; + actions.appendChild(deleteButton); + + item.appendChild(textContainer); + item.appendChild(actions); + fragment.appendChild(item); + }); + + deckListEl.appendChild(fragment); + } + + function highlightMatches(text, query) { + const fragment = document.createDocumentFragment(); + if (!query) { + fragment.appendChild(document.createTextNode(text)); + return fragment; + } + + const lowerText = text.toLowerCase(); + const lowerQuery = query.toLowerCase(); + let startIndex = 0; + + while (startIndex < text.length) { + const matchIndex = lowerText.indexOf(lowerQuery, startIndex); + if (matchIndex === -1) { + fragment.appendChild(document.createTextNode(text.slice(startIndex))); + break; + } + + if (matchIndex > startIndex) { + fragment.appendChild(document.createTextNode(text.slice(startIndex, matchIndex))); + } + + const mark = document.createElement('mark'); + mark.textContent = text.slice(matchIndex, matchIndex + lowerQuery.length); + fragment.appendChild(mark); + + startIndex = matchIndex + lowerQuery.length; + } + + return fragment; + } + + function getBrowseSource() { + if (!decks.length) { + return []; + } + + if (browserScope === 'deck') { + const deck = getActiveDeck(); + if (!deck || !deck.entries.length) { + return []; + } + + return deck.entries.map((text, index) => ({ + deckId: deck.id, + deckName: deck.name, + text, + index + })); + } + + const items = []; + decks.forEach((deck) => { + deck.entries.forEach((text, index) => { + items.push({ + deckId: deck.id, + deckName: deck.name, + text, + index + }); + }); + }); + return items; + } + + function renderBrowserResults() { + const source = getBrowseSource(); + browserResultsEl.innerHTML = ''; + + if (!source.length) { + browserEmptyEl.hidden = false; + browserEmptyEl.textContent = 'No cards to browse yet. Add notes to your decks.'; + browserCountEl.textContent = 'Showing 0 cards'; return; } - index = (index + 1) % deckOrder.length; - saveState(); - render(); + + const query = browserQuery.trim(); + const lowerQuery = query.toLowerCase(); + const filtered = query + ? source.filter((item) => item.text.toLowerCase().includes(lowerQuery)) + : source; + + if (!filtered.length) { + browserEmptyEl.hidden = false; + browserEmptyEl.textContent = 'No cards match your search.'; + } else { + browserEmptyEl.hidden = true; + } + + if (!filtered.length) { + browserCountEl.textContent = `Showing 0 of ${source.length} cards`; + return; + } + + const fragment = document.createDocumentFragment(); + filtered.forEach((item) => { + const listItem = document.createElement('li'); + listItem.className = 'browser__item'; + + const deckEl = document.createElement('p'); + deckEl.className = 'browser__deck'; + deckEl.textContent = `${item.deckName} • Card ${item.index + 1}`; + + const textEl = document.createElement('p'); + textEl.className = 'browser__text'; + textEl.appendChild(highlightMatches(item.text, query)); + + listItem.appendChild(deckEl); + listItem.appendChild(textEl); + fragment.appendChild(listItem); + }); + + browserResultsEl.appendChild(fragment); + + if (query) { + browserCountEl.textContent = `Showing ${filtered.length} of ${source.length} cards`; + } else { + browserCountEl.textContent = `Showing ${filtered.length} cards`; + } + } + + function syncInputsWithActiveDeck() { + const deck = getActiveDeck(); + if (!deck) { + notesInput.value = ''; + deckNameInput.value = ''; + return; + } + + notesInput.value = deck.notes; + deckNameInput.value = deck.name; + } + + function refreshAll() { + renderDeckSelect(); + renderDeckStats(); + syncInputsWithActiveDeck(); + renderActiveDeck(); + renderDeckList(); + browserScope = browserScopeSelect.value === 'all' ? 'all' : 'deck'; + browserQuery = browserSearchInput.value || ''; + renderBrowserResults(); + } + + function handleShowNext() { + const deck = getActiveDeck(); + if (!deck || !deck.entries.length) { + return; + } + ensureDeckState(deck); + if (!deck.state.order.length) { + return; + } + deck.state.index = (deck.state.index + 1) % deck.state.order.length; + saveCollection(); + renderActiveDeck(); + renderDeckList(); } - function showPrev() { - if (!entries.length) { + function handleShowPrev() { + const deck = getActiveDeck(); + if (!deck || !deck.entries.length) { return; } - ensureDeck(); - if (!deckOrder.length) { + ensureDeckState(deck); + if (!deck.state.order.length) { return; } - index = (index - 1 + deckOrder.length) % deckOrder.length; - saveState(); - render(); + deck.state.index = (deck.state.index - 1 + deck.state.order.length) % deck.state.order.length; + saveCollection(); + renderActiveDeck(); + renderDeckList(); } - function reshuffleDeck() { - if (entries.length <= 1) { + function handleShuffle() { + const deck = getActiveDeck(); + if (!deck || deck.entries.length <= 1) { return; } - deckOrder = shuffle(createSequentialDeck()); - index = 0; - saveState(); - render(); + deck.state.order = shuffle(createSequentialOrder(deck.entries.length)); + deck.state.index = 0; + saveCollection(); + renderActiveDeck(); + renderDeckList(); setStatus('Deck reshuffled.'); } - function updateEntriesFrom(raw, storedState = null) { - entries = parseNotes(raw); + function handleSaveNotes() { + const changed = syncActiveDeckNotesFromInput(); + const deck = getActiveDeck(); + if (!deck) { + setStatus('No deck selected.'); + return; + } + + renderActiveDeck(); + renderDeckSelect(); + renderDeckStats(); + renderDeckList(); + renderBrowserResults(); + + if (!deck.entries.length) { + setStatus('Saved. Add notes to build your deck.'); + return; + } - if (!entries.length) { - deckOrder = []; - index = 0; - render(); - safeRemoveItem(STATE_KEY); + if (!changed) { + setStatus('No changes to save.'); return; } - let nextDeck = createSequentialDeck(); - let nextIndex = 0; + const count = deck.entries.length; + setStatus(count === 1 ? 'Saved 1 card.' : `Saved ${count} cards.`); + } - if (storedState && typeof storedState === 'object') { - const sanitizedDeck = sanitizeStoredDeck(storedState.deck); - if (sanitizedDeck) { - nextDeck = sanitizedDeck; - nextIndex = clampIndex(storedState.index, nextDeck.length); - } + function handleClearNotes() { + const deck = getActiveDeck(); + if (!deck) { + setStatus('No deck selected.'); + return; + } + + notesInput.value = ''; + updateDeckEntries(deck, ''); + saveCollection(); + renderActiveDeck(); + renderDeckSelect(); + renderDeckStats(); + renderDeckList(); + renderBrowserResults(); + setStatus('Cleared notes. Add new entries to continue.'); + } + + function handleDeckSelectionChange(event) { + const nextDeckId = event.target.value; + if (!nextDeckId || nextDeckId === activeDeckId) { + return; + } + + syncActiveDeckNotesFromInput(); + if (!decks.some((deck) => deck.id === nextDeckId)) { + deckSelect.value = activeDeckId; + return; } - deckOrder = nextDeck; - index = nextIndex; - render(); - saveState(); + activeDeckId = nextDeckId; + saveCollection(); + refreshAll(); + setStatus(`Switched to ${getActiveDeck().name}.`); } - function handleSave() { - const raw = notesInput.value; - safeSetItem(STORAGE_KEY, raw); - updateEntriesFrom(raw); - const count = entries.length; - if (count === 0) { - setStatus('Saved. Add notes to build your deck.'); - } else if (count === 1) { - setStatus('Saved 1 card.'); + function handleAddDeck() { + syncActiveDeckNotesFromInput(); + const name = generateDeckName(); + const deck = createDeck(name, ''); + decks.push(deck); + activeDeckId = deck.id; + saveCollection(); + refreshAll(); + setStatus('Created a new deck. Start adding cards below.'); + } + + function handleRenameDeck() { + const deck = getActiveDeck(); + if (!deck) { + setStatus('No deck selected.'); + return; + } + + const nextName = deckNameInput.value.trim(); + if (!nextName) { + setStatus('Enter a name to save.'); + deckNameInput.value = deck.name; + deckNameInput.focus(); + return; + } + + if (nextName === deck.name) { + setStatus('Name unchanged.'); + return; + } + + deck.name = nextName; + saveCollection(); + refreshAll(); + setStatus('Deck renamed.'); + } + + function removeDeckById(id) { + const index = decks.findIndex((deck) => deck.id === id); + if (index === -1) { + return false; + } + decks.splice(index, 1); + return true; + } + + function selectFallbackDeck() { + if (decks.length) { + activeDeckId = decks[0].id; } else { - setStatus(`Saved ${count} cards.`); + const deck = createDeck(DEFAULT_DECK_NAME, DEFAULT_NOTES); + decks.push(deck); + activeDeckId = deck.id; } } - function handleClear() { - notesInput.value = ''; - safeSetItem(STORAGE_KEY, ''); - updateEntriesFrom(''); - setStatus('Cleared notes. Add new entries to continue.'); + function handleDeleteDeck(deckId) { + if (decks.length <= 1) { + setStatus('Keep at least one deck.'); + return; + } + + const targetDeck = decks.find((deck) => deck.id === deckId); + if (!targetDeck) { + setStatus('Deck not found.'); + return; + } + + const confirmed = window.confirm(`Delete "${targetDeck.name}"? This cannot be undone.`); + if (!confirmed) { + return; + } + + removeDeckById(deckId); + if (activeDeckId === deckId) { + selectFallbackDeck(); + } + + saveCollection(); + refreshAll(); + setStatus('Deck deleted.'); + } + + function handleDeleteCurrentDeck() { + const deck = getActiveDeck(); + if (!deck) { + setStatus('No deck selected.'); + return; + } + handleDeleteDeck(deck.id); } - prevButton.addEventListener('click', showPrev); - nextButton.addEventListener('click', showNext); - shuffleButton.addEventListener('click', reshuffleDeck); - saveButton.addEventListener('click', handleSave); - clearButton.addEventListener('click', handleClear); + function handleDeckListAction(event) { + const button = event.target.closest('button'); + if (!button) { + return; + } - document.addEventListener('keydown', (event) => { - if (event.defaultPrevented) { + const { action, deckId } = button.dataset; + if (!deckId) { return; } - if (event.key === 'ArrowRight') { - event.preventDefault(); - showNext(); - } else if (event.key === 'ArrowLeft') { - event.preventDefault(); - showPrev(); + if (action === 'activate') { + if (deckId === activeDeckId) { + return; + } + syncActiveDeckNotesFromInput(); + if (!decks.some((deck) => deck.id === deckId)) { + return; + } + activeDeckId = deckId; + saveCollection(); + refreshAll(); + setStatus(`Switched to ${getActiveDeck().name}.`); + } else if (action === 'delete') { + handleDeleteDeck(deckId); } - }); + } + + function handleBrowserScopeChange(event) { + browserScope = event.target.value === 'all' ? 'all' : 'deck'; + renderBrowserResults(); + } + + function handleBrowserSearch(event) { + browserQuery = event.target.value; + renderBrowserResults(); + } + + function initializeKeyboardShortcuts() { + document.addEventListener('keydown', (event) => { + if (event.defaultPrevented) { + return; + } + + if (event.key === 'ArrowRight') { + event.preventDefault(); + handleShowNext(); + } else if (event.key === 'ArrowLeft') { + event.preventDefault(); + handleShowPrev(); + } + }); + } + + function initialize() { + const initial = loadCollection(); + decks = initial.decks; + activeDeckId = initial.activeDeckId; + + refreshAll(); + setStatus(''); + + prevButton.addEventListener('click', handleShowPrev); + nextButton.addEventListener('click', handleShowNext); + shuffleButton.addEventListener('click', handleShuffle); + saveButton.addEventListener('click', handleSaveNotes); + clearButton.addEventListener('click', handleClearNotes); + deckSelect.addEventListener('change', handleDeckSelectionChange); + addDeckButton.addEventListener('click', handleAddDeck); + renameDeckButton.addEventListener('click', handleRenameDeck); + deleteDeckButton.addEventListener('click', handleDeleteCurrentDeck); + deckListEl.addEventListener('click', handleDeckListAction); + browserScopeSelect.addEventListener('change', handleBrowserScopeChange); + browserSearchInput.addEventListener('input', handleBrowserSearch); + browserPanel.addEventListener('toggle', () => { + if (browserPanel.open) { + browserSearchInput.focus(); + } + }); + initializeKeyboardShortcuts(); + } - const storedNotes = safeGetItem(STORAGE_KEY); - const initialNotes = storedNotes !== null ? storedNotes : DEFAULT_NOTES; - notesInput.value = initialNotes; - setStatus(''); - const storedDeckState = loadState(); - updateEntriesFrom(initialNotes, storedDeckState); + initialize(); })(); diff --git a/apps/total-recall/index.html b/apps/total-recall/index.html index 59730e6..cabce60 100644 --- a/apps/total-recall/index.html +++ b/apps/total-recall/index.html @@ -134,6 +134,8 @@ .card__meta { display: flex; align-items: center; + gap: 10px; + flex-wrap: wrap; color: var(--text-secondary); font-weight: 600; letter-spacing: 0.04em; @@ -141,6 +143,17 @@ font-size: 0.8rem; } + .card__deck-name { + color: var(--text-primary); + letter-spacing: normal; + text-transform: none; + font-size: 0.85rem; + } + + .card__meta-separator { + opacity: 0.6; + } + .card__body { display: flex; flex-direction: column; @@ -160,6 +173,66 @@ font-size: clamp(0.95rem, 1.2vw + 0.85rem, 1.1rem); } + .editor__section { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + background: var(--surface); + border-radius: 20px; + border: 1px solid var(--surface-border); + box-shadow: var(--surface-shadow); + } + + .deck-manager { + display: flex; + flex-wrap: wrap; + gap: 14px; + align-items: center; + } + + .deck-manager__field { + display: flex; + flex-direction: column; + gap: 6px; + } + + .deck-manager__label { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + } + + .deck-manager__select { + appearance: none; + border-radius: 999px; + border: 1px solid var(--control-border); + background: rgba(255, 255, 255, 0.94); + color: inherit; + padding: 10px 18px; + font-size: 1rem; + font-family: inherit; + min-width: 200px; + box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.08); + } + + @media (prefers-color-scheme: dark) { + .deck-manager__select { + background: rgba(12, 19, 38, 0.92); + } + } + + .deck-manager__select:focus-visible { + outline: 2px solid rgba(74, 99, 255, 0.45); + outline-offset: 2px; + } + + .deck-manager__stats { + margin: 0; + font-size: 0.95rem; + color: var(--text-secondary); + } + .controls { display: flex; flex-wrap: wrap; @@ -208,6 +281,30 @@ box-shadow: 0 0 0 3px rgba(74, 99, 255, 0.2); } + .control-button--danger { + background: rgba(244, 63, 94, 0.12); + color: #be123c; + border-color: rgba(244, 63, 94, 0.35); + } + + .control-button--danger:hover { + background: rgba(244, 63, 94, 0.18); + box-shadow: 0 12px 28px rgba(244, 63, 94, 0.2); + transform: translateY(-1px); + } + + @media (prefers-color-scheme: dark) { + .control-button--danger { + background: rgba(244, 114, 182, 0.16); + color: #fca5a5; + border-color: rgba(244, 114, 182, 0.38); + } + + .control-button--danger:hover { + background: rgba(244, 114, 182, 0.24); + } + } + .control-button:disabled { opacity: 0.5; cursor: not-allowed; @@ -311,12 +408,263 @@ gap: 12px; } + .editor__field { + display: flex; + flex-direction: column; + gap: 6px; + } + + .editor__label { + font-size: 0.9rem; + font-weight: 600; + color: var(--text-secondary); + } + + .editor__input { + border-radius: 999px; + border: 1px solid var(--control-border); + padding: 12px 16px; + font-size: 1rem; + font-family: inherit; + background: rgba(255, 255, 255, 0.94); + color: inherit; + } + + @media (prefers-color-scheme: dark) { + .editor__input { + background: rgba(12, 19, 38, 0.92); + } + } + + .editor__input:focus-visible { + outline: 2px solid rgba(74, 99, 255, 0.45); + outline-offset: 2px; + } + .editor__status { font-size: 0.95rem; color: var(--text-secondary); min-height: 1.2em; } + .deck-list { + display: grid; + gap: 12px; + } + + .deck-list__item { + border: 1px solid var(--control-border); + border-radius: 16px; + padding: 14px 16px; + display: flex; + justify-content: space-between; + gap: 16px; + align-items: center; + background: rgba(255, 255, 255, 0.9); + } + + @media (prefers-color-scheme: dark) { + .deck-list__item { + background: rgba(12, 19, 38, 0.9); + } + } + + .deck-list__text { + display: flex; + flex-direction: column; + gap: 4px; + } + + .deck-list__title { + margin: 0; + font-size: 1rem; + font-weight: 600; + } + + .deck-list__meta { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; + } + + .deck-list__badge { + display: inline-flex; + align-items: center; + border-radius: 999px; + background: var(--accent-soft); + color: var(--accent); + padding: 2px 10px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .deck-list__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .browser { + border-radius: 22px; + border: 1px solid var(--editor-border); + background: var(--editor-background); + box-shadow: 0 20px 48px rgba(15, 23, 42, 0.14); + padding: 0; + overflow: hidden; + } + + .browser[open] { + box-shadow: 0 24px 54px rgba(15, 23, 42, 0.18); + } + + .browser__summary { + list-style: none; + cursor: pointer; + margin: 0; + padding: 18px 24px; + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; + } + + .browser__summary::-webkit-details-marker { + display: none; + } + + .browser__summary::after { + content: "▾"; + font-size: 1.1rem; + color: var(--text-secondary); + } + + details[open] .browser__summary::after { + content: "▴"; + } + + .browser__summary:focus-visible { + outline: 2px solid rgba(74, 99, 255, 0.4); + outline-offset: 4px; + } + + .browser__content { + padding: 0 24px 24px; + display: flex; + flex-direction: column; + gap: 16px; + } + + .browser__controls { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + } + + .browser__scope { + border-radius: 999px; + border: 1px solid var(--control-border); + padding: 10px 16px; + font-family: inherit; + font-size: 0.95rem; + background: rgba(255, 255, 255, 0.94); + color: inherit; + } + + @media (prefers-color-scheme: dark) { + .browser__scope { + background: rgba(12, 19, 38, 0.92); + } + } + + .browser__scope:focus-visible { + outline: 2px solid rgba(74, 99, 255, 0.4); + outline-offset: 2px; + } + + .browser__search { + flex: 1 1 220px; + border-radius: 999px; + border: 1px solid var(--control-border); + padding: 12px 18px; + font-size: 1rem; + font-family: inherit; + background: rgba(255, 255, 255, 0.94); + color: inherit; + } + + @media (prefers-color-scheme: dark) { + .browser__search { + background: rgba(12, 19, 38, 0.92); + } + } + + .browser__search:focus-visible { + outline: 2px solid rgba(74, 99, 255, 0.4); + outline-offset: 2px; + } + + .browser__count { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; + } + + .browser__results { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 12px; + } + + .browser__item { + border-radius: 16px; + border: 1px solid var(--control-border); + padding: 14px 18px; + background: rgba(255, 255, 255, 0.92); + display: flex; + flex-direction: column; + gap: 6px; + } + + @media (prefers-color-scheme: dark) { + .browser__item { + background: rgba(12, 19, 38, 0.92); + } + } + + .browser__deck { + margin: 0; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + } + + .browser__text { + margin: 0; + font-size: 1rem; + line-height: 1.5; + white-space: pre-line; + } + + .browser__empty { + margin: 0; + color: var(--text-secondary); + font-size: 0.95rem; + } + + mark { + background: rgba(74, 99, 255, 0.2); + color: inherit; + padding: 0 2px; + border-radius: 4px; + } + @media (max-width: 720px) { main { gap: 28px; @@ -360,6 +708,8 @@
+ No deck selected + 0 of 0
@@ -378,7 +728,48 @@
Deck settings & raw notes
+
+
+
+ + +
+ +
+

0 decks · 0 cards total

+
+
+ Browse all cards +
+
+ + + + +

Showing 0 cards

+
+ +
    +
    +

    Separate every entry with an empty line. Your notes stay in this browser.

    +
    + + +
    +
    + + +
    @@ -394,6 +785,7 @@ Clear notes
    +