@nuasite/notes 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 +211 -0
- package/dist/overlay.js +1367 -0
- package/package.json +51 -0
- package/src/apply/apply-suggestion.ts +157 -0
- package/src/dev/api-handlers.ts +215 -0
- package/src/dev/middleware.ts +65 -0
- package/src/dev/request-utils.ts +71 -0
- package/src/index.ts +2 -0
- package/src/integration.ts +168 -0
- package/src/overlay/App.tsx +434 -0
- package/src/overlay/components/CommentPopover.tsx +96 -0
- package/src/overlay/components/DiffPreview.tsx +29 -0
- package/src/overlay/components/ElementHighlight.tsx +33 -0
- package/src/overlay/components/SelectionTooltip.tsx +48 -0
- package/src/overlay/components/Sidebar.tsx +70 -0
- package/src/overlay/components/SidebarItem.tsx +104 -0
- package/src/overlay/components/StaleWarning.tsx +19 -0
- package/src/overlay/components/SuggestPopover.tsx +139 -0
- package/src/overlay/components/Toolbar.tsx +38 -0
- package/src/overlay/env.d.ts +4 -0
- package/src/overlay/index.tsx +71 -0
- package/src/overlay/lib/cms-bridge.ts +33 -0
- package/src/overlay/lib/dom-walker.ts +61 -0
- package/src/overlay/lib/manifest-fetch.ts +35 -0
- package/src/overlay/lib/notes-fetch.ts +121 -0
- package/src/overlay/lib/range-anchor.ts +87 -0
- package/src/overlay/lib/url-mode.ts +43 -0
- package/src/overlay/styles.css +526 -0
- package/src/overlay/types.ts +66 -0
- package/src/storage/id-gen.ts +32 -0
- package/src/storage/json-store.ts +196 -0
- package/src/storage/slug.ts +35 -0
- package/src/storage/types.ts +100 -0
- package/src/tsconfig.json +6 -0
- package/src/types.ts +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# @nuasite/notes
|
|
2
|
+
|
|
3
|
+
Astro integration that adds a Pastel-style comment overlay and a Google Docs-style suggestion overlay alongside `@nuasite/cms`. Designed for content review with non-technical clients on a NuaSite project.
|
|
4
|
+
|
|
5
|
+
A reviewer opens any page with `?nua-notes` appended to the URL, sees a sidebar of existing notes, can click any element to leave a comment, can select any text to suggest a replacement (with a strikethrough/insertion diff), and can apply accepted suggestions back to the source files in one click. The CMS editor chrome is hidden in review mode and reappears the moment the flag goes away.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
**v0.1 — first usable release.** All five build phases of the original plan are shipped except Phase 5 polish (proxy/sandbox forwarding through the CMS Cloudflare Worker, replies, the agency inbox view, theming, and i18n). The local-first dev flow is feature-complete and verified end-to-end with puppeteer.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// astro.config.mjs
|
|
15
|
+
import nuaNotes from '@nuasite/notes'
|
|
16
|
+
import { defineConfig } from '@nuasite/nua/config'
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
integrations: [nuaNotes()],
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun add -D @nuasite/notes
|
|
25
|
+
bun run dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then:
|
|
29
|
+
|
|
30
|
+
- `http://localhost:4321/` — normal CMS editor view, notes is invisible
|
|
31
|
+
- `http://localhost:4321/?nua-notes` — review mode: sidebar visible, CMS chrome hidden, click "Pick element" to comment or select text to suggest
|
|
32
|
+
|
|
33
|
+
The `?nua-notes` flag sets a session cookie so subsequent navigation stays in review mode. Click "Exit" in the toolbar to drop back into CMS editing.
|
|
34
|
+
|
|
35
|
+
## What ships in v0.1
|
|
36
|
+
|
|
37
|
+
| Feature | Status |
|
|
38
|
+
| ------------------------------------------------------------------------------------------ | ------- |
|
|
39
|
+
| Dev API at `/_nua/notes/*` (list, create, update, resolve, reopen, delete, apply) | ✓ |
|
|
40
|
+
| Local JSON storage at `data/notes/pages/<slug>.json` (atomic writes, per-slug mutex) | ✓ |
|
|
41
|
+
| Preact overlay mounted in a shadow DOM (zero CSS leakage either direction) | ✓ |
|
|
42
|
+
| `?nua-notes` URL flag + cookie persistence + Exit toggle | ✓ |
|
|
43
|
+
| Hide `@nuasite/cms` editor chrome in review mode (mode exclusivity) | ✓ |
|
|
44
|
+
| **Pick mode** — hover any `data-cms-id` element, click to comment | ✓ |
|
|
45
|
+
| **Selection mode** — select text inside any element, leave a comment OR a range suggestion | ✓ |
|
|
46
|
+
| Diff preview (− original / + suggested) on suggestion items in the sidebar | ✓ |
|
|
47
|
+
| Anchor re-attachment after page reload — falls back to whitespace-collapsed match | ✓ |
|
|
48
|
+
| Stale badge when an anchor can't be found (source drifted) | ✓ |
|
|
49
|
+
| **Apply flow** — write the suggestion's replacement back to the source file | ✓ |
|
|
50
|
+
| Resolve / reopen / delete actions on every item | ✓ |
|
|
51
|
+
| Item author persisted in `localStorage` | ✓ |
|
|
52
|
+
| Pre-built bundle (~16 kB gzipped) for npm consumers; source mode for monorepo dev | ✓ |
|
|
53
|
+
| Sandbox / proxy mode (Cloudflare Worker forwarding) | Phase 5 |
|
|
54
|
+
| Replies / threaded comments | Phase 5 |
|
|
55
|
+
| Agency inbox view (cross-page list) | Phase 5 |
|
|
56
|
+
| Theming via CSS variables, i18n, screenshot attachments | Phase 5 |
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
`@nuasite/notes` is a sibling Astro integration to `@nuasite/cms`. It does not import a single function from CMS — it only consumes CMS's public surface:
|
|
61
|
+
|
|
62
|
+
1. **`data-cms-id` attributes** that CMS already injects on every editable element. Notes uses them as anchors for comments and suggestions.
|
|
63
|
+
2. **The per-page manifest endpoint** `/<page>.json` that CMS already serves in dev. Notes reads it at create time to capture each anchor's `sourcePath`, `sourceLine`, and `sourceSnippet`.
|
|
64
|
+
3. **Vite's HMR full-reload signal** — when notes writes a source file via Apply, CMS's own watcher picks up the change and reloads the page through the standard HMR path.
|
|
65
|
+
|
|
66
|
+
That's the entire integration surface. Notes does **not** patch, fork, or peer-import CMS internals.
|
|
67
|
+
|
|
68
|
+
### When the reviewer visits a page with `?nua-notes`
|
|
69
|
+
|
|
70
|
+
1. The notes loader script (injected on every dev page) reads the URL, sees the flag, and sets the `nua-notes-mode=1` session cookie so subsequent navigation stays in review mode.
|
|
71
|
+
2. A single `<style>` element is injected into the host document hiding `#cms-app-host` and `[data-nuasite-cms]`. CMS's DOM is still there, just `display: none`.
|
|
72
|
+
3. The Preact overlay mounts inside a shadow DOM attached to `<body>`. Its CSS lives entirely inside the shadow root and never touches the host page.
|
|
73
|
+
4. The overlay fetches `/_nua/notes/list?page=<slug>` for existing notes and `/<slug>.json` for the CMS manifest, then renders the toolbar, sidebar, and any range highlights.
|
|
74
|
+
|
|
75
|
+
### When the reviewer visits a page without the flag
|
|
76
|
+
|
|
77
|
+
The loader script runs, sees no flag and no cookie, and returns immediately. The shadow DOM is never created. CMS works exactly as it did before notes was installed.
|
|
78
|
+
|
|
79
|
+
### Production builds
|
|
80
|
+
|
|
81
|
+
Notes is dev-only, gated on `if (command !== 'dev') return` in every hook. Production builds are unaffected: zero JS, zero CSS, zero middleware. Reviewers use the same dev URL the editor uses, just with a query flag.
|
|
82
|
+
|
|
83
|
+
## Suggestion data model
|
|
84
|
+
|
|
85
|
+
One JSON file per page at `<notesDir>/pages/<slug>.json`:
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
{
|
|
89
|
+
"page": "/inspekce-nemovitosti",
|
|
90
|
+
"lastUpdated": "2026-04-08T12:00:00Z",
|
|
91
|
+
"items": [
|
|
92
|
+
{
|
|
93
|
+
"id": "n-2026-04-08-a3f2b1",
|
|
94
|
+
"type": "comment",
|
|
95
|
+
"targetCmsId": "cms-42",
|
|
96
|
+
"targetSourcePath": "src/content/pages/inspekce-nemovitosti.md",
|
|
97
|
+
"targetSourceLine": 14,
|
|
98
|
+
"targetSnippet": "Kupujete starší byt nebo dům?",
|
|
99
|
+
"range": null,
|
|
100
|
+
"body": "Should be 'Kupujete byt nebo dům', not 'starší'.",
|
|
101
|
+
"author": "Tomáš",
|
|
102
|
+
"createdAt": "2026-04-08T10:23:14Z",
|
|
103
|
+
"status": "open",
|
|
104
|
+
"replies": []
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "n-2026-04-08-7b8c9d",
|
|
108
|
+
"type": "suggestion",
|
|
109
|
+
"targetCmsId": "cms-43",
|
|
110
|
+
"targetSourcePath": "src/content/pages/inspekce-nemovitosti.md",
|
|
111
|
+
"targetSourceLine": 18,
|
|
112
|
+
"targetSnippet": "Detailně prověříme technický stav nemovitosti",
|
|
113
|
+
"range": {
|
|
114
|
+
"anchorText": "Detailně prověříme",
|
|
115
|
+
"originalText": "Detailně prověříme",
|
|
116
|
+
"suggestedText": "Profesionálně prověříme",
|
|
117
|
+
"rationale": "Stronger framing"
|
|
118
|
+
},
|
|
119
|
+
"body": "",
|
|
120
|
+
"author": "Eliška",
|
|
121
|
+
"createdAt": "2026-04-08T11:05:00Z",
|
|
122
|
+
"status": "open",
|
|
123
|
+
"replies": []
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Comments require a non-empty `body`. Suggestions may have an empty body — the diff itself is the message.
|
|
130
|
+
|
|
131
|
+
### Range survival across edits
|
|
132
|
+
|
|
133
|
+
Suggestions don't store character offsets. They store the original substring as `range.anchorText`. On reload, the overlay walks the text nodes inside the target element and looks for the anchor:
|
|
134
|
+
|
|
135
|
+
1. Exact substring match — preferred.
|
|
136
|
+
2. Whitespace-collapsed fallback — handles HTML re-flowing.
|
|
137
|
+
3. If neither finds the anchor, the suggestion is marked stale and surfaces in the sidebar with a warning badge.
|
|
138
|
+
|
|
139
|
+
## Apply flow
|
|
140
|
+
|
|
141
|
+
When the agency clicks **Apply** on an open, non-stale suggestion:
|
|
142
|
+
|
|
143
|
+
1. The overlay POSTs `/_nua/notes/apply` with the page + item id.
|
|
144
|
+
2. The dev API loads the suggestion, resolves `targetSourcePath` against the project root (with a path-traversal guard), reads the file, and finds `range.originalText`.
|
|
145
|
+
- Exactly one occurrence → replace it.
|
|
146
|
+
- Multiple occurrences → pick the one nearest `targetSourceLine` within an 8-line window.
|
|
147
|
+
- Zero occurrences → return 409, mark the suggestion `stale`, leave the file untouched.
|
|
148
|
+
3. On success the file is rewritten atomically (`.tmp` + `rename`) and the suggestion's `status` flips to `applied`.
|
|
149
|
+
4. Vite's file watcher picks up the source change → HMR reload → the page shows the new text.
|
|
150
|
+
|
|
151
|
+
The apply module lives in `src/apply/apply-suggestion.ts` and is fully self-contained — it only uses `node:fs`. It does not peer-import `@nuasite/cms`.
|
|
152
|
+
|
|
153
|
+
## Coexistence with @nuasite/cms
|
|
154
|
+
|
|
155
|
+
Both packages inject scripts into the same `astro:scripts/page.js` bundle. Mode exclusivity solves the clash:
|
|
156
|
+
|
|
157
|
+
- Without the URL flag: notes' loader returns early, mounts nothing, doesn't touch the DOM. CMS behaves byte-for-byte the same as before notes was installed.
|
|
158
|
+
- With the URL flag: notes injects a stylesheet hiding CMS chrome and mounts its own UI inside a shadow DOM. Click handlers, focus, and z-index never collide because only one of the two UIs is visible at a time.
|
|
159
|
+
|
|
160
|
+
A future version may negotiate via PostMessage so the two can coexist on screen (e.g. notes visible while CMS edit is active). For v0.1 the toggle is good enough.
|
|
161
|
+
|
|
162
|
+
## Options
|
|
163
|
+
|
|
164
|
+
| Option | Type | Default | Description |
|
|
165
|
+
| --------------------- | --------- | -------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
166
|
+
| `enabled` | `boolean` | `true` | Master switch. Set `false` to skip injection entirely. Ignored in production builds. |
|
|
167
|
+
| `notesDir` | `string` | `'data/notes'` | Project-relative directory where note JSON files live. |
|
|
168
|
+
| `urlFlag` | `string` | `'nua-notes'` | URL query parameter that activates review mode. |
|
|
169
|
+
| `hideCmsInReviewMode` | `boolean` | `true` | Hide CMS editor chrome when notes mode is active. (Reserved for v0.2; v0.1 always hides.) |
|
|
170
|
+
| `proxy` | `string?` | none | Forward `/_nua/notes/*` to this target. Mirrors the `proxy` option on `@nuasite/cms`. (Reserved for Phase 5.) |
|
|
171
|
+
|
|
172
|
+
## Hosting
|
|
173
|
+
|
|
174
|
+
No special hosting required. Notes runs entirely inside the same Astro dev server CMS already runs in. The reviewer uses the same URL the editor uses, just with `?nua-notes` appended. JSON files land in the project repo — commit them or `.gitignore` them, your call.
|
|
175
|
+
|
|
176
|
+
When the project's CMS is configured to forward writes through the existing nuasite Cloudflare Worker (sandbox mode), Phase 5 will add matching forwarding for `/_nua/notes/*`. v0.1 only supports the local-dev path.
|
|
177
|
+
|
|
178
|
+
## API reference (dev)
|
|
179
|
+
|
|
180
|
+
All endpoints are mounted under `/_nua/notes/`. Requests and responses are JSON.
|
|
181
|
+
|
|
182
|
+
| Method | Path | Body | Response |
|
|
183
|
+
| ------ | ------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
184
|
+
| `GET` | `/list?page=<page>` | — | `{ page, lastUpdated, items }` |
|
|
185
|
+
| `GET` | `/inbox` | — | `{ pages: [...] }` (all pages) |
|
|
186
|
+
| `POST` | `/create` | `{ page, type, targetCmsId, body, author, range?, ... }` | `{ item }` |
|
|
187
|
+
| `POST` | `/update` | `{ page, id, patch }` | `{ item }` |
|
|
188
|
+
| `POST` | `/resolve` | `{ page, id }` | `{ item }` (status → resolved) |
|
|
189
|
+
| `POST` | `/reopen` | `{ page, id }` | `{ item }` (status → open) |
|
|
190
|
+
| `POST` | `/delete` | `{ page, id }` | `{ ok: true }` |
|
|
191
|
+
| `POST` | `/apply` | `{ page, id }` | `{ item, file, before, after }` (200) or `{ item, error, reason }` (409 stale) |
|
|
192
|
+
|
|
193
|
+
## Architecture
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
NuaSite project (any consumer)
|
|
197
|
+
│
|
|
198
|
+
├─ @nuasite/cms → editor + manifest endpoints + data-cms-id markers
|
|
199
|
+
└─ @nuasite/notes → review overlay (this package)
|
|
200
|
+
│
|
|
201
|
+
├─ overlay client → Preact in a shadow DOM, mounted only on ?nua-notes
|
|
202
|
+
│ ├─ reads /<page>.json from @nuasite/cms (read-only)
|
|
203
|
+
│ ├─ talks to /_nua/notes/* via the dev middleware
|
|
204
|
+
│ └─ on apply, the dev middleware rewrites the source file
|
|
205
|
+
│
|
|
206
|
+
└─ dev middleware → /_nua/notes/* CRUD + apply, JSON storage, HMR full-reload
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
Apache-2.0
|