aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/browser-extension/components/icons.tsx18
-rw-r--r--apps/browser-extension/entrypoints/content/index.ts3
-rw-r--r--apps/browser-extension/entrypoints/content/twitter.ts678
-rw-r--r--apps/browser-extension/entrypoints/popup/App.tsx377
-rw-r--r--apps/browser-extension/entrypoints/popup/style.css2
-rw-r--r--apps/browser-extension/public/icon-48.pngbin110647 -> 12541 bytes
-rw-r--r--apps/browser-extension/public/logo-fullmark.svg15
-rw-r--r--apps/browser-extension/utils/api.ts2
-rw-r--r--apps/browser-extension/utils/constants.ts13
-rw-r--r--apps/browser-extension/utils/ui-components.ts14
-rw-r--r--apps/browser-extension/wxt.config.ts2
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' &gt; 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
index bf132193..10f7edd1 100644
--- a/apps/browser-extension/public/icon-48.png
+++ b/apps/browser-extension/public/icon-48.png
Binary files differ
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/*",