@tacktext/widget 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @tacktext/widget
2
+
3
+ Drop-in commenting for web prototypes. Pin comments to any element, capture screenshots, and collaborate with your team — like Figma comments, but for live sites.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx @tacktext/widget init
9
+ ```
10
+
11
+ This detects your framework, generates a project ID, and adds Tack to your entry file. Deploy your app and share the link — anyone with the link can leave feedback.
12
+
13
+ ## Manual Setup
14
+
15
+ ```bash
16
+ npm install @tacktext/widget
17
+ ```
18
+
19
+ ```typescript
20
+ import { Tack } from '@tacktext/widget'
21
+
22
+ Tack.init({ projectId: 'your-project-id' })
23
+ ```
24
+
25
+ Or via script tag:
26
+
27
+ ```html
28
+ <script src="https://unpkg.com/@tacktext/widget"></script>
29
+ <script>Tack.init({ projectId: 'your-project-id' })</script>
30
+ ```
31
+
32
+ ## How It Works
33
+
34
+ - Click the comment bubble to enter comment mode
35
+ - Click any element to pin a comment
36
+ - Comments are anchored to elements and reposition on scroll/resize
37
+ - Screenshots are captured automatically with each comment
38
+ - Sign in with Google to get persistent identity and presence indicators
39
+ - All data syncs in real-time across viewers
40
+
41
+ ## Auth
42
+
43
+ Tack uses Google OAuth scoped to `@plaid.com`. Unauthenticated users can view comments but must sign in to post.
44
+
45
+ ## Stack
46
+
47
+ - Widget runs in a Shadow DOM — won't interfere with your app's styles
48
+ - Backend: Next.js on Vercel + Supabase (Postgres, Auth, Realtime, Storage)
49
+ - Screenshots via html2canvas (lazy-loaded, ~1.4MB on first capture)
package/dist/cli.js ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var fs = __toESM(require("fs"));
28
+ var path = __toESM(require("path"));
29
+ var readline = __toESM(require("readline"));
30
+ var crypto = __toESM(require("crypto"));
31
+ async function prompt(question) {
32
+ const rl = readline.createInterface({
33
+ input: process.stdin,
34
+ output: process.stdout
35
+ });
36
+ return new Promise((resolve) => {
37
+ rl.question(question, (answer) => {
38
+ rl.close();
39
+ resolve(answer.trim());
40
+ });
41
+ });
42
+ }
43
+ function generateSlug(name) {
44
+ const base = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
45
+ const suffix = crypto.randomBytes(3).toString("hex");
46
+ return `${base}-${suffix}`;
47
+ }
48
+ function detectFramework() {
49
+ const cwd = process.cwd();
50
+ const pkgPath = path.join(cwd, "package.json");
51
+ if (!fs.existsSync(pkgPath)) {
52
+ return "unknown";
53
+ }
54
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
55
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
56
+ if (deps["next"]) return "nextjs";
57
+ if (deps["vite"]) return "vite";
58
+ if (deps["react-scripts"]) return "cra";
59
+ return "unknown";
60
+ }
61
+ function findEntryFile(framework) {
62
+ const cwd = process.cwd();
63
+ const candidates = {
64
+ nextjs: [
65
+ "src/app/layout.tsx",
66
+ "src/app/layout.js",
67
+ "app/layout.tsx",
68
+ "app/layout.js",
69
+ "src/pages/_app.tsx",
70
+ "src/pages/_app.js",
71
+ "pages/_app.tsx",
72
+ "pages/_app.js"
73
+ ],
74
+ vite: [
75
+ "src/main.tsx",
76
+ "src/main.ts",
77
+ "src/main.jsx",
78
+ "src/main.js"
79
+ ],
80
+ cra: [
81
+ "src/index.tsx",
82
+ "src/index.ts",
83
+ "src/index.jsx",
84
+ "src/index.js"
85
+ ],
86
+ unknown: [
87
+ "src/main.tsx",
88
+ "src/main.ts",
89
+ "src/index.tsx",
90
+ "src/index.ts",
91
+ "src/App.tsx",
92
+ "src/App.jsx",
93
+ "index.html"
94
+ ]
95
+ };
96
+ for (const candidate of candidates[framework] || candidates.unknown) {
97
+ const fullPath = path.join(cwd, candidate);
98
+ if (fs.existsSync(fullPath)) {
99
+ return fullPath;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ function injectWidget(entryFile, projectSlug) {
105
+ const content = fs.readFileSync(entryFile, "utf-8");
106
+ const ext = path.extname(entryFile);
107
+ if (content.includes("@tacktext/widget")) {
108
+ console.log("\u26A0\uFE0F Tack widget already present in", path.basename(entryFile));
109
+ return;
110
+ }
111
+ let newContent;
112
+ if (ext === ".html") {
113
+ const scriptTag = ` <script src="https://unpkg.com/@tacktext/widget"></script>
114
+ <script>Tack.init({ projectId: '${projectSlug}' })</script>
115
+ </body>`;
116
+ newContent = content.replace("</body>", scriptTag);
117
+ } else {
118
+ const importStatement = `import { Tack } from '@tacktext/widget'
119
+ `;
120
+ const initStatement = `
121
+ // Initialize Tack feedback widget
122
+ if (typeof window !== 'undefined') {
123
+ Tack.init({ projectId: '${projectSlug}' })
124
+ }
125
+ `;
126
+ if (content.includes("'use client'")) {
127
+ newContent = content.replace("'use client'", `'use client'
128
+ ${importStatement}`);
129
+ } else if (content.includes('"use client"')) {
130
+ newContent = content.replace('"use client"', `"use client"
131
+ ${importStatement}`);
132
+ } else {
133
+ newContent = importStatement + content;
134
+ }
135
+ const lines = newContent.split("\n");
136
+ let insertIndex = 0;
137
+ for (let i = 0; i < lines.length; i++) {
138
+ const line = lines[i].trim();
139
+ if (line.startsWith("import ") || line.startsWith("//") || line === "" || line.startsWith("'use") || line.startsWith('"use')) {
140
+ insertIndex = i + 1;
141
+ } else {
142
+ break;
143
+ }
144
+ }
145
+ lines.splice(insertIndex, 0, initStatement);
146
+ newContent = lines.join("\n");
147
+ }
148
+ fs.writeFileSync(entryFile, newContent);
149
+ console.log("\u2705 Added Tack widget to", path.basename(entryFile));
150
+ }
151
+ async function main() {
152
+ const args = process.argv.slice(2);
153
+ const command = args[0];
154
+ if (command === "init") {
155
+ let projectName = args.slice(1).join(" ");
156
+ console.log("\n\u{1F4CC} Tack - Feedback for Prototypes\n");
157
+ if (!projectName) {
158
+ const pkgPath = path.join(process.cwd(), "package.json");
159
+ if (fs.existsSync(pkgPath)) {
160
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
161
+ projectName = pkg.name || "";
162
+ }
163
+ if (!projectName || projectName === "my-app") {
164
+ projectName = await prompt("Project name: ");
165
+ } else {
166
+ const useDefault = await prompt(`Project name (${projectName}): `);
167
+ if (useDefault) projectName = useDefault;
168
+ }
169
+ }
170
+ if (!projectName) {
171
+ console.error("\u274C Project name is required");
172
+ process.exit(1);
173
+ }
174
+ const slug = generateSlug(projectName);
175
+ console.log(`
176
+ \u{1F4E6} Project ID: ${slug}`);
177
+ const framework = detectFramework();
178
+ console.log(`\u{1F50D} Detected: ${framework}`);
179
+ const entryFile = findEntryFile(framework);
180
+ if (!entryFile) {
181
+ console.log("\n\u26A0\uFE0F Could not detect entry file. Add this to your app manually:\n");
182
+ console.log(` import { Tack } from '@tacktext/widget'`);
183
+ console.log(` Tack.init({ projectId: '${slug}' })
184
+ `);
185
+ return;
186
+ }
187
+ injectWidget(entryFile, slug);
188
+ console.log("\n\u{1F389} Tack is ready!\n");
189
+ console.log("Next steps:");
190
+ console.log(" 1. Deploy your app");
191
+ console.log(" 2. Share the link - anyone can add feedback");
192
+ console.log(" 3. Click the \u{1F4AC} button to view/add comments\n");
193
+ console.log(`To claim this project later: https://tacktext.vercel.app/claim/${slug}
194
+ `);
195
+ } else {
196
+ console.log(`
197
+ \u{1F4CC} Tack CLI - Feedback for Prototypes
198
+
199
+ Usage:
200
+ npx @tacktext/widget init [project-name] Add Tack to your project
201
+
202
+ Examples:
203
+ npx @tacktext/widget init "My Prototype"
204
+ npx @tacktext/widget init
205
+ `);
206
+ }
207
+ }
208
+ main().catch(console.error);
@@ -0,0 +1,67 @@
1
+ interface TackConfig {
2
+ projectId: string;
3
+ apiUrl?: string;
4
+ supabaseUrl?: string;
5
+ supabaseKey?: string;
6
+ }
7
+
8
+ declare class TackWidget {
9
+ private config;
10
+ private api;
11
+ private auth;
12
+ private container;
13
+ private shadowRoot;
14
+ private selector;
15
+ private form;
16
+ private panel;
17
+ private pins;
18
+ private card;
19
+ private realtime;
20
+ private presence;
21
+ private commentMode;
22
+ private comments;
23
+ private currentVersion;
24
+ private currentPagePath;
25
+ private originalPushState;
26
+ private originalReplaceState;
27
+ private pollingInterval;
28
+ private boundOnUrlChange;
29
+ constructor(config: TackConfig);
30
+ mount(): Promise<void>;
31
+ private joinPresence;
32
+ signIn(): Promise<void>;
33
+ private getPagePath;
34
+ private setupUrlChangeDetection;
35
+ private onUrlChange;
36
+ private createToggleButton;
37
+ private toggleCommentMode;
38
+ private enableCommentMode;
39
+ private disableCommentMode;
40
+ private onPanelClose;
41
+ private onShare;
42
+ private injectPageStyles;
43
+ private setPanelOpen;
44
+ private updateToggleAria;
45
+ private loadComments;
46
+ private onElementSelected;
47
+ private onCommentSubmit;
48
+ private onCommentClick;
49
+ private onPinClick;
50
+ private onCardClose;
51
+ private onResolve;
52
+ private onReply;
53
+ private onEdit;
54
+ private onDelete;
55
+ private scrollToComment;
56
+ private handleDeepLink;
57
+ private subscribeToRealtime;
58
+ private handleRealtimeEvent;
59
+ private showToast;
60
+ destroy(): void;
61
+ }
62
+
63
+ declare const Tack: {
64
+ init(config: TackConfig): TackWidget;
65
+ };
66
+
67
+ export { Tack, type TackConfig, TackWidget };
@@ -0,0 +1,67 @@
1
+ interface TackConfig {
2
+ projectId: string;
3
+ apiUrl?: string;
4
+ supabaseUrl?: string;
5
+ supabaseKey?: string;
6
+ }
7
+
8
+ declare class TackWidget {
9
+ private config;
10
+ private api;
11
+ private auth;
12
+ private container;
13
+ private shadowRoot;
14
+ private selector;
15
+ private form;
16
+ private panel;
17
+ private pins;
18
+ private card;
19
+ private realtime;
20
+ private presence;
21
+ private commentMode;
22
+ private comments;
23
+ private currentVersion;
24
+ private currentPagePath;
25
+ private originalPushState;
26
+ private originalReplaceState;
27
+ private pollingInterval;
28
+ private boundOnUrlChange;
29
+ constructor(config: TackConfig);
30
+ mount(): Promise<void>;
31
+ private joinPresence;
32
+ signIn(): Promise<void>;
33
+ private getPagePath;
34
+ private setupUrlChangeDetection;
35
+ private onUrlChange;
36
+ private createToggleButton;
37
+ private toggleCommentMode;
38
+ private enableCommentMode;
39
+ private disableCommentMode;
40
+ private onPanelClose;
41
+ private onShare;
42
+ private injectPageStyles;
43
+ private setPanelOpen;
44
+ private updateToggleAria;
45
+ private loadComments;
46
+ private onElementSelected;
47
+ private onCommentSubmit;
48
+ private onCommentClick;
49
+ private onPinClick;
50
+ private onCardClose;
51
+ private onResolve;
52
+ private onReply;
53
+ private onEdit;
54
+ private onDelete;
55
+ private scrollToComment;
56
+ private handleDeepLink;
57
+ private subscribeToRealtime;
58
+ private handleRealtimeEvent;
59
+ private showToast;
60
+ destroy(): void;
61
+ }
62
+
63
+ declare const Tack: {
64
+ init(config: TackConfig): TackWidget;
65
+ };
66
+
67
+ export { Tack, type TackConfig, TackWidget };