>(({ className, ...props }, ref) => (
-
>(({ className, ...props }, ref) => (
+
-
-
-
-
-
+ "plasmo-flex plasmo-items-center plasmo-justify-center plasmo-text-current"
+ )}>
+
+
+
+
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index fbbec96..97d07ed 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -1,6 +1,8 @@
+"use client"
+
+import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
-import * as React from "react"
import { cn } from "~common/lib/utils"
@@ -39,7 +41,8 @@ const DialogContent = React.forwardRef<
"plasmo-fixed plasmo-left-[50%] plasmo-top-[50%] plasmo-z-50 plasmo-grid plasmo-w-full plasmo-max-w-lg plasmo-translate-x-[-50%] plasmo-translate-y-[-50%] plasmo-gap-4 plasmo-border plasmo-bg-background plasmo-p-6 plasmo-shadow-lg plasmo-duration-200 data-[state=open]:plasmo-animate-in data-[state=closed]:plasmo-animate-out data-[state=closed]:plasmo-fade-out-0 data-[state=open]:plasmo-fade-in-0 data-[state=closed]:plasmo-zoom-out-95 data-[state=open]:plasmo-zoom-in-95 data-[state=closed]:plasmo-slide-out-to-left-1/2 data-[state=closed]:plasmo-slide-out-to-top-[48%] data-[state=open]:plasmo-slide-in-from-left-1/2 data-[state=open]:plasmo-slide-in-from-top-[48%] sm:plasmo-rounded-lg",
className
)}
- {...props}>
+ {...props}
+ >
{children}
@@ -115,5 +118,5 @@ export {
DialogHeader,
DialogFooter,
DialogTitle,
- DialogDescription
+ DialogDescription,
}
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
index 4c1ad06..89f05f1 100644
--- a/src/components/ui/index.ts
+++ b/src/components/ui/index.ts
@@ -1,10 +1,11 @@
-export * from "./button"
-export * from "./card"
-export * from "./checkbox"
-export * from "./dialog"
-export * from "./input"
-export * from "./label"
-export * from "./separator"
-export * from "./switch"
-export * from "./tabs"
-export * from "./tooltip"
+export * from './button';
+export * from './card';
+export * from './checkbox';
+export * from './dialog';
+export * from './input';
+export * from './label';
+export * from './select';
+export * from './separator';
+export * from './switch';
+export * from './tabs';
+export * from './tooltip';
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index 1486f33..a90834e 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -2,16 +2,13 @@ import * as React from "react"
import { cn } from "~common/lib/utils"
-export interface InputProps
- extends React.InputHTMLAttributes {}
-
-const Input = React.forwardRef(
+const Input = React.forwardRef>(
({ className, type, ...props }, ref) => {
return (
,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:plasmo-line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
index 7c79a50..ee63ece 100644
--- a/src/components/ui/separator.tsx
+++ b/src/components/ui/separator.tsx
@@ -1,5 +1,5 @@
-import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "~common/lib/utils"
@@ -17,9 +17,7 @@ const Separator = React.forwardRef<
orientation={orientation}
className={cn(
"plasmo-shrink-0 plasmo-bg-border",
- orientation === "horizontal"
- ? "plasmo-h-[1px] plasmo-w-full"
- : "plasmo-h-full plasmo-w-[1px]",
+ orientation === "horizontal" ? "plasmo-h-[1px] plasmo-w-full" : "plasmo-h-full plasmo-w-[1px]",
className
)}
{...props}
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
index f5f5341..d49b6ee 100644
--- a/src/components/ui/switch.tsx
+++ b/src/components/ui/switch.tsx
@@ -1,5 +1,7 @@
-import * as SwitchPrimitives from "@radix-ui/react-switch"
+"use client"
+
import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "~common/lib/utils"
@@ -13,7 +15,8 @@ const Switch = React.forwardRef<
className
)}
{...props}
- ref={ref}>
+ ref={ref}
+ >
("export")
+ /**
+ * Handles export functionality
+ */
+ const handleExport = async (format: ExportFormat) => {
+ try {
+ let content: string
+ let mimeType: string
+ let fileName: string
+
+ switch (format) {
+ case "html":
+ content = await exportToHTML()
+ mimeType = "text/html"
+ fileName = chrome.i18n.getMessage("exportFileNameHTML")
+ break
+ case "json":
+ const jsonData = await exportToJSON()
+ content = JSON.stringify(jsonData, null, 2)
+ mimeType = "application/json"
+ fileName = chrome.i18n.getMessage("exportFileNameJSON")
+ break
+ case "csv":
+ content = await exportToCSV()
+ mimeType = "text/csv"
+ fileName = chrome.i18n.getMessage("exportFileNameCSV")
+ break
+ }
+
+ const blob = new Blob([content], { type: mimeType })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement("a")
+ link.href = url
+ link.download = fileName
+ link.click()
+ } catch (error) {
+ console.error(chrome.i18n.getMessage("exportError"), error)
+ }
+ }
+
/**
* Handles tab change
* @param {TabValue} value - The new tab value
@@ -45,13 +85,12 @@ function IndexPopup(): JSX.Element {
activeTab === "export" ? "" : "plasmo-hidden"
}`}>
-
-
+
),
- [activeTab]
+ [activeTab, handleExport]
)
/**
@@ -72,7 +111,8 @@ function IndexPopup(): JSX.Element {
)
return (
-
+
+
{chrome.i18n.getMessage("extensionName")}
@@ -92,10 +132,11 @@ function IndexPopup(): JSX.Element {
{renderImportTab()}
-
+
{chrome.i18n.getMessage("extensionDescription")}
+
)
}
diff --git a/src/style.css b/src/style.css
index 1fae57d..87981a2 100644
--- a/src/style.css
+++ b/src/style.css
@@ -3,58 +3,66 @@
@tailwind utilities;
@layer base {
- :root {
- --background: 0 0% 100%;
- --foreground: 0 0% 3.9%;
- --card: 0 0% 100%;
- --card-foreground: 0 0% 3.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 0 0% 3.9%;
- --primary: 0 0% 9%;
- --primary-foreground: 0 0% 98%;
- --secondary: 0 0% 96.1%;
- --secondary-foreground: 0 0% 9%;
- --muted: 0 0% 96.1%;
- --muted-foreground: 0 0% 45.1%;
- --accent: 0 0% 96.1%;
- --accent-foreground: 0 0% 9%;
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 0 0% 98%;
- --border: 0 0% 89.8%;
- --input: 0 0% 89.8%;
- --ring: 0 0% 3.9%;
- --radius: 0.5rem;
- --chart-1: 12 76% 61%;
- --chart-2: 173 58% 39%;
- --chart-3: 197 37% 24%;
- --chart-4: 43 74% 66%;
- --chart-5: 27 87% 67%;
- }
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
- .dark {
- --background: 0 0% 3.9%;
- --foreground: 0 0% 98%;
- --card: 0 0% 3.9%;
- --card-foreground: 0 0% 98%;
- --popover: 0 0% 3.9%;
- --popover-foreground: 0 0% 98%;
- --primary: 0 0% 98%;
- --primary-foreground: 0 0% 9%;
- --secondary: 0 0% 14.9%;
- --secondary-foreground: 0 0% 98%;
- --muted: 0 0% 14.9%;
- --muted-foreground: 0 0% 63.9%;
- --accent: 0 0% 14.9%;
- --accent-foreground: 0 0% 98%;
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 0 0% 98%;
- --border: 0 0% 14.9%;
- --input: 0 0% 14.9%;
- --ring: 0 0% 83.1%;
- --chart-1: 220 70% 50%;
- --chart-2: 160 60% 45%;
- --chart-3: 30 80% 55%;
- --chart-4: 280 65% 60%;
- --chart-5: 340 75% 55%;
- }
+@layer base {
+ * {
+ @apply plasmo-border-border;
+ }
+ body {
+ @apply plasmo-bg-background plasmo-text-foreground;
+ }
}
diff --git a/src/tabs/advanced-export.tsx b/src/tabs/advanced-export.tsx
index c87746c..62e66cd 100644
--- a/src/tabs/advanced-export.tsx
+++ b/src/tabs/advanced-export.tsx
@@ -1,11 +1,11 @@
import { useCallback, useEffect, useRef, useState } from "react"
-import { exportToHTML, exportToJSON } from "~common/lib"
+import { exportToCSV, exportToHTML, exportToJSON } from "~common/lib"
import type {
BookmarkTreeHandle,
ExtendedBookmarkTreeNode
} from "~common/types"
-import { BookmarkTree, Header } from "~components"
+import { BookmarkTree, Header, ThemeProvider } from "~components"
import "~style.css"
@@ -105,9 +105,9 @@ export default function AdvancedExportPage(): JSX.Element {
/**
* Export selected bookmarks in the specified format
- * @param {("html" | "json")} format - The format to export bookmarks in
+ * @param {("html" | "json" | "csv")} format - The format to export bookmarks in
*/
- const handleExport = async (format: "html" | "json") => {
+ const handleExport = async (format: "html" | "json" | "csv") => {
if (!bookmarkTreeRef.current) {
console.error(chrome.i18n.getMessage("bookmarkTreeRefNotAvailable"))
return
@@ -145,12 +145,12 @@ export default function AdvancedExportPage(): JSX.Element {
/**
* Export bookmarks based on the specified format and configuration
- * @param {("html" | "json")} format - The format to export bookmarks in
+ * @param {("html" | "json" | "csv")} format - The format to export bookmarks in
* @param {Object} config - Export configuration
* @returns {Promise<{exportedData: string, fileName: string, mimeType: string}>} Export result
*/
const exportBookmarks = async (
- format: "html" | "json",
+ format: "html" | "json" | "csv",
config: {
selectedBookmarks: ExtendedBookmarkTreeNode[]
includeIconData: boolean
@@ -161,32 +161,55 @@ export default function AdvancedExportPage(): JSX.Element {
hideParentFolder: boolean
}
): Promise<{ exportedData: string; fileName: string; mimeType: string }> => {
- if (format === "html") {
- const exportedData = await exportToHTML(
- config.selectedBookmarks,
- config.includeIconData,
- config.includeDateAdded,
- config.includeDateLastUsed,
- config.includeDateGroupModified,
- config.hideOtherBookmarks,
- config.hideParentFolder
- )
- return { exportedData, fileName: "bookmarks.html", mimeType: "text/html" }
- } else {
- const jsonData = await exportToJSON(
- config.selectedBookmarks,
- config.includeIconData,
- config.includeDateAdded,
- config.includeDateLastUsed,
- config.includeDateGroupModified,
- config.hideOtherBookmarks,
- config.hideParentFolder
- )
- const exportedData = JSON.stringify(jsonData, null, 2)
- return {
- exportedData,
- fileName: "bookmarks.json",
- mimeType: "application/json"
+ let exportedData: string
+
+ switch (format) {
+ case "html": {
+ exportedData = await exportToHTML(
+ config.selectedBookmarks,
+ config.includeIconData,
+ config.includeDateAdded,
+ config.includeDateLastUsed,
+ config.includeDateGroupModified,
+ config.hideOtherBookmarks,
+ config.hideParentFolder
+ )
+ return {
+ exportedData,
+ fileName: chrome.i18n.getMessage("exportFileNameHTML"),
+ mimeType: "text/html"
+ }
+ }
+ case "json": {
+ const jsonData = await exportToJSON(
+ config.selectedBookmarks,
+ config.includeIconData,
+ config.includeDateAdded,
+ config.includeDateLastUsed,
+ config.includeDateGroupModified,
+ config.hideOtherBookmarks,
+ config.hideParentFolder
+ )
+ exportedData = JSON.stringify(jsonData, null, 2)
+ return {
+ exportedData,
+ fileName: chrome.i18n.getMessage("exportFileNameJSON"),
+ mimeType: "application/json"
+ }
+ }
+ case "csv": {
+ exportedData = await exportToCSV(
+ config.selectedBookmarks,
+ config.includeIconData,
+ config.includeDateAdded,
+ config.includeDateLastUsed,
+ config.includeDateGroupModified
+ )
+ return {
+ exportedData,
+ fileName: chrome.i18n.getMessage("exportFileNameCSV"),
+ mimeType: "text/csv"
+ }
}
}
}
@@ -212,24 +235,26 @@ export default function AdvancedExportPage(): JSX.Element {
}
return (
-
-
-
+
)
}
diff --git a/src/tabs/update.tsx b/src/tabs/update.tsx
index f65e9c4..225253a 100644
--- a/src/tabs/update.tsx
+++ b/src/tabs/update.tsx
@@ -1,5 +1,6 @@
import logo from "data-base64:assets/icon.png"
import { Star } from "lucide-react"
+import { ThemeProvider } from "~components"
import "~style.css"
@@ -16,6 +17,15 @@ interface ChangelogEntry {
}
const changelog: ChangelogEntry[] = [
+ {
+ version: "1.3.0",
+ date: chrome.i18n.getMessage("changelog_1_3_0_date"),
+ changes: [
+ {
+ description: chrome.i18n.getMessage("changelog_1_3_0_1")
+ }
+ ]
+ },
{
version: "1.2.0",
date: chrome.i18n.getMessage("changelog_1_2_0_date"),
@@ -198,11 +208,13 @@ export default function UpdatePage(): JSX.Element {
)
return (
-
- {renderHeader()}
- {renderFeedbackLink()}
- {renderChangelog()}
- {renderFooter()}
-
+
+
+ {renderHeader()}
+ {renderFeedbackLink()}
+ {renderChangelog()}
+ {renderFooter()}
+
+
)
}
diff --git a/src/tabs/welcome.tsx b/src/tabs/welcome.tsx
index 8a2f7c2..b519df7 100644
--- a/src/tabs/welcome.tsx
+++ b/src/tabs/welcome.tsx
@@ -1,7 +1,15 @@
import logo from "data-base64:assets/icon.png"
-import { BookmarkPlus, FileJson, FileText, Settings, Star } from "lucide-react"
+import {
+ BookmarkPlus,
+ FileJson,
+ FileSpreadsheet,
+ FileText,
+ Languages,
+ Settings,
+ Star
+} from "lucide-react"
-import { Card, CardHeader, FeatureCard } from "~components"
+import { Card, CardHeader, FeatureCard, ThemeProvider } from "~components"
import "~style.css"
@@ -37,8 +45,8 @@ export default function WelcomePage(): JSX.Element {
* @returns {JSX.Element} Card with instructions to start using the extension
*/
const renderGettingStarted = (): JSX.Element => (
-
-
+
+
{chrome.i18n.getMessage("gettingStarted")}
@@ -60,6 +68,11 @@ export default function WelcomePage(): JSX.Element {
title={chrome.i18n.getMessage("exportToHTML")}
description={chrome.i18n.getMessage("exportToHTMLDescription")}
/>
+
+
{chrome.i18n.getMessage("compatibleBrowsers")}
@@ -98,7 +116,7 @@ export default function WelcomePage(): JSX.Element {
const renderFooter = (): JSX.Element => (