Back
Lesson 13

Wrapping Up Our Web Extension

View Source Code

Overview

Apply final polish to TabNotes before store submission - fixing UX issues, improving accessibility, and adding quality-of-life features. This lesson covers the small details that separate amateur extensions from professional ones.

By the end, TabNotes will be store-ready with polished interactions and professional user experience.

What You’ll Learn

  • Auto-focus textarea for better UX
  • Handle empty states gracefully
  • Add keyboard shortcuts
  • Improve error messages
  • Polish visual details
  • Test edge cases
  • Prepare for production

Auto-Focus Textarea

Problem: Users must click textarea before typing.

Fix: Auto-focus on popup open:

// entrypoints/popup/main.ts
const loadExistingNote = async () => {
  const [tab] = await browser.tabs.query({ 
    active: true, 
    currentWindow: true 
  });
  
  if (!tab || !tab.url) return;
  
  const pageKey = getPageKey(tab.url);
  const notesStorage = await storage.getItem('sync:notesStorage');
  
  if (notesStorage?.[pageKey]) {
    noteText.value = notesStorage[pageKey].note;
  }
  
  // Auto-focus for immediate typing
  noteText.focus();
};

loadExistingNote();

Test: Open popup → Cursor blinks in textarea immediately


Empty State Handling

Show placeholder when no notes exist:

const loadExistingNote = async () => {
  // ... existing code ...
  
  if (notesStorage?.[pageKey]) {
    noteText.value = notesStorage[pageKey].note;
  } else {
    noteText.placeholder = 'Start typing your note...';
  }
  
  noteText.focus();
};

Notes List Empty State

Improve messaging when no notes exist:

// entrypoints/notes-list.ts
if (Object.keys(notesStorage).length === 0) {
  container.innerHTML = `
    <div class="empty-state">
      <h2>No notes yet</h2>
      <p>Visit any website and click the extension icon to start taking notes.</p>
    </div>
  `;
}

Keyboard Shortcuts

Add keyboard shortcuts for common actions:

Close Popup on Escape

// entrypoints/popup/main.ts
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    window.close();
  }
});

Save on Ctrl+S

noteText.addEventListener('keydown', async (e) => {
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();  // Prevent browser save dialog
    
    // Trigger save (already happens on keyup, but provide feedback)
    const saveIndicator = document.createElement('div');
    saveIndicator.textContent = 'Saved!';
    saveIndicator.className = 'save-indicator';
    document.body.appendChild(saveIndicator);
    
    setTimeout(() => saveIndicator.remove(), 1000);
  }
});

Error Handling

Graceful Storage Failures

const saveNote = async (noteData: any) => {
  try {
    await storage.setItem('sync:notesStorage', notesStorage);
  } catch (error) {
    console.error('Failed to save:', error);
    
    // Show user-friendly error
    const errorMsg = document.createElement('div');
    errorMsg.className = 'error-message';
    errorMsg.textContent = 'Failed to save note. Please try again.';
    document.body.appendChild(errorMsg);
    
    setTimeout(() => errorMsg.remove(), 3000);
  }
};

Handle Tab Access Errors

const getCurrentTab = async () => {
  try {
    const [tab] = await browser.tabs.query({ 
      active: true, 
      currentWindow: true 
    });
    
    if (!tab || !tab.url) {
      throw new Error('Cannot access current tab');
    }
    
    return tab;
  } catch (error) {
    console.error('Tab access error:', error);
    
    // Show helpful message
    noteText.disabled = true;
    noteText.placeholder = 'Cannot save notes on this page';
    return null;
  }
};

Visual Polish

Loading States

Show feedback during async operations:

// entrypoints/notes-list.ts
const container = document.getElementById('notes-container');
container.innerHTML = '<div class="loading">Loading notes...</div>';

const notesStorage = await storage.getItem('sync:notesStorage') || {};

// Clear loading, show notes
container.innerHTML = renderNotes(notesStorage);

Smooth Animations

/* entrypoints/popup/style.css */
textarea {
  transition: opacity 0.2s;
}

textarea:focus {
  opacity: 1;
}

.save-indicator {
  position: fixed;
  top: 10px;
  right: 10px;
  background: #4caf50;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  animation: slideIn 0.3s;
}

@keyframes slideIn {
  from {
    transform: translateY(-20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

Accessibility Improvements

ARIA Labels

<textarea 
  id="note-text"
  aria-label="Note content"
  aria-describedby="note-help"
></textarea>
<span id="note-help" class="sr-only">
  Type your note here. It saves automatically.
</span>

Keyboard Navigation

Ensure all interactive elements are keyboard-accessible:

button:focus,
textarea:focus {
  outline: 2px solid #4caf50;
  outline-offset: 2px;
}

Testing Edge Cases

Test Scenarios

Special Characters:

  • Type notes with emoji, unicode, special chars
  • Verify proper storage and display

Very Long Notes:

  • Type 10,000+ character note
  • Check performance, storage limits

Rapid Typing:

  • Type very quickly
  • Ensure no lost characters

Multiple Tabs:

  • Open same site in multiple tabs
  • Verify consistent note display

Private/Incognito Mode:

  • Test extension in incognito
  • Ensure expected behavior (or disable in incognito)

Network Conditions

Test with sync storage:

  • Sign out of Chrome → Notes stay local
  • Sign back in → Notes sync

Performance Optimizations

Debounce Auto-Save

Reduce storage writes during typing:

let saveTimeout: number;

noteText.addEventListener('keyup', () => {
  clearTimeout(saveTimeout);
  
  saveTimeout = setTimeout(async () => {
    await saveNote();
    await updateIcon();
    await sendMessage();
  }, 300);  // Wait 300ms after last keystroke
});

Lazy Load Notes List

For users with many notes:

// entrypoints/notes-list.ts
const renderNotes = (notesStorage: any, limit = 50) => {
  const entries = Object.entries(notesStorage);
  const visible = entries.slice(0, limit);
  
  return visible.map(([key, note]) => renderNote(key, note)).join('');
};

// Add "Load More" button if needed
if (Object.keys(notesStorage).length > 50) {
  // Show load more button
}

Final Checklist

Before submitting to store:

  • Extension name, description, version set in wxt.config.ts
  • All icons created (16, 32, 48, 96, 128px)
  • No console errors or warnings
  • All features work in incognito (if enabled)
  • Tested on Chrome and Firefox
  • Empty states handled gracefully
  • Error messages are user-friendly
  • Loading states provide feedback
  • Keyboard shortcuts documented
  • Permissions minimized
  • README includes usage instructions

Production Build

Building for Production

bun run build      # or npm run build
bun run zip        # or npm run zip

Creates optimized bundles in .output/:

  • .output/chrome-mv3/ - Chrome production build
  • .output/firefox-mv3/ - Firefox production build
  • .output/chrome-mv3.zip - Ready for Chrome Web Store
  • .output/firefox-mv3.zip - Ready for Firefox Add-ons

Verifying Build

  1. Load .output/chrome-mv3/ in Chrome
  2. Test all features work
  3. Check file sizes (should be smaller than dev build)
  4. Verify no dev tools or debug code included

What’s Next

TabNotes is polished and production-ready. All features work smoothly, edge cases are handled, and the user experience is professional.

Lesson 14 covers the submission process - uploading to Chrome Web Store and Firefox Add-ons, writing store listings, handling reviews, and publishing your extension to millions of users.