@sylergydigital/issue-pin-sdk
v0.6.1
Published
Issue Pin — in-app visual feedback SDK
Readme
@sylergydigital/issue-pin-sdk
In-app visual feedback SDK for Issue Pin.
Installation
1. Install
npm install @sylergydigital/issue-pin-sdk2. Peer dependencies
Ensure these are installed in your app:
npm install react react-dom @supabase/supabase-js html2canvas lucide-reactThe public GitHub repo at sylergydigital/issue-pin-sdk is a read-only mirror. Source changes should be made in the private source repo and will sync across automatically.
react-router-dom is not required. The SDK tracks window.location for thread fetching and ?highlight_thread= without router context.
Usage
The SDK is disabled by default. Pass enabled={true} (or bind it to state) to activate the feedback UI.
import { useState } from "react";
import { IssuePin } from "@sylergydigital/issue-pin-sdk";
import { supabase } from "./lib/supabase"; // your app's Supabase client
function App() {
const [feedbackOn, setFeedbackOn] = useState(false);
return (
<>
<nav>
<button onClick={() => setFeedbackOn(f => !f)}>
💬 Feedback
</button>
</nav>
<Home />
{/* Recommended: pass supabaseClient for automatic user identity */}
<IssuePin
apiKey="ew_live_..."
enabled={feedbackOn}
supabaseClient={supabase}
/>
</>
);
}Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| apiKey | string | — | Required. Your workspace API key |
| enabled | boolean | false | Toggle feedback UI visibility without unmounting |
| supabaseClient | SupabaseClient | — | Recommended. Your app's Supabase client — auto-extracts user identity from the auth session and auto-federates users into the workspace |
| user.id | string | — | Manual user ID for attribution (overrides auto-detect) |
| user.email | string | — | Manual user email (overrides auto-detect) |
| user.displayName | string | — | Name shown on comments (overrides auto-detect) |
| allowPinOnPage | boolean | true | Allow creating new element-based pins from the launcher |
| allowScreenshot | boolean | true | Allow screenshot capture from the launcher |
| showHints | boolean | true | Show SDK-owned instructional hints and coachmarks, including the "Open Issue Pin" toast |
| scrollContainer | { key: string; ref: RefObject<HTMLElement> } | — | Store and render new element pins in a host-owned scroll container |
| resolveAnchor | (target: Element) => { key: string; selector?: string \| null } \| null | — | Resolve a stable logical anchor key for clicked elements |
| pinsVisible | boolean | true | Show historical thread pins independently from the rest of the SDK UI |
| buttonPosition | "bottom-right" \| "bottom-left" | "bottom-right" | Floating button position |
| renderLauncher | (props) => ReactNode | — | Replace the stock launcher while keeping SDK-owned state/actions |
| renderModalCaptureButton | (props) => ReactNode | — | Replace the default screenshot button injected into open dialogs |
| showModalCaptureButton | boolean | true | Control whether modal screenshot buttons are injected |
| mode | "view" \| "annotate" | — | Controlled annotate mode — use with onModeChange |
| onModeChange | (mode: FeedbackMode) => void | — | Called when annotate mode changes |
| feedbackActive | boolean | — | Deprecated. Use mode: annotate ↔ true, view ↔ false |
| onFeedbackActiveChange | (active: boolean) => void | — | Deprecated. Use onModeChange |
Migration (v0.x → coordinate-first pins)
x_position/y_positionremain the persisted source of truth.- Element pins now resolve in this order:
anchor_key→selector→ stored coordinates. - Without
scrollContainer, element pins stay document-relative. WithscrollContainer, new element pins are stored and rendered in that container's coordinate space. - Prefer
mode/onModeChangeinstead offeedbackActive/onFeedbackActiveChangefor clarity. - No
<BrowserRouter>wrapper is required for the SDK. The SDK listens tohistory.pushState/replaceStateandpopstateso thread fetching and?highlight_thread=stay in sync in SPAs without importing React Router.
TypeScript
import type { FeedbackMode } from "@sylergydigital/issue-pin-sdk";
// FeedbackMode = "view" | "annotate"Logical anchors in a scroll container
import { useRef } from "react";
import { IssuePin, useIssuePinAnchor } from "@sylergydigital/issue-pin-sdk";
function VisitRow({ visit }: { visit: { id: string; name: string } }) {
const ref = useIssuePinAnchor(`visit-row:${visit.id}`);
return <div ref={ref} data-visit-id={visit.id}>{visit.name}</div>;
}
function AppShell() {
const mainRef = useRef<HTMLDivElement>(null);
return (
<>
<div ref={mainRef} style={{ overflow: "auto", height: 600 }}>
<VisitRow visit={{ id: "123", name: "Annual review" }} />
</div>
<IssuePin
apiKey="ew_live_..."
enabled
scrollContainer={{ key: "dashboard-main", ref: mainRef }}
resolveAnchor={(target) => {
const row = target.closest("[data-visit-id]");
if (!row) return null;
return {
key: `visit-row:${row.getAttribute("data-visit-id")}`,
};
}}
/>
</>
);
}Layer stacking (z-index)
UI layers use a single internal scale exported as Z (pins → overlay → popover → launcher → screenshot modal → flash). If your app uses very high z-index values (e.g. 99999 modals), raise them above Z.launcher (or import Z and position relative to it):
import { Z } from "@sylergydigital/issue-pin-sdk";
// Example: host modal above the SDK launcher
<div style={{ zIndex: Z.launcher + 1 }}>...</div>Controlling visibility
Since enabled defaults to false, the SDK mounts its provider (preserving state and subscriptions) but renders no UI until activated. Common patterns:
Navbar toggle (recommended)
const [feedbackOn, setFeedbackOn] = useState(false);
<button onClick={() => setFeedbackOn(f => !f)}>Feedback</button>
<IssuePin apiKey="ew_live_..." enabled={feedbackOn} />Role-based
{["qa", "admin"].includes(currentUser.role) && (
<IssuePin apiKey="ew_live_..." enabled={feedbackOn} user={currentUser} />
)}Environment-based
{import.meta.env.VITE_APP_ENV !== "production" && (
<IssuePin apiKey="ew_live_..." enabled={feedbackOn} />
)}Advanced usage
Use the built-in customization props first. Composing low-level SDK internals directly is no longer the recommended way to customize launcher behavior.
Launcher action flags
<IssuePin
apiKey="ew_live_..."
supabaseClient={supabase}
enabled={feedbackOn}
allowPinOnPage={false}
/>If only one launcher action is enabled, the stock launcher triggers it directly instead of opening the menu.
Compose feature flags
<IssuePin
apiKey="ew_live_..."
enabled={feedbackEnabled}
allowPinOnPage={pinOnPageEnabled}
allowScreenshot={screenshotEnabled}
pinsVisible={pinsVisible}
showHints={false}
/>Hide historical pins without disabling feedback
<IssuePin
apiKey="ew_live_..."
supabaseClient={supabase}
enabled
pinsVisible={showHistoricalPins}
/>Custom launcher
<IssuePin
apiKey="ew_live_..."
supabaseClient={supabase}
enabled
renderLauncher={({ mode, canPinOnPage, canScreenshot, toggleMenu, enterPinMode, exitPinMode, startScreenshotCapture }) => (
<div style={{ position: "fixed", right: 24, bottom: 24 }}>
{mode === "annotate" ? (
<button onClick={exitPinMode}>Exit pin mode</button>
) : canPinOnPage ? (
<button onClick={enterPinMode}>Pin on page</button>
) : canScreenshot ? (
<button onClick={() => void startScreenshotCapture()}>Screenshot</button>
) : (
<button onClick={toggleMenu}>Feedback</button>
)}
</div>
)}
/>Custom modal screenshot button
<IssuePin
apiKey="ew_live_..."
supabaseClient={supabase}
enabled
renderModalCaptureButton={({ captureScreenshot }) => (
<button
onClick={() => void captureScreenshot()}
style={{ position: "absolute", top: 12, right: 12 }}
>
Capture
</button>
)}
/>Low-level composition (advanced only)
Import individual components only if you need full custom assembly:
import { FeedbackProvider, FeedbackButton, FeedbackOverlay, ThreadPins } from "@sylergydigital/issue-pin-sdk";
function App() {
return (
<FeedbackProvider apiKey="ew_live_...">
<YourApp />
<FeedbackOverlay />
<ThreadPins />
<FeedbackButton />
</FeedbackProvider>
);
}ThreadPins renders markers at stored document-percentage coordinates and updates on scroll/resize (no CSS selector resolution).
User identity & auto-federation
Supabase auth (recommended — zero config)
If your app uses Supabase for authentication, just pass your existing client. The SDK:
- Auto-detects identity from
auth.getSession()— no manual user props needed - Auto-federates the user into the Issue Pin workspace as a
commenter— they appear in the workspace member list and their comments are properly attributed - Uses the dashboard org
site_urlfor thread deep-links when a user clicks an in-app pin
import { supabase } from "./lib/supabase";
<IssuePin apiKey="ew_live_..." supabaseClient={supabase} enabled={feedbackOn} />No backend code, edge functions, or sync secrets needed. The SDK handles everything transparently.
How auto-federation works
When a user is detected via supabaseClient:
- The SDK calls the
sdk-federateendpoint with the API key + user identity - The endpoint validates the API key, resolves the workspace, and upserts the user as a
client_federatedworkspace member withcommenterrole - An identity source (
sdk:<workspace-slug>) is auto-created if it doesn't exist
sequenceDiagram
participant App as Host App
participant SDK as IssuePin SDK
participant EF as sdk-federate (Edge Fn)
participant DB as Database
App->>SDK: Mount with apiKey + supabaseClient
SDK->>SDK: auth.getSession() → extract id, email, name
SDK->>EF: POST { apiKey, externalId, email, displayName }
EF->>DB: SHA-256(apiKey) → resolve_api_key()
DB-->>EF: workspace_id
EF->>DB: Upsert user_identities
EF->>DB: Upsert workspace_members (commenter role)
DB-->>EF: OK
EF-->>SDK: { success, user_id, workspace_id }
SDK-->>App: User can now post feedback
Note over SDK: Result cached — runs once per sessionOther auth providers (manual)
For non-Supabase auth systems, pass identity props directly:
// NextAuth / Auth.js
const { data: session } = useSession();
<IssuePin apiKey="ew_live_..." user={{ email: session?.user?.email, displayName: session?.user?.name }} />// Clerk
const { user } = useUser();
<IssuePin apiKey="ew_live_..." user={{ id: user?.id, email: user?.primaryEmailAddress?.emailAddress, displayName: user?.fullName }} />If neither supabaseClient nor user props are provided, a console warning will remind you during development.
Server-to-server federation (advanced)
For bulk provisioning or non-Supabase apps, use the federate-user edge function with a sync_secret. See the Federation docs for details.
Security
The API key is a publishable key — safe to include in client-side code. It only grants scoped access to create threads and comments for the associated workspace, enforced server-side via Row Level Security.
AI Agent Prompt
Paste this into Claude Code, Cursor, or Codex to integrate the SDK automatically:
Add the Issue Pin feedback SDK to this React app.
Install: npm install @sylergydigital/issue-pin-sdk
Peer deps: npm install @supabase/supabase-js html2canvas lucide-react
.npmrc setup (required for GitHub Packages):
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
@sylergydigital:registry=https://npm.pkg.github.com
Integration:
1. Import { IssuePin } from "@sylergydigital/issue-pin-sdk"
2. Add a boolean state: const [feedbackOn, setFeedbackOn] = useState(false)
3. Place <IssuePin apiKey={import.meta.env.VITE_ISSUE_PIN_API_KEY} enabled={feedbackOn} supabaseClient={supabase} /> in your app tree (e.g. layout root)
4. Add a toggle button: <button onClick={() => setFeedbackOn(f => !f)}>💬 Feedback</button>
5. Import your app's Supabase client: import { supabase } from "./lib/supabase"
Constraints:
- enabled defaults to false; SDK mounts but renders no UI until true
- apiKey is a publishable key (safe for client-side code)
- Pass supabaseClient for automatic user identity (recommended for Supabase auth apps)
- Alternative: pass user={{ email: "[email protected]", displayName: "Name" }} for non-Supabase auth
- Optional feature flags: allowPinOnPage, allowScreenshot, pinsVisible, showHints
- Use showHints={false} if the host app wants to suppress SDK instructional coachmarks such as the "Open Issue Pin" toast