apostil 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/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/apostil.js +2 -0
- package/dist/adapters/localStorage.d.ts +5 -0
- package/dist/adapters/localStorage.js +23 -0
- package/dist/adapters/localStorage.js.map +1 -0
- package/dist/adapters/nextjs.d.ts +22 -0
- package/dist/adapters/nextjs.js +75 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/adapters/rest.d.ts +9 -0
- package/dist/adapters/rest.js +8 -0
- package/dist/adapters/rest.js.map +1 -0
- package/dist/chunk-ASP7WAEG.js +42 -0
- package/dist/chunk-ASP7WAEG.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +233 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +1281 -0
- package/dist/index.js.map +1 -0
- package/dist/types-oQRt3lYH.d.ts +30 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Batzorig Tsergiinkhuu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Apostil
|
|
2
|
+
|
|
3
|
+
Pin-and-comment feedback overlay for React & Next.js. Let your team leave contextual feedback directly on the UI.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Click to pin** — drop comment pins anywhere on the page
|
|
8
|
+
- **Smart target detection** — auto-anchors to nearest meaningful element
|
|
9
|
+
- **Thread-based** — replies, resolve/unresolve, delete
|
|
10
|
+
- **Keyboard shortcuts** — `C` to toggle comment mode, `Esc` to cancel
|
|
11
|
+
- **Popover-aware** — comments inside modals/popovers re-appear when reopened
|
|
12
|
+
- **All Pages view** — see every comment across your project in one sidebar
|
|
13
|
+
- **Auto z-index** — overlay detects highest z-index and sits above everything
|
|
14
|
+
- **SSR-safe** — works with Next.js App Router
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install apostil # or: pnpm add apostil / yarn add apostil
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Initialize
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx apostil init # or: pnpm exec apostil init
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This will:
|
|
31
|
+
- Create `app/api/apostil/route.ts` — API route for comment storage
|
|
32
|
+
- Create `components/apostil-wrapper.tsx` — pre-configured wrapper component
|
|
33
|
+
- Create `.apostil/` directory and add it to `.gitignore`
|
|
34
|
+
- **Automatically wrap `{children}` in your root layout** with `<ApostilWrapper>`
|
|
35
|
+
|
|
36
|
+
### 3. Done
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Press `C` on any page to start commenting. That's it.
|
|
43
|
+
|
|
44
|
+
## How It Works
|
|
45
|
+
|
|
46
|
+
Comments are stored as JSON files in `.apostil/`:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
.apostil/
|
|
50
|
+
├── home.json
|
|
51
|
+
├── about.json
|
|
52
|
+
└── dashboard--settings.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The wrapper auto-detects the current page from `usePathname()` and loads the corresponding comments. Every page in your app gets commenting automatically.
|
|
56
|
+
|
|
57
|
+
## Uninstall
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx apostil remove
|
|
61
|
+
npm uninstall apostil
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`remove` cleans up everything — deletes the API route, wrapper component, `.apostil/` directory, removes the `<ApostilWrapper>` from your layout, and cleans `.gitignore`.
|
|
65
|
+
|
|
66
|
+
## Keyboard Shortcuts
|
|
67
|
+
|
|
68
|
+
| Key | Action |
|
|
69
|
+
|-----|--------|
|
|
70
|
+
| `C` | Toggle comment mode |
|
|
71
|
+
| `Escape` | Cancel unsaved comment / exit comment mode |
|
|
72
|
+
| `Enter` | Submit comment |
|
|
73
|
+
|
|
74
|
+
## Components
|
|
75
|
+
|
|
76
|
+
### `<ApostilProvider>`
|
|
77
|
+
|
|
78
|
+
Core context provider. Use directly for custom setups:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<ApostilProvider pageId="my-page" storage={customAdapter}>
|
|
82
|
+
{children}
|
|
83
|
+
<CommentOverlay />
|
|
84
|
+
<CommentToggle />
|
|
85
|
+
</ApostilProvider>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `<CommentOverlay>`
|
|
89
|
+
|
|
90
|
+
Captures clicks in comment mode. Auto-detects z-index to sit above popovers and modals.
|
|
91
|
+
|
|
92
|
+
### `<CommentToggle>`
|
|
93
|
+
|
|
94
|
+
Floating button (bottom-right) with unresolved count badge.
|
|
95
|
+
|
|
96
|
+
### `<CommentSidebar>`
|
|
97
|
+
|
|
98
|
+
Right panel with two tabs:
|
|
99
|
+
- **This Page** — comments on the current page
|
|
100
|
+
- **All Pages** — every comment across the project. Click to navigate.
|
|
101
|
+
|
|
102
|
+
## Hooks
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { useApostil, useComments, useCommentMode } from "apostil";
|
|
106
|
+
|
|
107
|
+
const { threads, user, addThread, addReply, resolveThread } = useApostil();
|
|
108
|
+
const { openThreads, resolvedThreads, unresolvedCount } = useComments();
|
|
109
|
+
const { commentMode, toggleCommentMode, sidebarOpen, toggleSidebar } = useCommentMode();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Storage Adapters
|
|
113
|
+
|
|
114
|
+
### Default (file-based)
|
|
115
|
+
|
|
116
|
+
No config needed. `npx apostil init` sets up the API route that reads/writes `.apostil/` JSON files.
|
|
117
|
+
|
|
118
|
+
### localStorage
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { localStorageAdapter } from "apostil/adapters/localStorage";
|
|
122
|
+
<ApostilProvider pageId="my-page" storage={localStorageAdapter}>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Custom REST API
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { createRestAdapter } from "apostil/adapters/rest";
|
|
129
|
+
<ApostilProvider pageId="my-page" storage={createRestAdapter("/api/my-comments")}>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Custom Adapter
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
const myAdapter: ApostilStorage = {
|
|
136
|
+
async load(pageId) { /* return threads */ },
|
|
137
|
+
async save(pageId, threads) { /* persist */ },
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Target Detection
|
|
142
|
+
|
|
143
|
+
Apostil auto-detects meaningful elements when placing comments:
|
|
144
|
+
|
|
145
|
+
1. `data-comment-target="id"` — explicit anchor
|
|
146
|
+
2. Elements with `id` or `aria-label`
|
|
147
|
+
3. Semantic HTML (`section`, `nav`, `aside`)
|
|
148
|
+
4. Visual panels (scrollable, bordered, shadowed)
|
|
149
|
+
|
|
150
|
+
Pins are stored as percentages relative to the target — they follow on scroll/resize.
|
|
151
|
+
|
|
152
|
+
## Debug
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
// Browser console
|
|
156
|
+
__apostil_debug.enable()
|
|
157
|
+
__apostil_debug.disable()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Requirements
|
|
161
|
+
|
|
162
|
+
- React 18+
|
|
163
|
+
- Next.js (App Router)
|
|
164
|
+
- Tailwind CSS
|
|
165
|
+
- lucide-react
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
package/bin/apostil.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/adapters/localStorage.ts
|
|
4
|
+
var STORAGE_PREFIX = "apostil-";
|
|
5
|
+
var localStorageAdapter = {
|
|
6
|
+
async load(pageId) {
|
|
7
|
+
if (typeof window === "undefined") return [];
|
|
8
|
+
try {
|
|
9
|
+
const raw = localStorage.getItem(`${STORAGE_PREFIX}${pageId}`);
|
|
10
|
+
return raw ? JSON.parse(raw) : [];
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
async save(pageId, threads) {
|
|
16
|
+
if (typeof window === "undefined") return;
|
|
17
|
+
localStorage.setItem(`${STORAGE_PREFIX}${pageId}`, JSON.stringify(threads));
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export {
|
|
21
|
+
localStorageAdapter
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=localStorage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/localStorage.ts"],"sourcesContent":["import type { ApostilStorage, ApostilThread } from \"../types\";\n\nconst STORAGE_PREFIX = \"apostil-\";\n\nexport const localStorageAdapter: ApostilStorage = {\n async load(pageId: string): Promise<ApostilThread[]> {\n if (typeof window === \"undefined\") return [];\n try {\n const raw = localStorage.getItem(`${STORAGE_PREFIX}${pageId}`);\n return raw ? JSON.parse(raw) : [];\n } catch {\n return [];\n }\n },\n\n async save(pageId: string, threads: ApostilThread[]): Promise<void> {\n if (typeof window === \"undefined\") return;\n localStorage.setItem(`${STORAGE_PREFIX}${pageId}`, JSON.stringify(threads));\n },\n};\n"],"mappings":";;;AAEA,IAAM,iBAAiB;AAEhB,IAAM,sBAAsC;AAAA,EACjD,MAAM,KAAK,QAA0C;AACnD,QAAI,OAAO,WAAW,YAAa,QAAO,CAAC;AAC3C,QAAI;AACF,YAAM,MAAM,aAAa,QAAQ,GAAG,cAAc,GAAG,MAAM,EAAE;AAC7D,aAAO,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAAA,IAClC,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,QAAgB,SAAyC;AAClE,QAAI,OAAO,WAAW,YAAa;AACnC,iBAAa,QAAQ,GAAG,cAAc,GAAG,MAAM,IAAI,KAAK,UAAU,OAAO,CAAC;AAAA,EAC5E;AACF;","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js API route handler for apostil comment storage.
|
|
3
|
+
* Stores comments as JSON files in the project's .apostil/ directory.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
*
|
|
7
|
+
* // app/api/apostil/route.ts
|
|
8
|
+
* export { GET, POST } from "apostil/adapters/nextjs";
|
|
9
|
+
*
|
|
10
|
+
* Or with custom directory:
|
|
11
|
+
* import { createNextjsHandler } from "apostil/adapters/nextjs";
|
|
12
|
+
* const { GET, POST } = createNextjsHandler(".my-comments");
|
|
13
|
+
* export { GET, POST };
|
|
14
|
+
*/
|
|
15
|
+
declare function createNextjsHandler(directory?: string): {
|
|
16
|
+
GET(request: Request): Promise<Response>;
|
|
17
|
+
POST(request: Request): Promise<Response>;
|
|
18
|
+
};
|
|
19
|
+
declare const GET: (request: Request) => Promise<Response>;
|
|
20
|
+
declare const POST: (request: Request) => Promise<Response>;
|
|
21
|
+
|
|
22
|
+
export { GET, POST, createNextjsHandler };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/adapters/nextjs.ts
|
|
2
|
+
function createNextjsHandler(directory = ".apostil") {
|
|
3
|
+
return {
|
|
4
|
+
async GET(request) {
|
|
5
|
+
const { promises: fs } = await import("fs");
|
|
6
|
+
const path = await import("path");
|
|
7
|
+
const url = new URL(request.url);
|
|
8
|
+
const pageId = url.searchParams.get("pageId");
|
|
9
|
+
const dir = path.join(process.cwd(), directory);
|
|
10
|
+
if (!pageId) {
|
|
11
|
+
try {
|
|
12
|
+
const files = await fs.readdir(dir);
|
|
13
|
+
const pages = [];
|
|
14
|
+
for (const file2 of files) {
|
|
15
|
+
if (!file2.endsWith(".json")) continue;
|
|
16
|
+
const id = file2.replace(".json", "");
|
|
17
|
+
try {
|
|
18
|
+
const data = await fs.readFile(path.join(dir, file2), "utf-8");
|
|
19
|
+
const threads = JSON.parse(data);
|
|
20
|
+
if (Array.isArray(threads) && threads.length > 0) {
|
|
21
|
+
pages.push({ pageId: id, threads });
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
pages.sort((a, b) => {
|
|
27
|
+
const aLatest = Math.max(...a.threads.map((t) => new Date(t.createdAt).getTime()));
|
|
28
|
+
const bLatest = Math.max(...b.threads.map((t) => new Date(t.createdAt).getTime()));
|
|
29
|
+
return bLatest - aLatest;
|
|
30
|
+
});
|
|
31
|
+
return Response.json(pages);
|
|
32
|
+
} catch {
|
|
33
|
+
return Response.json([]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const safeName = pageId.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
37
|
+
const file = path.join(dir, `${safeName}.json`);
|
|
38
|
+
try {
|
|
39
|
+
const data = await fs.readFile(file, "utf-8");
|
|
40
|
+
return Response.json(JSON.parse(data));
|
|
41
|
+
} catch {
|
|
42
|
+
return Response.json([]);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async POST(request) {
|
|
46
|
+
const { promises: fs } = await import("fs");
|
|
47
|
+
const path = await import("path");
|
|
48
|
+
const url = new URL(request.url);
|
|
49
|
+
const pageId = url.searchParams.get("pageId");
|
|
50
|
+
if (!pageId) {
|
|
51
|
+
return Response.json({ error: "Missing pageId" }, { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
const dir = path.join(process.cwd(), directory);
|
|
54
|
+
const safeName = pageId.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
55
|
+
const file = path.join(dir, `${safeName}.json`);
|
|
56
|
+
try {
|
|
57
|
+
await fs.mkdir(dir, { recursive: true });
|
|
58
|
+
const threads = await request.json();
|
|
59
|
+
await fs.writeFile(file, JSON.stringify(threads, null, 2), "utf-8");
|
|
60
|
+
return Response.json({ ok: true });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return Response.json({ error: String(e) }, { status: 500 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
var defaultHandler = createNextjsHandler();
|
|
68
|
+
var GET = defaultHandler.GET;
|
|
69
|
+
var POST = defaultHandler.POST;
|
|
70
|
+
export {
|
|
71
|
+
GET,
|
|
72
|
+
POST,
|
|
73
|
+
createNextjsHandler
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=nextjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/nextjs.ts"],"sourcesContent":["/**\n * Next.js API route handler for apostil comment storage.\n * Stores comments as JSON files in the project's .apostil/ directory.\n *\n * Usage:\n *\n * // app/api/apostil/route.ts\n * export { GET, POST } from \"apostil/adapters/nextjs\";\n *\n * Or with custom directory:\n * import { createNextjsHandler } from \"apostil/adapters/nextjs\";\n * const { GET, POST } = createNextjsHandler(\".my-comments\");\n * export { GET, POST };\n */\n\nexport function createNextjsHandler(directory: string = \".apostil\") {\n return {\n async GET(request: Request) {\n const { promises: fs } = await import(\"fs\");\n const path = await import(\"path\");\n\n const url = new URL(request.url);\n const pageId = url.searchParams.get(\"pageId\");\n const dir = path.join(process.cwd(), directory);\n\n // If no pageId, return ALL comments across all pages\n if (!pageId) {\n try {\n const files = await fs.readdir(dir);\n const pages: { pageId: string; threads: unknown[] }[] = [];\n for (const file of files) {\n if (!file.endsWith(\".json\")) continue;\n const id = file.replace(\".json\", \"\");\n try {\n const data = await fs.readFile(path.join(dir, file), \"utf-8\");\n const threads = JSON.parse(data);\n if (Array.isArray(threads) && threads.length > 0) {\n pages.push({ pageId: id, threads });\n }\n } catch {}\n }\n pages.sort((a, b) => {\n const aLatest = Math.max(...(a.threads as Array<{ createdAt: string }>).map((t) => new Date(t.createdAt).getTime()));\n const bLatest = Math.max(...(b.threads as Array<{ createdAt: string }>).map((t) => new Date(t.createdAt).getTime()));\n return bLatest - aLatest;\n });\n return Response.json(pages);\n } catch {\n return Response.json([]);\n }\n }\n\n // Single page\n const safeName = pageId.replace(/[^a-zA-Z0-9_-]/g, \"\");\n const file = path.join(dir, `${safeName}.json`);\n try {\n const data = await fs.readFile(file, \"utf-8\");\n return Response.json(JSON.parse(data));\n } catch {\n return Response.json([]);\n }\n },\n\n async POST(request: Request) {\n const { promises: fs } = await import(\"fs\");\n const path = await import(\"path\");\n\n const url = new URL(request.url);\n const pageId = url.searchParams.get(\"pageId\");\n if (!pageId) {\n return Response.json({ error: \"Missing pageId\" }, { status: 400 });\n }\n\n const dir = path.join(process.cwd(), directory);\n const safeName = pageId.replace(/[^a-zA-Z0-9_-]/g, \"\");\n const file = path.join(dir, `${safeName}.json`);\n\n try {\n await fs.mkdir(dir, { recursive: true });\n const threads = await request.json();\n await fs.writeFile(file, JSON.stringify(threads, null, 2), \"utf-8\");\n return Response.json({ ok: true });\n } catch (e) {\n return Response.json({ error: String(e) }, { status: 500 });\n }\n },\n };\n}\n\n// Default handler with \".apostil\" directory\nconst defaultHandler = createNextjsHandler();\nexport const GET = defaultHandler.GET;\nexport const POST = defaultHandler.POST;\n"],"mappings":"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","names":["file"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { A as ApostilStorage } from '../types-oQRt3lYH.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* REST API storage adapter.
|
|
5
|
+
* Works with any backend that implements GET/POST for threads.
|
|
6
|
+
*/
|
|
7
|
+
declare function createRestAdapter(baseUrl: string): ApostilStorage;
|
|
8
|
+
|
|
9
|
+
export { createRestAdapter };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/adapters/rest.ts
|
|
4
|
+
function createRestAdapter(baseUrl) {
|
|
5
|
+
return {
|
|
6
|
+
async load(pageId) {
|
|
7
|
+
const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}`;
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(url);
|
|
10
|
+
if (!res.ok) {
|
|
11
|
+
console.warn(`[apostil] load failed: ${res.status} ${res.statusText} \u2014 ${url}`);
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const data = await res.json();
|
|
15
|
+
return data;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.warn(`[apostil] load error:`, e, `\u2014 ${url}`);
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
async save(pageId, threads) {
|
|
22
|
+
const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}`;
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify(threads)
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
console.warn(`[apostil] save failed: ${res.status} ${res.statusText} \u2014 ${url}`);
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.warn(`[apostil] save error:`, e, `\u2014 ${url}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
createRestAdapter
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=chunk-ASP7WAEG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/rest.ts"],"sourcesContent":["import type { ApostilStorage, ApostilThread } from \"../types\";\n\n/**\n * REST API storage adapter.\n * Works with any backend that implements GET/POST for threads.\n */\nexport function createRestAdapter(baseUrl: string): ApostilStorage {\n return {\n async load(pageId: string): Promise<ApostilThread[]> {\n const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}`;\n try {\n const res = await fetch(url);\n if (!res.ok) {\n console.warn(`[apostil] load failed: ${res.status} ${res.statusText} — ${url}`);\n return [];\n }\n const data = await res.json();\n return data;\n } catch (e) {\n console.warn(`[apostil] load error:`, e, `— ${url}`);\n return [];\n }\n },\n\n async save(pageId: string, threads: ApostilThread[]): Promise<void> {\n const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}`;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(threads),\n });\n if (!res.ok) {\n console.warn(`[apostil] save failed: ${res.status} ${res.statusText} — ${url}`);\n }\n } catch (e) {\n console.warn(`[apostil] save error:`, e, `— ${url}`);\n }\n },\n };\n}\n"],"mappings":";;;AAMO,SAAS,kBAAkB,SAAiC;AACjE,SAAO;AAAA,IACL,MAAM,KAAK,QAA0C;AACnD,YAAM,MAAM,GAAG,OAAO,WAAW,mBAAmB,MAAM,CAAC;AAC3D,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,YAAI,CAAC,IAAI,IAAI;AACX,kBAAQ,KAAK,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,WAAM,GAAG,EAAE;AAC9E,iBAAO,CAAC;AAAA,QACV;AACA,cAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,eAAO;AAAA,MACT,SAAS,GAAG;AACV,gBAAQ,KAAK,yBAAyB,GAAG,UAAK,GAAG,EAAE;AACnD,eAAO,CAAC;AAAA,MACV;AAAA,IACF;AAAA,IAEA,MAAM,KAAK,QAAgB,SAAyC;AAClE,YAAM,MAAM,GAAG,OAAO,WAAW,mBAAmB,MAAM,CAAC;AAC3D,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,KAAK;AAAA,UAC3B,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,QAC9B,CAAC;AACD,YAAI,CAAC,IAAI,IAAI;AACX,kBAAQ,KAAK,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,WAAM,GAAG,EAAE;AAAA,QAChF;AAAA,MACF,SAAS,GAAG;AACV,gBAAQ,KAAK,yBAAyB,GAAG,UAAK,GAAG,EAAE;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import fs from "fs/promises";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var args = process.argv.slice(2);
|
|
7
|
+
var command = args[0];
|
|
8
|
+
if (command === "init") {
|
|
9
|
+
init();
|
|
10
|
+
} else if (command === "remove") {
|
|
11
|
+
remove();
|
|
12
|
+
} else if (command === "help" || command === "--help" || command === "-h" || !command) {
|
|
13
|
+
printHelp();
|
|
14
|
+
} else {
|
|
15
|
+
console.log(` Unknown command: ${command}
|
|
16
|
+
`);
|
|
17
|
+
printHelp();
|
|
18
|
+
}
|
|
19
|
+
function printHelp() {
|
|
20
|
+
console.log(`
|
|
21
|
+
apostil \u2014 Pin-and-comment feedback for React & Next.js
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
npx apostil init Set up apostil in your Next.js project
|
|
25
|
+
npx apostil remove Remove apostil from your project
|
|
26
|
+
npx apostil help Show this help
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
async function init() {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const appDir = await findAppDir(cwd);
|
|
32
|
+
if (!appDir) {
|
|
33
|
+
console.log(" Could not find a Next.js app/ directory.");
|
|
34
|
+
console.log(" Make sure you're running this from your project root.\n");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const useSrc = appDir.includes("src/app");
|
|
38
|
+
console.log("\n Setting up apostil...\n");
|
|
39
|
+
const apiDir = path.join(appDir, "api", "apostil");
|
|
40
|
+
const apiFile = path.join(apiDir, "route.ts");
|
|
41
|
+
if (await fileExists(apiFile)) {
|
|
42
|
+
console.log(" \u2713 API route already exists");
|
|
43
|
+
} else {
|
|
44
|
+
await fs.mkdir(apiDir, { recursive: true });
|
|
45
|
+
await fs.writeFile(
|
|
46
|
+
apiFile,
|
|
47
|
+
`export { GET, POST } from "apostil/adapters/nextjs";
|
|
48
|
+
`,
|
|
49
|
+
"utf-8"
|
|
50
|
+
);
|
|
51
|
+
console.log(" \u2713 Created ${rel(cwd, apiFile)}");
|
|
52
|
+
}
|
|
53
|
+
const componentsDir = path.join(cwd, useSrc ? "src/components" : "components");
|
|
54
|
+
const wrapperFile = path.join(componentsDir, "apostil-wrapper.tsx");
|
|
55
|
+
if (await fileExists(wrapperFile)) {
|
|
56
|
+
console.log(" \u2713 Wrapper component already exists");
|
|
57
|
+
} else {
|
|
58
|
+
await fs.mkdir(componentsDir, { recursive: true });
|
|
59
|
+
await fs.writeFile(wrapperFile, getWrapperComponent(), "utf-8");
|
|
60
|
+
console.log(` \u2713 Created ${rel(cwd, wrapperFile)}`);
|
|
61
|
+
}
|
|
62
|
+
const commentsDir = path.join(cwd, ".apostil");
|
|
63
|
+
await fs.mkdir(commentsDir, { recursive: true });
|
|
64
|
+
console.log(" \u2713 Created .apostil/ directory");
|
|
65
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
66
|
+
let gitignore = "";
|
|
67
|
+
try {
|
|
68
|
+
gitignore = await fs.readFile(gitignorePath, "utf-8");
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
if (!gitignore.includes(".apostil")) {
|
|
72
|
+
const entry = "\n# Apostil comments\n.apostil/\n";
|
|
73
|
+
await fs.appendFile(gitignorePath, entry, "utf-8");
|
|
74
|
+
console.log(" \u2713 Added .apostil/ to .gitignore");
|
|
75
|
+
} else {
|
|
76
|
+
console.log(" \u2713 .gitignore already configured");
|
|
77
|
+
}
|
|
78
|
+
const layoutInjected = await injectIntoLayout(appDir, useSrc);
|
|
79
|
+
if (layoutInjected) {
|
|
80
|
+
console.log(` \u2713 Added <ApostilWrapper> to root layout`);
|
|
81
|
+
}
|
|
82
|
+
console.log("\n Done! Run your dev server and press C to start commenting.\n");
|
|
83
|
+
}
|
|
84
|
+
async function remove() {
|
|
85
|
+
const cwd = process.cwd();
|
|
86
|
+
const appDir = await findAppDir(cwd);
|
|
87
|
+
const useSrc = appDir?.includes("src/app") ?? false;
|
|
88
|
+
console.log("\n Removing apostil...\n");
|
|
89
|
+
if (appDir) {
|
|
90
|
+
const apiDir = path.join(appDir, "api", "apostil");
|
|
91
|
+
if (await fileExists(path.join(apiDir, "route.ts"))) {
|
|
92
|
+
await fs.rm(apiDir, { recursive: true });
|
|
93
|
+
console.log(" \u2713 Removed API route");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const componentsDir = path.join(cwd, useSrc ? "src/components" : "components");
|
|
97
|
+
const wrapperFile = path.join(componentsDir, "apostil-wrapper.tsx");
|
|
98
|
+
if (await fileExists(wrapperFile)) {
|
|
99
|
+
await fs.rm(wrapperFile);
|
|
100
|
+
console.log(" \u2713 Removed wrapper component");
|
|
101
|
+
}
|
|
102
|
+
if (appDir) {
|
|
103
|
+
const unwrapped = await removeFromLayout(appDir);
|
|
104
|
+
if (unwrapped) {
|
|
105
|
+
console.log(" \u2713 Removed <ApostilWrapper> from root layout");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const commentsDir = path.join(cwd, ".apostil");
|
|
109
|
+
if (await fileExists(commentsDir)) {
|
|
110
|
+
await fs.rm(commentsDir, { recursive: true });
|
|
111
|
+
console.log(" \u2713 Removed .apostil/ directory");
|
|
112
|
+
}
|
|
113
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
114
|
+
try {
|
|
115
|
+
let gitignore = await fs.readFile(gitignorePath, "utf-8");
|
|
116
|
+
gitignore = gitignore.replace(/\n?# Apostil comments\n\.apostil\/\n?/g, "");
|
|
117
|
+
await fs.writeFile(gitignorePath, gitignore, "utf-8");
|
|
118
|
+
console.log(" \u2713 Cleaned .gitignore");
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
console.log(`
|
|
122
|
+
Done! Now run: npm uninstall apostil
|
|
123
|
+
`);
|
|
124
|
+
}
|
|
125
|
+
function getWrapperComponent() {
|
|
126
|
+
return `"use client";
|
|
127
|
+
|
|
128
|
+
import { usePathname } from "next/navigation";
|
|
129
|
+
import {
|
|
130
|
+
ApostilProvider,
|
|
131
|
+
CommentOverlay,
|
|
132
|
+
CommentToggle,
|
|
133
|
+
CommentSidebar,
|
|
134
|
+
} from "apostil";
|
|
135
|
+
|
|
136
|
+
export function ApostilWrapper({ children }: { children: React.ReactNode }) {
|
|
137
|
+
const pathname = usePathname();
|
|
138
|
+
const pageId = pathname.replace(/\\//g, "--").replace(/^--/, "") || "home";
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<ApostilProvider pageId={pageId}>
|
|
142
|
+
{children}
|
|
143
|
+
<CommentOverlay />
|
|
144
|
+
<CommentSidebar />
|
|
145
|
+
<CommentToggle />
|
|
146
|
+
</ApostilProvider>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
async function injectIntoLayout(appDir, useSrc) {
|
|
152
|
+
const layoutPath = await findLayout(appDir);
|
|
153
|
+
if (!layoutPath) return false;
|
|
154
|
+
let content = await fs.readFile(layoutPath, "utf-8");
|
|
155
|
+
if (content.includes("ApostilWrapper")) {
|
|
156
|
+
console.log(" \u2713 Layout already has <ApostilWrapper>");
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const importPath = useSrc ? "@/components/apostil-wrapper" : "../components/apostil-wrapper";
|
|
160
|
+
const importLine = `import { ApostilWrapper } from "${importPath}";
|
|
161
|
+
`;
|
|
162
|
+
const importRegex = /^import\s.+$/gm;
|
|
163
|
+
let lastImportIndex = 0;
|
|
164
|
+
let match;
|
|
165
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
166
|
+
lastImportIndex = match.index + match[0].length;
|
|
167
|
+
}
|
|
168
|
+
if (lastImportIndex > 0) {
|
|
169
|
+
content = content.slice(0, lastImportIndex) + "\n" + importLine + content.slice(lastImportIndex);
|
|
170
|
+
} else {
|
|
171
|
+
const useDirective = content.match(/^["']use (client|server)["'];?\n/);
|
|
172
|
+
const insertAt = useDirective ? useDirective[0].length : 0;
|
|
173
|
+
content = content.slice(0, insertAt) + importLine + content.slice(insertAt);
|
|
174
|
+
}
|
|
175
|
+
const bodyChildrenRegex = /(<body[^>]*>)([\s\S]*?)(\{[\s]*children[\s]*\})([\s\S]*?)(<\/body>)/;
|
|
176
|
+
const bodyMatch = content.match(bodyChildrenRegex);
|
|
177
|
+
if (bodyMatch) {
|
|
178
|
+
content = content.replace(
|
|
179
|
+
bodyChildrenRegex,
|
|
180
|
+
`$1$2<ApostilWrapper>$3</ApostilWrapper>$4$5`
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
const childrenRegex = /(\{[\s]*children[\s]*\})/;
|
|
184
|
+
if (childrenRegex.test(content)) {
|
|
185
|
+
content = content.replace(childrenRegex, `<ApostilWrapper>$1</ApostilWrapper>`);
|
|
186
|
+
} else {
|
|
187
|
+
console.log(" \u26A0 Could not find {children} in layout \u2014 add <ApostilWrapper> manually");
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
await fs.writeFile(layoutPath, content, "utf-8");
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
async function removeFromLayout(appDir) {
|
|
195
|
+
const layoutPath = await findLayout(appDir);
|
|
196
|
+
if (!layoutPath) return false;
|
|
197
|
+
let content = await fs.readFile(layoutPath, "utf-8");
|
|
198
|
+
if (!content.includes("ApostilWrapper")) return false;
|
|
199
|
+
content = content.replace(/import\s*\{[^}]*ApostilWrapper[^}]*\}\s*from\s*["'][^"']+["'];?\n?/g, "");
|
|
200
|
+
content = content.replace(/<ApostilWrapper>([\s\S]*?)<\/ApostilWrapper>/g, "$1");
|
|
201
|
+
await fs.writeFile(layoutPath, content, "utf-8");
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
async function findAppDir(cwd) {
|
|
205
|
+
for (const candidate of ["src/app", "app"]) {
|
|
206
|
+
const dir = path.join(cwd, candidate);
|
|
207
|
+
try {
|
|
208
|
+
const stat = await fs.stat(dir);
|
|
209
|
+
if (stat.isDirectory()) return dir;
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
async function findLayout(appDir) {
|
|
216
|
+
for (const ext of ["tsx", "jsx", "ts", "js"]) {
|
|
217
|
+
const file = path.join(appDir, `layout.${ext}`);
|
|
218
|
+
if (await fileExists(file)) return file;
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
async function fileExists(p) {
|
|
223
|
+
try {
|
|
224
|
+
await fs.stat(p);
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function rel(cwd, filePath) {
|
|
231
|
+
return path.relative(cwd, filePath);
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=index.js.map
|