feat: update Obsidian connector UI and improve user instructions

This commit is contained in:
Anish Sarkar 2026-04-22 06:07:38 +05:30
parent 9ecccc5403
commit ae264290d0
3 changed files with 117 additions and 134 deletions

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { Check, Copy, Download, Info, KeyRound, Settings2 } from "lucide-react"; import { Check, Copy, Info } from "lucide-react";
import { type FC, useCallback, useRef, useState } from "react"; import { type FC, useCallback, useRef, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -55,145 +55,126 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
that just closes the dialog (see component-level docstring). */} that just closes the dialog (see component-level docstring). */}
<form id="obsidian-connect-form" onSubmit={handleSubmit} /> <form id="obsidian-connect-form" onSubmit={handleSubmit} />
<Alert className="border-purple-500/30 bg-purple-500/10 p-2 sm:p-3 dark:bg-purple-500/10"> <Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3">
<Info className="size-4 shrink-0 text-purple-500" /> <Info className="size-4 shrink-0 text-purple-500" />
<AlertTitle className="text-xs sm:text-sm">Plugin-based sync</AlertTitle> <AlertTitle className="text-xs sm:text-sm">Plugin-based sync</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs"> <AlertDescription className="text-[10px] sm:text-xs">
SurfSense now syncs Obsidian via an official plugin that runs inside SurfSense now syncs Obsidian via an official plugin that runs inside
Obsidian itself. Works on desktop and mobile, in cloud and self-hosted Obsidian itself. Works on desktop and mobile, in cloud and self-hosted
deployments no server-side vault mounts required. deployments.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
{/* Step 1 — Install plugin */}
<section className="rounded-xl border border-border bg-slate-400/5 p-3 sm:p-6 dark:bg-white/5"> <section className="rounded-xl border border-border bg-slate-400/5 p-3 sm:p-6 dark:bg-white/5">
<header className="mb-3 flex items-center gap-2"> <div className="space-y-5 sm:space-y-6">
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium"> {/* Step 1 — Install plugin */}
1 <article>
</div> <header className="mb-3 flex items-center gap-2">
<h3 className="text-sm font-medium sm:text-base">Install the plugin</h3> <div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
</header> 1
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs"> </div>
Grab the latest SurfSense plugin release. Once it's in the community <h3 className="text-sm font-medium sm:text-base">Install the plugin</h3>
store, you'll also be able to install it from{" "} </header>
<span className="font-medium">Settings Community plugins</span>{" "} <p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
inside Obsidian. Grab the latest SurfSense plugin release. Once it's in the community
</p> store, you'll also be able to install it from{" "}
<a <span className="font-medium">Settings Community plugins</span>{" "}
href={PLUGIN_RELEASES_URL} inside Obsidian.
target="_blank"
rel="noopener noreferrer"
className="inline-flex"
>
<Button type="button" variant="outline" size="sm" className="gap-2">
<Download className="size-3.5" />
Open plugin releases
</Button>
</a>
</section>
{/* Step 2 — Copy API key */}
<section className="rounded-xl border border-border bg-slate-400/5 p-3 sm:p-6 dark:bg-white/5">
<header className="mb-3 flex items-center gap-2">
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
2
</div>
<h3 className="text-sm font-medium sm:text-base">
Copy your API key
</h3>
<KeyRound className="ml-auto size-4 text-muted-foreground" />
</header>
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
Paste this into the plugin's <span className="font-medium">API token</span>{" "}
setting. The token expires after 24 hours; long-lived personal access
tokens are coming in a future release.
</p>
{isLoading ? (
<div className="h-10 w-full animate-pulse rounded-md border border-border/60 bg-muted/30" />
) : apiKey ? (
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
<p className="cursor-text select-all whitespace-nowrap font-mono text-[10px] text-muted-foreground">
{apiKey}
</p>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={copyToClipboard}
className="size-7 shrink-0 text-muted-foreground hover:text-foreground"
aria-label={copied ? "Copied" : "Copy API key"}
>
{copied ? (
<Check className="size-3.5 text-green-500" />
) : (
<Copy className="size-3.5" />
)}
</Button>
</div>
) : (
<p className="text-center text-xs text-muted-foreground/60">
No API key available try refreshing the page.
</p>
)}
</section>
{/* Step 3 — Server URL */}
<section className="rounded-xl border border-border bg-slate-400/5 p-3 sm:p-6 dark:bg-white/5">
<header className="mb-3 flex items-center gap-2">
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
3
</div>
<h3 className="text-sm font-medium sm:text-base">
Point the plugin at this server
</h3>
</header>
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
Paste this URL into the plugin's <span className="font-medium">Server URL</span>{" "}
setting. We auto-detect it from your current dashboard origin.
</p>
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
<p className="cursor-text select-all whitespace-nowrap font-mono text-[10px] text-muted-foreground">
{BACKEND_URL}
</p> </p>
</div> <a
<Button href={PLUGIN_RELEASES_URL}
type="button" target="_blank"
variant="ghost" rel="noopener noreferrer"
size="icon" className="inline-flex"
onClick={copyServerUrl} >
className="size-7 shrink-0 text-muted-foreground hover:text-foreground" <Button type="button" variant="secondary" size="sm" className="gap-2 text-xs sm:text-sm">
aria-label={copiedUrl ? "Copied" : "Copy server URL"} Open plugin releases
> </Button>
{copiedUrl ? ( </a>
<Check className="size-3.5 text-green-500" /> </article>
) : (
<Copy className="size-3.5" />
)}
</Button>
</div>
</section>
{/* Step 4 — Pick search space */} <div className="h-px bg-border/60" />
<section className="rounded-xl border border-border bg-slate-400/5 p-3 sm:p-6 dark:bg-white/5">
<header className="mb-3 flex items-center gap-2"> {/* Step 2 — Copy API key */}
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium"> <article>
4 <header className="mb-3 flex items-center gap-2">
</div> <div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
<h3 className="text-sm font-medium sm:text-base"> 2
Pick this search space </div>
</h3> <h3 className="text-sm font-medium sm:text-base">Copy your API key</h3>
<Settings2 className="ml-auto size-4 text-muted-foreground" /> </header>
</header> <p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
<p className="text-[11px] text-muted-foreground sm:text-xs"> Paste this into the plugin's <span className="font-medium">API token</span>{" "}
In the plugin's <span className="font-medium">Search space</span>{" "} setting. The token expires after 24 hours. Long-lived personal access
setting, choose the search space you want this vault to sync into. tokens are coming in a future release.
The connector will appear here automatically once the plugin makes </p>
its first sync.
</p> {isLoading ? (
<div className="h-10 w-full animate-pulse rounded-md border border-border/60 bg-muted/30" />
) : apiKey ? (
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
<p className="cursor-text select-all whitespace-nowrap font-mono text-[10px] text-muted-foreground">
{apiKey}
</p>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={copyToClipboard}
className="size-7 shrink-0 text-muted-foreground hover:text-foreground"
aria-label={copied ? "Copied" : "Copy API key"}
>
{copied ? (
<Check className="size-3.5 text-green-500" />
) : (
<Copy className="size-3.5" />
)}
</Button>
</div>
) : (
<p className="text-center text-xs text-muted-foreground/60">
No API key available try refreshing the page.
</p>
)}
</article>
<div className="h-px bg-border/60" />
{/* Step 3 — Server URL */}
<article>
<header className="mb-3 flex items-center gap-2">
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
3
</div>
<h3 className="text-sm font-medium sm:text-base">Point the plugin at this server</h3>
</header>
<p className="text-[11px] text-muted-foreground sm:text-xs">
For SurfSense Cloud, use the default <span className="font-medium">surfsense.com</span>.
If you are self-hosting, set the plugin's{" "}
<span className="font-medium">Server URL</span> to your frontend domain.
</p>
</article>
<div className="h-px bg-border/60" />
{/* Step 4 — Pick search space */}
<article>
<header className="mb-3 flex items-center gap-2">
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
4
</div>
<h3 className="text-sm font-medium sm:text-base">Pick this search space</h3>
</header>
<p className="text-[11px] text-muted-foreground sm:text-xs">
In the plugin's <span className="font-medium">Search space</span>{" "}
setting, choose the search space you want this vault to sync into.
The connector will appear here automatically once the plugin makes
its first sync.
</p>
</article>
</div>
</section> </section>
{getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR) && ( {getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR) && (

View file

@ -130,20 +130,20 @@ const PluginStats: FC<{ config: Record<string, unknown> }> = ({ config }) => {
<Info className="size-4 shrink-0 text-emerald-500" /> <Info className="size-4 shrink-0 text-emerald-500" />
<AlertTitle className="text-xs sm:text-sm">Plugin connected</AlertTitle> <AlertTitle className="text-xs sm:text-sm">Plugin connected</AlertTitle>
<AlertDescription className="text-[11px] sm:text-xs"> <AlertDescription className="text-[11px] sm:text-xs">
Edits in Obsidian sync over HTTPS. To stop syncing, disable or uninstall the plugin in Your notes stay synced automatically. To stop syncing, disable or uninstall the plugin in
Obsidian, or delete this connector. Obsidian, or delete this connector.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<div className="rounded-xl border border-border bg-slate-400/5 p-3 sm:p-6 dark:bg-white/5"> <div className="rounded-xl bg-slate-400/5 p-3 sm:p-6 dark:bg-white/5">
<h3 className="mb-3 text-sm font-medium sm:text-base">Vault status</h3> <h3 className="mb-3 text-sm font-medium sm:text-base">Vault status</h3>
<dl className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <dl className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{tileRows.map((stat) => ( {tileRows.map((stat) => (
<div <div
key={stat.label} key={stat.label}
className="rounded-lg border border-slate-400/20 bg-background/50 p-3" className="rounded-lg bg-background/50 p-3"
> >
<dt className="text-[10px] uppercase tracking-wide text-muted-foreground sm:text-xs"> <dt className="text-xs tracking-wide text-muted-foreground sm:text-sm">
{stat.label} {stat.label}
</dt> </dt>
<dd className="mt-1 truncate text-xs font-medium sm:text-sm">{stat.value}</dd> <dd className="mt-1 truncate text-xs font-medium sm:text-sm">{stat.value}</dd>

View file

@ -151,7 +151,9 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
<span className={isSubmitting ? "opacity-0" : ""}> <span className={isSubmitting ? "opacity-0" : ""}>
{connectorType === "MCP_CONNECTOR" {connectorType === "MCP_CONNECTOR"
? "Connect" ? "Connect"
: `Connect ${getConnectorTypeDisplay(connectorType)}`} : connectorType === "OBSIDIAN_CONNECTOR"
? "Done"
: `Connect ${getConnectorTypeDisplay(connectorType)}`}
</span> </span>
{isSubmitting && <Spinner size="sm" className="absolute" />} {isSubmitting && <Spinner size="sm" className="absolute" />}
</Button> </Button>