diff options
| author | MaheshtheDev <[email protected]> | 2026-01-18 04:06:36 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2026-01-18 04:06:36 +0000 |
| commit | 3fa72c4ec782cfc84e0df49aa1924b84e4f63889 (patch) | |
| tree | 7fb0aaf678e995bf15efe088be7c1d7ec716b2b8 /apps/browser-extension | |
| parent | add (mcp): projects aware tool on every init (#676) (diff) | |
| download | supermemory-3fa72c4ec782cfc84e0df49aa1924b84e4f63889.tar.xz supermemory-3fa72c4ec782cfc84e0df49aa1924b84e4f63889.zip | |
feat: fix interaction and improve Design for extension (#679)01-17-feat_fix_interaction_and_improve_design_for_extension
### TL;DR
Redesigned the browser extension UI with a dark theme and improved the Twitter bookmarks import experience with a new onboarding flow.
### What changed?
- Added a new `RightArrow` icon component for UI navigation
- Completely redesigned the popup UI with a dark theme and improved layout
- Enhanced Twitter bookmarks import functionality:
- Added an onboarding toast that appears the first time a user visits the bookmarks page
- Implemented a persistent import intent system that automatically opens the import modal when navigating to the bookmarks page
- Created a progress toast to show import status
- Improved folder import UI
- Updated the extension icon and added a new logo SVG
- Improved the project selection modal with better styling
Diffstat (limited to 'apps/browser-extension')
| -rw-r--r-- | apps/browser-extension/components/icons.tsx | 18 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/index.ts | 3 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/twitter.ts | 678 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/popup/App.tsx | 377 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/popup/style.css | 2 | ||||
| -rw-r--r-- | apps/browser-extension/public/icon-48.png | bin | 110647 -> 12541 bytes | |||
| -rw-r--r-- | apps/browser-extension/public/logo-fullmark.svg | 15 | ||||
| -rw-r--r-- | apps/browser-extension/utils/api.ts | 2 | ||||
| -rw-r--r-- | apps/browser-extension/utils/constants.ts | 13 | ||||
| -rw-r--r-- | apps/browser-extension/utils/ui-components.ts | 14 | ||||
| -rw-r--r-- | apps/browser-extension/wxt.config.ts | 2 |
11 files changed, 855 insertions, 269 deletions
diff --git a/apps/browser-extension/components/icons.tsx b/apps/browser-extension/components/icons.tsx new file mode 100644 index 00000000..2cddd587 --- /dev/null +++ b/apps/browser-extension/components/icons.tsx @@ -0,0 +1,18 @@ +export function RightArrow({ className }: { className?: string }) { + return ( + <svg + width="10" + height="11" + viewBox="0 0 10 11" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className={className} + > + <title>Right arrow</title> + <path + d="M-1.26511e-05 5.82399V4.53599H7.81199L3.90599 0.895994L4.78799 -6.19888e-06L9.79999 4.77399V5.54399L4.78799 10.332L3.90599 9.43599L7.78399 5.82399H-1.26511e-05Z" + fill="#737373" + /> + </svg> + ) +} diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts index d67b37b0..34a77b6c 100644 --- a/apps/browser-extension/entrypoints/content/index.ts +++ b/apps/browser-extension/entrypoints/content/index.ts @@ -15,6 +15,7 @@ import { initializeT3 } from "./t3" import { handleTwitterNavigation, initializeTwitter, + openImportModal, updateTwitterImportUI, } from "./twitter" @@ -29,6 +30,8 @@ export default defineContentScript({ await saveMemory() } else if (message.action === MESSAGE_TYPES.OPEN_SEARCH_PANEL) { handleOpenSearchPanel(message.data as string) + } else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) { + await openImportModal() } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { updateTwitterImportUI(message) } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts index d5328245..4ed67315 100644 --- a/apps/browser-extension/entrypoints/content/twitter.ts +++ b/apps/browser-extension/entrypoints/content/twitter.ts @@ -3,10 +3,11 @@ import { ELEMENT_IDS, MESSAGE_TYPES, POSTHOG_EVENT_KEY, + STORAGE_KEYS, + UI_CONFIG, } from "../../utils/constants" import { trackEvent } from "../../utils/posthog" import { - createTwitterImportButton, createProjectSelectionModal, createSaveTweetElement, DOMUtils, @@ -27,46 +28,105 @@ async function loadSpaceGroteskFonts(): Promise<void> { await document.fonts.ready } -export function initializeTwitter() { +/** + * Check if import intent is valid (exists and not expired) + */ +async function checkAndConsumeImportIntent(): Promise<boolean> { + try { + const result = await browser.storage.local.get( + STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL, + ) + const intentUntil = result[ + STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL + ] as number | undefined + + if (intentUntil && Date.now() < intentUntil) { + await browser.storage.local.remove( + STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL, + ) + return true + } + return false + } catch (error) { + console.error("Error checking import intent:", error) + return false + } +} + +/** + * Check if onboarding toast has been shown before + */ +async function hasOnboardingBeenShown(): Promise<boolean> { + try { + const result = await browser.storage.local.get( + STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN, + ) + return !!result[STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN] + } catch (error) { + console.error("Error checking onboarding status:", error) + return true // Default to true to avoid showing toast on error + } +} + +/** + * Mark onboarding toast as shown + */ +async function markOnboardingAsShown(): Promise<void> { + try { + await browser.storage.local.set({ + [STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN]: true, + }) + } catch (error) { + console.error("Error marking onboarding as shown:", error) + } +} + +export async function initializeTwitter() { if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { return } - // Initial setup if (window.location.pathname === "/i/bookmarks") { - setTimeout(() => { - addTwitterImportButton() - addTwitterImportButtonForFolders() + setTimeout(async () => { + if (window.location.pathname === "/i/bookmarks") { + await handleBookmarksPageLoad() + } }, 2000) } else { - // Remove button if not on bookmarks page - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) - } + // Clean up any injected UI if navigating away + removeAllTwitterUI() } } -function addTwitterImportButton() { +/** + * Handle what to show when user lands on bookmarks page + */ +async function handleBookmarksPageLoad() { if (window.location.pathname !== "/i/bookmarks") { return } - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + addTwitterImportButtonForFolders() // Add buttons to bookmark folders + + const hasIntent = await checkAndConsumeImportIntent() + + if (hasIntent) { + await openImportModal() return } - const button = createTwitterImportButton(async () => { - try { - await handleAllBookmarksImportClick() - } catch (error) { - console.error("Error starting import:", error) - } - }) + const onboardingShown = await hasOnboardingBeenShown() - document.body.appendChild(button) + if (!onboardingShown) { + await showOnboardingToast() + await markOnboardingAsShown() + } } -async function handleAllBookmarksImportClick() { +/** + * Opens the import modal and handles the import flow + */ +export async function openImportModal() { try { const response = await browser.runtime.sendMessage({ action: MESSAGE_TYPES.FETCH_PROJECTS, @@ -85,14 +145,13 @@ async function handleAllBookmarksImportClick() { await showAllBookmarksProjectModal(projects) } } catch (error) { - console.error("Error handling all bookmarks import:", error) + console.error("Error opening import modal:", error) await browser.runtime.sendMessage({ type: MESSAGE_TYPES.BATCH_IMPORT_ALL, }) } } - async function showAllBookmarksProjectModal( projects: Array<{ id: string; name: string; containerTag: string }>, ) { @@ -124,6 +183,420 @@ async function showAllBookmarksProjectModal( document.body.appendChild(modal) } +/** + * Shows the one-time onboarding toast with progress bar + */ +async function showOnboardingToast() { + await loadSpaceGroteskFonts() + + // Remove any existing toast + const existingToast = document.getElementById( + ELEMENT_IDS.TWITTER_ONBOARDING_TOAST, + ) + if (existingToast) { + existingToast.remove() + } + + const duration = UI_CONFIG.ONBOARDING_TOAST_DURATION + + // Create toast container + const toast = document.createElement("div") + toast.id = ELEMENT_IDS.TWITTER_ONBOARDING_TOAST + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2147483647; + background: #ffffff; + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #374151; + min-width: 320px; + max-width: 380px; + box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12); + animation: smSlideInUp 0.3s ease-out; + overflow: hidden; + ` + + // Add keyframe animations if not already present + if (!document.getElementById("supermemory-onboarding-toast-styles")) { + const style = document.createElement("style") + style.id = "supermemory-onboarding-toast-styles" + style.textContent = ` + @keyframes smSlideInUp { + from { transform: translateY(100%); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } + @keyframes smFadeOut { + from { transform: translateY(0); opacity: 1; } + to { transform: translateY(100%); opacity: 0; } + } + @keyframes smProgressGrow { + from { transform: scaleX(0); } + to { transform: scaleX(1); } + } + @keyframes smPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } + } + ` + document.head.appendChild(style) + } + + // Header with icon, text and close button + const header = document.createElement("div") + header.style.cssText = + "display: flex; align-items: flex-start; gap: 12px; position: relative;" + + const iconUrl = browser.runtime.getURL("/icon-16.png") + const icon = document.createElement("img") + icon.src = iconUrl + icon.alt = "Supermemory" + icon.style.cssText = "width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;" + + const textContainer = document.createElement("div") + textContainer.style.cssText = "display: flex; flex-direction: column; gap: 4px; flex: 1;" + + const title = document.createElement("span") + title.style.cssText = "font-weight: 600; font-size: 14px; color: #111827;" + title.textContent = "Import X/Twitter Bookmarks" + + const description = document.createElement("span") + description.style.cssText = "font-size: 13px; color: #6b7280; line-height: 1.4;" + description.textContent = + "You can import all your Twitter bookmarks to Supermemory with one click." + + textContainer.appendChild(title) + textContainer.appendChild(description) + + // Close button + const closeButton = document.createElement("button") + closeButton.setAttribute("aria-label", "Close onboarding toast") + closeButton.style.cssText = ` + position: absolute; + top: 0; + right: 0; + background: transparent; + border: none; + cursor: pointer; + padding: 4px; + color: #9ca3af; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s; + ` + closeButton.innerHTML = ` + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> + <line x1="18" y1="6" x2="6" y2="18"></line> + <line x1="6" y1="6" x2="18" y2="18"></line> + </svg> + ` + closeButton.addEventListener("mouseenter", () => { + closeButton.style.backgroundColor = "#f3f4f6" + }) + closeButton.addEventListener("mouseleave", () => { + closeButton.style.backgroundColor = "transparent" + }) + closeButton.addEventListener("click", () => { + dismissToast(toast) + }) + + header.appendChild(icon) + header.appendChild(textContainer) + header.appendChild(closeButton) + + // Action buttons + const buttonsContainer = document.createElement("div") + buttonsContainer.style.cssText = "display: flex; gap: 8px; margin-top: 4px;" + + const importButton = document.createElement("button") + importButton.style.cssText = ` + padding: 8px 16px; + border: none; + border-radius: 8px; + background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%); + color: white; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: opacity 0.2s; + font-family: inherit; + ` + importButton.textContent = "Import now" + importButton.addEventListener("mouseenter", () => { + importButton.style.opacity = "0.9" + }) + importButton.addEventListener("mouseleave", () => { + importButton.style.opacity = "1" + }) + importButton.addEventListener("click", async () => { + dismissToast(toast) + await openImportModal() + }) + + const learnMoreButton = document.createElement("button") + learnMoreButton.style.cssText = ` + padding: 8px 16px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: transparent; + color: #374151; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + font-family: inherit; + ` + learnMoreButton.textContent = "Learn more" + learnMoreButton.addEventListener("mouseenter", () => { + learnMoreButton.style.backgroundColor = "#f9fafb" + }) + learnMoreButton.addEventListener("mouseleave", () => { + learnMoreButton.style.backgroundColor = "transparent" + }) + learnMoreButton.addEventListener("click", () => { + window.open( + "https://docs.supermemory.ai/connectors/twitter", + "_blank", + ) + }) + + buttonsContainer.appendChild(importButton) + buttonsContainer.appendChild(learnMoreButton) + + // Progress bar container + const progressBarContainer = document.createElement("div") + progressBarContainer.setAttribute("role", "progressbar") + progressBarContainer.setAttribute("aria-valuemin", "0") + progressBarContainer.setAttribute("aria-valuemax", "100") + progressBarContainer.setAttribute("aria-valuenow", "0") + progressBarContainer.setAttribute("aria-label", "Onboarding toast auto-dismiss progress") + progressBarContainer.style.cssText = ` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: #e5e7eb; + ` + + const progressBar = document.createElement("div") + progressBar.style.cssText = ` + height: 100%; + background: linear-gradient(90deg, #0ff0d2, #5bd3fb, #1e0ff0); + transform-origin: left; + animation: smProgressGrow ${duration}ms linear forwards; + ` + + // Update progress bar ARIA value as animation progresses + const startTime = Date.now() + const updateProgress = () => { + const elapsed = Date.now() - startTime + const progress = Math.min(100, Math.round((elapsed / duration) * 100)) + progressBarContainer.setAttribute("aria-valuenow", String(progress)) + if (progress < 100) { + requestAnimationFrame(updateProgress) + } + } + requestAnimationFrame(updateProgress) + + progressBarContainer.appendChild(progressBar) + + // Assemble toast + toast.appendChild(header) + toast.appendChild(buttonsContainer) + toast.appendChild(progressBarContainer) + + document.body.appendChild(toast) + + // Auto-dismiss after duration + setTimeout(() => { + if (document.body.contains(toast)) { + dismissToast(toast) + } + }, duration) +} + +/** + * Dismiss the toast with animation + */ +function dismissToast(toast: HTMLElement) { + toast.style.animation = "smFadeOut 0.3s ease-out forwards" + setTimeout(() => { + if (document.body.contains(toast)) { + toast.remove() + } + }, 300) +} + +/** + * Remove all Twitter-specific injected UI + */ +function removeAllTwitterUI() { + // Remove import button (legacy) + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) + } + // Remove onboarding toast + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST)) { + DOMUtils.removeElement(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST) + } + // Remove import progress toast + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)) { + DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST) + } + // Remove any folder buttons + document.querySelectorAll("[data-supermemory-button]").forEach((button) => { + button.remove() + }) +} + +/** + * Shows or updates the import progress toast in the bottom-right + */ +function showOrUpdateImportProgressToast(message: string, isComplete = false) { + let toast = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST) + + if (!toast) { + // Ensure animation styles are available + if (!document.getElementById("supermemory-onboarding-toast-styles")) { + const style = document.createElement("style") + style.id = "supermemory-onboarding-toast-styles" + style.textContent = ` + @keyframes smSlideInUp { + from { transform: translateY(100%); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } + @keyframes smFadeOut { + from { transform: translateY(0); opacity: 1; } + to { transform: translateY(100%); opacity: 0; } + } + @keyframes smPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } + } + ` + document.head.appendChild(style) + } + + // Create new toast + toast = document.createElement("div") + toast.id = ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2147483647; + background: #ffffff; + border-radius: 12px; + padding: 14px 16px; + display: flex; + align-items: center; + gap: 12px; + font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #374151; + min-width: 280px; + max-width: 360px; + box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12); + animation: smSlideInUp 0.3s ease-out; + ` + + const iconUrl = browser.runtime.getURL("/icon-16.png") + const icon = document.createElement("img") + icon.src = iconUrl + icon.alt = "Supermemory" + icon.id = "sm-import-progress-icon" + icon.style.cssText = + "width: 20px; height: 20px; border-radius: 4px; flex-shrink: 0; animation: smPulse 1.5s ease-in-out infinite;" + + const textSpan = document.createElement("span") + textSpan.id = "sm-import-progress-text" + textSpan.style.cssText = "font-weight: 500; flex: 1;" + textSpan.textContent = message + + toast.appendChild(icon) + toast.appendChild(textSpan) + document.body.appendChild(toast) + } else { + // Update existing toast + const textSpan = toast.querySelector( + "#sm-import-progress-text", + ) as HTMLSpanElement + if (textSpan) { + textSpan.textContent = message + } + } + + // Style for completion + if (isComplete) { + const icon = toast.querySelector( + "#sm-import-progress-icon", + ) as HTMLImageElement + if (icon) { + icon.style.animation = "none" + icon.style.opacity = "1" + } + + const textSpan = toast.querySelector( + "#sm-import-progress-text", + ) as HTMLSpanElement + if (textSpan) { + textSpan.style.color = "#059669" + } + + // Auto-dismiss after 4 seconds on completion + setTimeout(() => { + const existingToast = document.getElementById( + ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST, + ) + if (existingToast) { + dismissToast(existingToast) + } + }, 4000) + } +} + +export function updateTwitterImportUI(message: { + type: string + importedMessage?: string + totalImported?: number +}) { + if (message.type === MESSAGE_TYPES.IMPORT_UPDATE && message.importedMessage) { + showOrUpdateImportProgressToast(message.importedMessage, false) + } + + if (message.type === MESSAGE_TYPES.IMPORT_DONE) { + showOrUpdateImportProgressToast( + `✓ Imported ${message.totalImported} tweets!`, + true, + ) + } +} + +export async function handleTwitterNavigation() { + if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + return + } + + if (window.location.pathname === "/i/bookmarks") { + addTwitterImportButtonForFolders() + await handleBookmarksPageLoad() + } else { + removeAllTwitterUI() + } +} + +/** + * Adds import buttons to bookmark folders + */ function addTwitterImportButtonForFolders() { if (window.location.pathname !== "/i/bookmarks") { return @@ -138,6 +611,9 @@ function addTwitterImportButtonForFolders() { }) } +/** + * Adds an import button to a bookmark folder element + */ function addButtonToElement(element: HTMLElement) { if (element.querySelector("[data-supermemory-button]")) { return @@ -148,9 +624,8 @@ function addButtonToElement(element: HTMLElement) { const button = createSaveTweetElement(async () => { const url = element.getAttribute("href") const bookmarkCollectionId = url?.split("/").pop() - console.log("Bookmark collection ID:", bookmarkCollectionId) if (bookmarkCollectionId) { - await showProjectSelectionModal(bookmarkCollectionId) + await showFolderProjectSelectionModal(bookmarkCollectionId) } }) @@ -164,132 +639,55 @@ function addButtonToElement(element: HTMLElement) { element.style.padding = "10px" } -export function updateTwitterImportUI(message: { - type: string - importedMessage?: string - totalImported?: number -}) { - const importButton = document.getElementById( - ELEMENT_IDS.TWITTER_IMPORT_BUTTON, - ) - if (!importButton) return - - const existingImg = importButton.querySelector("img") - if (existingImg) { - existingImg.remove() - const iconUrl = browser.runtime.getURL("/icon-16.png") - importButton.style.backgroundImage = `url("${iconUrl}")` - importButton.style.backgroundRepeat = "no-repeat" - importButton.style.backgroundSize = "20px 20px" - importButton.style.backgroundPosition = "8px center" - importButton.style.padding = "10px 16px 10px 32px" - } - - let textSpan = importButton.querySelector( - "#sm-import-text", - ) as HTMLSpanElement - if (!textSpan) { - textSpan = document.createElement("span") - textSpan.id = "sm-import-text" - textSpan.style.cssText = "font-weight: 500; font-size: 14px;" - importButton.appendChild(textSpan) - } - - if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - textSpan.textContent = message.importedMessage || "" - importButton.style.cursor = "default" - } - - if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - textSpan.textContent = `✓ Imported ${message.totalImported} tweets!` - textSpan.style.color = "#059669" - - setTimeout(() => { - textSpan.textContent = "Import Bookmarks" - textSpan.style.color = "" - importButton.style.cursor = "pointer" - }, 3000) - } -} - -export function handleTwitterNavigation() { - if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return - } - - if (window.location.pathname === "/i/bookmarks") { - addTwitterImportButton() - addTwitterImportButtonForFolders() - } else { - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) - } - document.querySelectorAll("[data-supermemory-button]").forEach((button) => { - button.remove() - }) - } -} - /** * Shows the project selection modal for folder imports - * @param bookmarkCollectionId - The ID of the bookmark collection to import */ -async function showProjectSelectionModal(bookmarkCollectionId: string) { - try { - const modal = createProjectSelectionModal( - [], - async (selectedProject) => { - modal.remove() - - try { - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - isFolderImport: true, - bookmarkCollectionId: bookmarkCollectionId, - selectedProject: selectedProject, - }) - } catch (error) { - console.error("Error importing bookmarks:", error) - } - }, - () => { - modal.remove() - }, - ) +async function showFolderProjectSelectionModal(bookmarkCollectionId: string) { + await loadSpaceGroteskFonts() - document.body.appendChild(modal) + const modal = createProjectSelectionModal( + [], + async (selectedProject) => { + modal.remove() - try { - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.FETCH_PROJECTS, - }) + try { + await browser.runtime.sendMessage({ + type: MESSAGE_TYPES.BATCH_IMPORT_ALL, + isFolderImport: true, + bookmarkCollectionId: bookmarkCollectionId, + selectedProject: selectedProject, + }) + } catch (error) { + console.error("Error importing bookmarks:", error) + } + }, + () => { + modal.remove() + }, + ) - if (response.success && response.data) { - const projects = response.data + document.body.appendChild(modal) - if (projects.length === 0) { - console.warn("No projects available for import") - updateModalWithProjects(modal, []) - } else { - updateModalWithProjects(modal, projects) - } - } else { - console.error("Failed to fetch projects:", response.error) - updateModalWithProjects(modal, []) - } - } catch (error) { - console.error("Error fetching projects:", error) + try { + const response = await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.FETCH_PROJECTS, + }) + + if (response.success && response.data) { + const projects = response.data + updateModalWithProjects(modal, projects) + } else { + console.error("Failed to fetch projects:", response.error) updateModalWithProjects(modal, []) } } catch (error) { - console.error("Error showing project selection modal:", error) + console.error("Error fetching projects:", error) + updateModalWithProjects(modal, []) } } /** * Updates the modal with fetched projects - * @param modal - The modal element - * @param projects - Array of projects to populate the dropdown */ function updateModalWithProjects( modal: HTMLElement, @@ -316,10 +714,10 @@ function updateModalWithProjects( importButton.disabled = true importButton.style.cssText = ` padding: 10px 16px; - border: none; - border-radius: 8px; - background: #d1d5db; - color: #9ca3af; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.3); font-size: 14px; font-weight: 500; cursor: not-allowed; diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index c8472da4..f61a2415 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -2,7 +2,7 @@ import { useQueryClient } from "@tanstack/react-query" import { useEffect, useState } from "react" import "./App.css" import { validateAuthToken } from "../../utils/api" -import { MESSAGE_TYPES } from "../../utils/constants" +import { MESSAGE_TYPES, STORAGE_KEYS, UI_CONFIG } from "../../utils/constants" import { useDefaultProject, useProjects, @@ -17,6 +17,7 @@ import { userData as userDataStorage, } from "../../utils/storage" import type { Project } from "../../utils/types" +import { RightArrow } from "@/components/icons" const Tooltip = ({ children, @@ -181,6 +182,11 @@ function App() { } }, [defaultProject, projects, setDefaultProjectMutation]) + // biome-ignore lint/correctness/useExhaustiveDependencies: close space selector when tab changes + useEffect(() => { + setShowProjectSelector(false) + }, [activeTab]) + const handleSaveCurrentPage = async () => { setSaving(true) @@ -262,25 +268,51 @@ function App() { if (loading) { return ( - <div className="w-80 p-0 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] bg-white rounded-lg relative overflow-hidden"> - <div className="flex items-center justify-between gap-3 p-2.5 border-b border-gray-200 relative"> - <img - alt="supermemory" - className="w-8 h-8 shrink-0" - src="./dark-transparent.svg" - style={{ width: "80%", height: "45px" }} - /> + <div + className="w-80 p-0 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] rounded-lg relative overflow-hidden" + style={{ + background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)", + boxShadow: + "1.5px 1.5px 20px 0 rgba(0, 0, 0, 0.65), 1px 1.5px 2px 0 rgba(128, 189, 255, 0.07) inset, -0.5px -1.5px 4px 0 rgba(0, 35, 73, 0.40) inset", + }} + > + <div + id="popup-header" + className="flex items-center justify-between p-2.5 relative" + > + <div className="flex items-center gap-2"> + <div + className="w-8 h-8 shrink-0 rounded-[3.75px] overflow-hidden relative" + style={{ boxShadow: "inset 0px 1px 3.75px 0px #000" }} + > + <img + alt="supermemory" + src="./icon-48.png" + className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]" + /> + </div> + <div className="flex flex-col"> + <span className="text-[11px] font-medium text-[#737373] leading-normal"> + Your + </span> + <img + alt="supermemory" + src="./logo-fullmark.svg" + className="h-[14.5px] w-auto" + /> + </div> + </div> </div> - <div className="p-4 text-black min-h-[300px] flex items-center justify-center"> - <div className="flex items-center gap-3 text-base text-black"> + <div className="p-4 min-h-[300px] flex items-center justify-center"> + <div className="flex items-center gap-3 text-sm text-[#737373]"> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" - className="text-black" + className="text-[#737373]" aria-hidden="true" > <path d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"> @@ -294,7 +326,7 @@ function App() { </path> </svg> - <span className="font-semibold">Loading...</span> + <span className="font-medium">Loading...</span> </div> </div> </div> @@ -303,26 +335,56 @@ function App() { return ( <div - className="w-80 p-0 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] bg-white rounded-lg relative overflow-hidden" + className="w-80 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] rounded-lg relative overflow-hidden p-4" style={{ background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)", boxShadow: "1.5px 1.5px 20px 0 rgba(0, 0, 0, 0.65), 1px 1.5px 2px 0 rgba(128, 189, 255, 0.07) inset, -0.5px -1.5px 4px 0 rgba(0, 35, 73, 0.40) inset", }} > - <div className="flex items-center justify-between gap-3 p-2.5 relative"> - <div className="text-white text-lg font-semibold ml-2">supermemory</div> + <div + id="popup-header" + className="flex items-center justify-between p-2.5 relative" + > + <div className="flex items-center gap-2"> + <div + className="w-8 h-8 shrink-0 rounded-[3.75px] overflow-hidden relative" + style={{ boxShadow: "inset 0px 1px 3.75px 0px #000" }} + > + <img + alt="supermemory" + src="./icon-48.png" + className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]" + /> + </div> + <div className="flex flex-col"> + <span className="text-[11px] font-medium text-[#737373] leading-normal"> + {(() => { + const name = + userData?.name?.split(" ")[0] || + userData?.email?.split("@")[0] + if (!name) return "Your" + return name.endsWith("s") ? `${name}'` : `${name}'s` + })()} + </span> + <img + alt="supermemory" + src="./logo-fullmark.svg" + className="h-[14.5px] w-auto" + /> + </div> + </div> {userSignedIn && ( <button - className="bg-none border-none text-base cursor-pointer text-gray-500 p-1 rounded transition-colors duration-200" + className="bg-transparent border-none cursor-pointer p-1 rounded transition-colors duration-200" onClick={handleSignOut} - title="Logout" + aria-label="Logout" type="button" > <svg - width="19" - height="18" - viewBox="0 0 19 18" + width="24" + height="24" + viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" > @@ -330,15 +392,15 @@ function App() { <path d="M17 9H7.5M15 12L18 9L15 6M10 4V3C10 2.46957 9.78929 1.96086 9.41421 1.58579C9.03914 1.21071 8.53043 1 8 1H3C2.46957 1 1.96086 1.21071 1.58579 1.58579C1.21071 1.96086 1 2.46957 1 3V15C1 15.5304 1.21071 16.0391 1.58579 16.4142C1.96086 16.7893 2.46957 17 3 17H8C8.53043 17 9.03914 16.7893 9.41421 16.4142C9.78929 16.0391 10 15.5304 10 15V14" stroke="#FAFAFA" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" /> </svg> </button> )} </div> - <div className="p-4 min-h-[250px] pt-1"> + <div className="min-h-[250px] pt-1"> {userSignedIn ? ( <div className="text-left"> {/* Tab Navigation */} @@ -408,43 +470,119 @@ function App() { </div> </div> - {/* Project Selection */} - <div className="mb-0"> - <button - className="w-full bg-[#5B7EF50A] border-none p-4 cursor-pointer text-left rounded-xl flex justify-between items-center transition-colors duration-200 hover:bg-[#5B7EF520]" - onClick={handleShowProjectSelector} - type="button" - style={{ - boxShadow: - "2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset", - }} - > + {/* Space Selection */} + <div className="flex flex-col gap-2"> + <div className="flex items-center justify-between pl-1"> <span className="text-sm font-medium text-[#737373]"> - Save to project: + Save to Space </span> - <div className="flex items-center gap-2"> - <span className="text-sm font-medium text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-[120px]"> + {showProjectSelector && ( + <button + id="close-space-selector" + className="bg-transparent border-none cursor-pointer p-0 text-[#737373] hover:text-white transition-colors" + onClick={() => setShowProjectSelector(false)} + type="button" + > + <svg + width="16" + height="16" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + > + <title>Close</title> + <line x1="18" y1="6" x2="6" y2="18" /> + <line x1="6" y1="6" x2="18" y2="18" /> + </svg> + </button> + )} + </div> + + {showProjectSelector ? ( + <div className="flex flex-col gap-1 max-h-[180px] overflow-y-auto"> + {loadingProjects ? ( + <div className="h-11 flex items-center justify-center text-sm text-[#737373]"> + Loading spaces... + </div> + ) : ( + projects.map((project) => ( + <button + id={`space-option-${project.id}`} + className={`w-full h-11 flex items-center justify-between px-4 rounded-lg bg-transparent border-none cursor-pointer text-left transition-colors duration-200 hover:bg-[#5B7EF510] ${ + defaultProject?.id === project.id + ? "bg-[#5B7EF50A]" + : "" + }`} + style={ + defaultProject?.id === project.id + ? { + boxShadow: + "2px 2px 1px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset", + } + : undefined + } + key={project.id} + onClick={() => handleProjectSelect(project)} + type="button" + > + <span className="text-sm font-normal text-[rgba(255,255,255,0.94)] overflow-hidden text-ellipsis whitespace-nowrap tracking-tight"> + {project.name} + </span> + {defaultProject?.id === project.id && ( + <svg + width="16" + height="16" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + className="text-white shrink-0" + > + <title>Selected</title> + <polyline points="20 6 9 17 4 12" /> + </svg> + )} + </button> + )) + )} + </div> + ) : ( + <button + id="space-selector-trigger" + className="w-full h-11 flex items-center justify-between px-4 rounded-lg bg-[#5B7EF50A] border-none cursor-pointer text-left transition-colors duration-200 hover:bg-[#5B7EF520]" + onClick={handleShowProjectSelector} + type="button" + style={{ + boxShadow: + "2px 2px 1px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset", + }} + > + <span className="text-sm font-normal text-[rgba(255,255,255,0.94)] overflow-hidden text-ellipsis whitespace-nowrap tracking-tight"> {defaultProject ? defaultProject.name - : "Default Project"} + : "Select a space"} </span> <svg - aria-label="Select project" - className="text-white shrink-0 transition-transform duration-200 hover:text-gray-700 transform rotate-90" - fill="none" + width="16" height="16" + viewBox="0 0 24 24" + fill="none" stroke="currentColor" + strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - strokeWidth="2" - viewBox="0 0 24 24" - width="16" + className="text-white shrink-0" > - <title>Select project</title> - <path d="M9 18l6-6-6-6" /> + <title>Expand</title> + <polyline points="6 9 12 15 18 9" /> </svg> - </div> - </button> + </button> + )} </div> {/* Save Button at Bottom */} @@ -496,7 +634,7 @@ function App() { <div className="flex flex-col gap-4"> <div className="flex flex-col gap-2"> <button - className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-center justify-start transition-colors duration-200 hover:bg-[#5B7EF520]" + className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-start transition-colors duration-200 hover:bg-[#5B7EF520]" style={{ boxShadow: "2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset", @@ -524,34 +662,79 @@ function App() { Import ChatGPT Memories </p> <p className="m-0 text-[14px] text-[#737373] leading-tight"> - open 'manage', save your memories to supermemory + open 'manage' > save your memories to supermemory </p> </div> + <RightArrow className="size-4" /> </button> </div> <div className="flex flex-col gap-2"> <button - className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-center justify-center transition-colors duration-200 outline-none appearance-none hover:bg-[#5B7EF520] focus:outline-none" + className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-start transition-colors duration-200 outline-none appearance-none hover:bg-[#5B7EF520] focus:outline-none" style={{ boxShadow: "2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset", }} onClick={async () => { - const [activeTab] = await chrome.tabs.query({ - active: true, - currentWindow: true, - }) - const targetUrl = "https://x.com/i/bookmarks" - if (activeTab?.url === targetUrl) { - return + try { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }) + + const isOnBookmarksPage = + activeTab?.url?.includes("x.com/i/bookmarks") || + activeTab?.url?.includes("twitter.com/i/bookmarks") + + if (isOnBookmarksPage && activeTab?.id) { + try { + await chrome.tabs.sendMessage(activeTab.id, { + action: MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL, + }) + } catch (error) { + // Content script may not be loaded yet, fall back to intent-based approach + console.error( + "Failed to send message to content script:", + error, + ) + const intentExpiry = + Date.now() + UI_CONFIG.IMPORT_INTENT_TTL + await chrome.storage.local.set({ + [STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]: + intentExpiry, + }) + await chrome.tabs.create({ + url: targetUrl, + }) + } + } else { + const intentExpiry = + Date.now() + UI_CONFIG.IMPORT_INTENT_TTL + await chrome.storage.local.set({ + [STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]: + intentExpiry, + }) + await chrome.tabs.create({ + url: targetUrl, + }) + } + } catch (error) { + console.error("Error opening Twitter import:", error) + // Fallback: try to open the bookmarks page anyway + try { + await chrome.tabs.create({ + url: targetUrl, + }) + } catch (fallbackError) { + console.error( + "Failed to open bookmarks page:", + fallbackError, + ) + } } - - await chrome.tabs.create({ - url: targetUrl, - }) }} type="button" > @@ -570,9 +753,10 @@ function App() { Import X/Twitter Bookmarks </p> <p className="m-0 text-[14px] text-[#737373] leading-tight"> - Click on supermemory on top right to import bookmarks + Opens import dialog automatically </p> </div> + <RightArrow className="size-4" /> </button> </div> </div> @@ -588,7 +772,9 @@ function App() { </div> ) : userData?.email ? ( <> - <span className="font-medium text-base">Email</span> + <span className="font-medium text-base text-white"> + Email + </span> <span className="text-sm text-[#525966] p-3 rounded-xl bg-[#5B7EF50A]" style={{ @@ -608,7 +794,7 @@ function App() { {/* Chat Integration Section */} <div className="mb-4"> - <h3 className="text-base font-semibold mb-3"> + <h3 className="text-base font-semibold mb-3 text-white"> Chat Integration </h3> <div className="flex items-center justify-between p-3 rounded-xl bg-[#5B7EF50A] mb-3"> @@ -654,53 +840,6 @@ function App() { </div> </div> )} - - {showProjectSelector && ( - <div className="absolute inset-0 bg-white rounded-lg z-1000 shadow-xl flex flex-col"> - <div className="flex justify-between items-center p-4 border-b border-gray-200 text-base font-semibold text-black shrink-0"> - <span>Select the Project</span> - <button - className="bg-transparent border-none text-xl cursor-pointer text-gray-500 p-0 w-6 h-6 flex items-center justify-center hover:text-black" - onClick={() => setShowProjectSelector(false)} - type="button" - > - × - </button> - </div> - {loadingProjects ? ( - <div className="py-8 px-4 text-center text-gray-500 text-sm"> - Loading projects... - </div> - ) : ( - <div className="flex-1 overflow-y-auto min-h-0"> - {projects.map((project) => ( - <button - className={`flex justify-between items-center py-3 px-4 cursor-pointer transition-colors duration-200 border-b border-gray-100 bg-transparent border-none w-full text-left last:border-b-0 hover:bg-gray-50 ${ - defaultProject?.id === project.id ? "bg-blue-50" : "" - }`} - key={project.id} - onClick={() => handleProjectSelect(project)} - type="button" - > - <div className="flex flex-col flex-1 gap-0.5"> - <span className="text-sm font-medium text-black wrap-break-word leading-tight"> - {project.name} - </span> - <span className="text-xs text-gray-500"> - {project.documentCount} docs - </span> - </div> - {defaultProject?.id === project.id && ( - <span className="text-blue-600 font-bold text-base"> - ✓ - </span> - )} - </button> - ))} - </div> - )} - </div> - )} </div> ) : ( <div className="text-center py-2"> @@ -718,18 +857,18 @@ function App() { </div> ) : ( <div className="mb-8"> - <h2 className="m-0 mb-4 text-sm font-normal text-black leading-tight"> + <h2 className="m-0 mb-4 text-sm font-normal leading-tight"> Login to unlock all chrome extension features </h2> <ul className="list-none p-0 m-0 text-left"> - <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> + <li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold"> Save any page to your supermemory </li> - <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> + <li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold"> Import all your Twitter / X Bookmarks </li> - <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> + <li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold"> Import your ChatGPT Memories </li> </ul> @@ -737,7 +876,7 @@ function App() { )} <div className="mt-8"> - <p className="m-0 mb-4 text-sm text-gray-500"> + <p className="m-0 mb-4 text-sm text-[#737373]"> Having trouble logging in?{" "} <button className="bg-transparent border-none text-blue-500 cursor-pointer underline text-sm p-0 hover:text-blue-700" diff --git a/apps/browser-extension/entrypoints/popup/style.css b/apps/browser-extension/entrypoints/popup/style.css index ea67f153..84427e60 100644 --- a/apps/browser-extension/entrypoints/popup/style.css +++ b/apps/browser-extension/entrypoints/popup/style.css @@ -7,6 +7,7 @@ color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; + border: 1px solid #242424; font-synthesis: none; text-rendering: optimizeLegibility; @@ -18,7 +19,6 @@ @media (prefers-color-scheme: light) { :root { color: #213547; - background-color: #ffffff; } a:hover { color: #747bff; diff --git a/apps/browser-extension/public/icon-48.png b/apps/browser-extension/public/icon-48.png Binary files differindex bf132193..10f7edd1 100644 --- a/apps/browser-extension/public/icon-48.png +++ b/apps/browser-extension/public/icon-48.png diff --git a/apps/browser-extension/public/logo-fullmark.svg b/apps/browser-extension/public/logo-fullmark.svg new file mode 100644 index 00000000..f7fbf41b --- /dev/null +++ b/apps/browser-extension/public/logo-fullmark.svg @@ -0,0 +1,15 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="286 0 1190 168" +> + <title>supermemory</title> + <g fill="#EFEFEF" clipPath="url(#a)"> + <path d="M330.13 132.123c-11.97 0-21.792-2.607-29.438-7.823-7.66-5.216-12.284-12.665-13.888-22.362l21.477-5.59c.859 4.351 2.319 7.766 4.353 10.244 2.033 2.493 4.567 4.251 7.588 5.317 3.021 1.052 6.329 1.585 9.908 1.585 5.427 0 9.436-.966 12.027-2.882 2.592-1.931 3.895-4.308 3.895-7.175 0-2.868-1.231-5.058-3.709-6.614-2.477-1.556-6.414-2.824-11.855-3.818l-5.183-.936c-6.414-1.24-12.285-2.954-17.582-5.13-5.312-2.175-9.565-5.187-12.772-9.034-3.207-3.847-4.811-8.817-4.811-14.898 0-9.192 3.336-16.238 9.994-21.151 6.672-4.899 15.421-7.363 26.288-7.363 10.237 0 18.756 2.306 25.543 6.887 6.787 4.597 11.226 10.62 13.33 18.068l-21.663 6.7c-.988-4.711-2.992-8.068-6.013-10.057-3.022-1.988-6.759-2.982-11.197-2.982-4.439 0-7.846.778-10.18 2.334-2.348 1.556-3.522 3.703-3.522 6.426 0 2.982 1.231 5.187 3.708 6.613 2.463 1.427 5.799 2.522 9.994 3.257l5.183.936c6.916 1.24 13.173 2.882 18.785 4.942 5.613 2.046 10.051 4.971 13.33 8.76 3.265 3.79 4.911 8.919 4.911 15.374 0 9.682-3.493 17.175-10.466 22.448-6.973 5.288-16.323 7.924-28.049 7.924h.014ZM409.294 131.749c-7.159 0-13.416-1.643-18.785-4.942-5.369-3.285-9.536-7.853-12.499-13.688-2.964-5.835-4.439-12.549-4.439-20.114v-55.14h23.324v53.282c0 6.959 1.69 12.174 5.097 15.647 3.394 3.472 8.233 5.216 14.533 5.216 7.159 0 12.714-2.392 16.666-7.176 3.952-4.783 5.928-11.454 5.928-20.027V37.865h23.324v92.4h-22.952v-12.103h-3.336c-1.475 3.112-4.252 6.152-8.333 9.135-4.066 2.982-10.252 4.466-18.513 4.466l-.015-.014ZM479.095 167.525V37.865h22.952v11.18h3.336c2.09-3.601 5.369-6.8 9.807-9.595 4.439-2.795 10.796-4.193 19.072-4.193 7.402 0 14.261 1.83 20.546 5.49 6.3 3.66 11.355 9.034 15.177 16.108 3.823 7.074 5.742 15.647 5.742 25.704v2.983c0 10.057-1.919 18.63-5.742 25.704-3.822 7.074-8.891 12.449-15.177 16.108-6.3 3.66-13.144 5.49-20.546 5.49-5.555 0-10.209-.648-13.974-1.96-3.766-1.296-6.787-2.982-9.078-5.028-2.291-2.046-4.109-4.121-5.455-6.239h-3.336v47.879h-23.324v.029Zm48.137-55.141c7.288 0 13.301-2.334 18.055-6.988 4.753-4.654 7.13-11.454 7.13-20.402v-1.859c0-8.947-2.405-15.748-7.216-20.402-4.811-4.653-10.796-6.987-17.955-6.987-7.159 0-13.144 2.334-17.955 6.987-4.81 4.654-7.216 11.455-7.216 20.402v1.86c0 8.947 2.406 15.747 7.216 20.401 4.811 4.654 10.796 6.988 17.955 6.988h-.014ZM629.792 132.873c-9.135 0-17.182-1.96-24.155-5.864-6.973-3.919-12.399-9.438-16.294-16.584-3.88-7.147-5.827-15.561-5.827-25.243v-2.234c0-9.682 1.904-18.096 5.741-25.243 3.823-7.146 9.193-12.665 16.108-16.584 6.916-3.904 14.934-5.864 24.069-5.864s16.852 2.017 23.51 6.051c6.658 4.035 11.855 9.625 15.549 16.772 3.709 7.146 5.556 15.43 5.556 24.868v8.011h-66.837c.244 6.34 2.592 11.484 7.03 15.46 4.439 3.977 9.88 5.965 16.294 5.965 6.415 0 11.354-1.426 14.433-4.279 3.078-2.853 5.426-6.023 7.03-9.495l19.071 10.057c-1.732 3.227-4.223 6.743-7.502 10.532-3.279 3.79-7.617 7.017-13.058 9.683-5.427 2.665-12.342 4.005-20.733 4.005l.015-.014Zm-22.408-59.434h42.954c-.501-5.346-2.62-9.625-6.386-12.852-3.765-3.228-8.677-4.842-14.719-4.842s-11.297 1.614-14.991 4.842c-3.708 3.227-5.985 7.52-6.844 12.852h-.014ZM685.517 130.265v-92.4h22.952v10.431h3.336c1.36-3.731 3.608-6.454 6.758-8.198 3.15-1.743 6.816-2.608 11.011-2.608h11.111v20.863h-11.483c-5.928 0-10.796 1.585-14.619 4.755-3.823 3.17-5.742 8.04-5.742 14.624v52.533h-23.324ZM749.562 130.265v-92.4h22.951v10.057h3.337c1.603-3.098 4.252-5.807 7.96-8.098 3.709-2.29 8.577-3.443 14.619-3.443 6.543 0 11.784 1.268 15.736 3.818 3.951 2.55 6.972 5.864 9.077 9.97h3.336c2.091-3.976 5.055-7.261 8.892-9.87 3.823-2.607 9.249-3.904 16.294-3.904 5.67 0 10.824 1.21 15.463 3.631 4.625 2.42 8.333 6.08 11.111 10.993 2.778 4.914 4.166 11.08 4.166 18.544v60.731H859.18v-59.06c0-5.086-1.303-8.904-3.88-11.454-2.591-2.55-6.242-3.819-10.924-3.819-5.312 0-9.407 1.715-12.314 5.13-2.906 3.415-4.353 8.285-4.353 14.624v54.593h-23.324v-59.06c0-5.085-1.303-8.904-3.88-11.454-2.591-2.55-6.242-3.818-10.924-3.818-5.312 0-9.407 1.715-12.314 5.13-2.906 3.414-4.353 8.284-4.353 14.624v54.592H749.59l-.028-.057ZM940.249 132.873c-9.135 0-17.182-1.96-24.155-5.864-6.972-3.919-12.399-9.438-16.293-16.584-3.881-7.147-5.828-15.561-5.828-25.243v-2.234c0-9.682 1.904-18.096 5.742-25.243 3.823-7.146 9.192-12.665 16.107-16.584 6.916-3.904 14.934-5.864 24.069-5.864s16.852 2.017 23.51 6.051c6.658 4.035 11.855 9.625 15.549 16.772 3.709 7.146 5.556 15.43 5.556 24.868v8.011h-66.837c.244 6.34 2.592 11.484 7.031 15.46 4.438 3.977 9.879 5.965 16.294 5.965 6.414 0 11.354-1.426 14.446-4.279 3.079-2.853 5.427-6.023 7.031-9.495l19.071 10.057c-1.732 3.227-4.224 6.743-7.503 10.532-3.278 3.79-7.617 7.017-13.058 9.683-5.426 2.665-12.342 4.005-20.732 4.005v-.014Zm-22.393-59.434h42.954c-.502-5.346-2.621-9.625-6.386-12.852-3.766-3.228-8.677-4.842-14.719-4.842s-11.297 1.614-14.991 4.842c-3.708 3.227-5.985 7.52-6.844 12.852h-.014ZM995.975 130.265v-92.4h22.955v10.057h3.33c1.61-3.098 4.25-5.807 7.96-8.098 3.71-2.29 8.58-3.443 14.62-3.443 6.55 0 11.79 1.268 15.74 3.818 3.95 2.55 6.97 5.864 9.08 9.97h3.33c2.09-3.976 5.06-7.261 8.89-9.87 3.83-2.607 9.25-3.904 16.3-3.904 5.67 0 10.82 1.21 15.46 3.631 4.62 2.42 8.33 6.08 11.11 10.993 2.78 4.914 4.17 11.08 4.17 18.544v60.731h-23.33v-59.06c0-5.086-1.3-8.904-3.88-11.454-2.59-2.55-6.24-3.819-10.92-3.819-5.31 0-9.41 1.715-12.32 5.13-2.9 3.415-4.35 8.285-4.35 14.624v54.593h-23.32v-59.06c0-5.085-1.3-8.904-3.88-11.454-2.59-2.55-6.24-3.818-10.93-3.818-5.31 0-9.4 1.715-12.31 5.13-2.91 3.414-4.35 8.284-4.35 14.624v54.592h-23.327l-.028-.057ZM1188.52 132.873c-9.13 0-17.34-1.859-24.62-5.591-7.29-3.731-13.03-9.134-17.23-16.209-4.19-7.074-6.3-15.59-6.3-25.517v-2.982c0-9.942 2.09-18.443 6.3-25.517 4.2-7.075 9.94-12.478 17.23-16.21 7.27-3.731 15.49-5.59 24.62-5.59 9.14 0 17.34 1.859 24.63 5.59 7.27 3.732 13.02 9.135 17.22 16.21 4.2 7.074 6.29 15.59 6.29 25.517v2.982c0 9.942-2.1 18.443-6.29 25.517-4.19 7.075-9.93 12.478-17.22 16.209-7.29 3.732-15.49 5.591-24.63 5.591Zm0-20.863c7.16 0 13.08-2.335 17.77-6.988 4.7-4.654 7.03-11.34 7.03-20.028v-1.859c0-8.688-2.32-15.373-6.94-20.027-4.63-4.654-10.58-6.988-17.87-6.988-7.29 0-13.09 2.334-17.77 6.988-4.7 4.654-7.03 11.34-7.03 20.027v1.86c0 8.687 2.33 15.373 7.03 20.027 4.7 4.653 10.61 6.988 17.77 6.988h.01ZM1248.87 130.265v-92.4h22.96v10.431h3.33c1.36-3.731 3.61-6.454 6.76-8.198 3.15-1.743 6.81-2.608 11.01-2.608h11.11v20.863h-11.48c-5.93 0-10.8 1.585-14.62 4.755-3.82 3.17-5.74 8.04-5.74 14.624v52.533h-23.33ZM1322.93 167.525v-20.489h49.98c3.45 0 5.18-1.859 5.18-5.59v-23.284h-3.33c-.99 2.118-2.54 4.222-4.63 6.34-2.1 2.118-4.94 3.847-8.52 5.215-3.58 1.369-8.14 2.046-13.7 2.046-7.16 0-13.43-1.642-18.78-4.942-5.37-3.285-9.54-7.852-12.5-13.688-2.97-5.835-4.44-12.549-4.44-20.113V37.865h23.32v53.282c0 6.959 1.69 12.174 5.1 15.647 3.39 3.472 8.23 5.216 14.53 5.216 7.16 0 12.72-2.392 16.67-7.176 3.95-4.783 5.93-11.454 5.93-20.027V37.865h23.32V146.66c0 6.34-1.85 11.397-5.56 15.187-3.7 3.789-8.64 5.677-14.8 5.677H1322.93Z" /> + </g> + <defs> + <clipPath id="a"> + <path fill="#fff" d="M286 0h1190v168H286z" /> + </clipPath> + </defs> +</svg> diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts index b002aff7..1a22af04 100644 --- a/apps/browser-extension/utils/api.ts +++ b/apps/browser-extension/utils/api.ts @@ -114,7 +114,7 @@ export async function validateAuthToken(): Promise<boolean> { /** * Get user data from storage */ -export async function getUserData(): Promise<{ email?: string } | null> { +export async function getUserData(): Promise<{ email?: string; name?: string } | null> { try { return (await userData.getValue()) || null } catch (error) { diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index 14552865..5d61ffdf 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -15,6 +15,8 @@ export const API_ENDPOINTS = { */ export const ELEMENT_IDS = { TWITTER_IMPORT_BUTTON: "sm-twitter-import-button", + TWITTER_ONBOARDING_TOAST: "sm-twitter-onboarding-toast", + TWITTER_IMPORT_PROGRESS_TOAST: "sm-twitter-import-progress-toast", SUPERMEMORY_TOAST: "sm-toast", SUPERMEMORY_SAVE_BUTTON: "sm-save-button", SAVE_TWEET_ELEMENT: "sm-save-tweet-element", @@ -27,11 +29,21 @@ export const ELEMENT_IDS = { } as const /** + * Storage Keys for local + */ +export const STORAGE_KEYS = { + TWITTER_BOOKMARKS_ONBOARDING_SEEN: "sm_twitter_bookmarks_onboarding_seen", + TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL: "sm_twitter_bookmarks_import_intent_until", +} as const + +/** * UI Configuration */ export const UI_CONFIG = { BUTTON_SHOW_DELAY: 2000, // milliseconds TOAST_DURATION: 3000, // milliseconds + ONBOARDING_TOAST_DURATION: 6000, // milliseconds (6 seconds for progress bar) + IMPORT_INTENT_TTL: 2 * 60 * 1000, // 2 minutes TTL for import intent RATE_LIMIT_BASE_WAIT: 60000, // 1 minute PAGINATION_DELAY: 1000, // 1 second between requests AUTO_SEARCH_DEBOUNCE_DELAY: 1500, // milliseconds to wait after user stops typing @@ -75,6 +87,7 @@ export const MESSAGE_TYPES = { FETCH_PROJECTS: "sm-fetch-projects", SEARCH_SELECTION: "sm-search-selection", OPEN_SEARCH_PANEL: "sm-open-search-panel", + TWITTER_IMPORT_OPEN_MODAL: "sm-twitter-import-open-modal", } as const export const CONTEXT_MENU_IDS = { diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 3654f6f7..99f96cbb 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -545,10 +545,10 @@ export function createProjectSelectionModal( importButton.textContent = "Import" importButton.style.cssText = ` padding: 10px 16px; - border: none; + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; - background: #d1d5db; - color: #9ca3af; + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.3); font-size: 14px; font-weight: 500; cursor: not-allowed; @@ -577,10 +577,10 @@ export function createProjectSelectionModal( importButton.disabled = true importButton.style.cssText = ` padding: 10px 16px; - border: none; - border-radius: 8px; - background: #d1d5db; - color: #9ca3af; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.3); font-size: 14px; font-weight: 500; cursor: not-allowed; diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index ac51510e..6d689557 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ manifest: { name: "supermemory", homepage_url: "https://supermemory.ai", - version: "6.0.105", + version: "6.1.0", permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"], host_permissions: [ "*://x.com/*", |