@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.
Files changed (35) hide show
  1. package/README.md +211 -0
  2. package/dist/overlay.js +1367 -0
  3. package/package.json +51 -0
  4. package/src/apply/apply-suggestion.ts +157 -0
  5. package/src/dev/api-handlers.ts +215 -0
  6. package/src/dev/middleware.ts +65 -0
  7. package/src/dev/request-utils.ts +71 -0
  8. package/src/index.ts +2 -0
  9. package/src/integration.ts +168 -0
  10. package/src/overlay/App.tsx +434 -0
  11. package/src/overlay/components/CommentPopover.tsx +96 -0
  12. package/src/overlay/components/DiffPreview.tsx +29 -0
  13. package/src/overlay/components/ElementHighlight.tsx +33 -0
  14. package/src/overlay/components/SelectionTooltip.tsx +48 -0
  15. package/src/overlay/components/Sidebar.tsx +70 -0
  16. package/src/overlay/components/SidebarItem.tsx +104 -0
  17. package/src/overlay/components/StaleWarning.tsx +19 -0
  18. package/src/overlay/components/SuggestPopover.tsx +139 -0
  19. package/src/overlay/components/Toolbar.tsx +38 -0
  20. package/src/overlay/env.d.ts +4 -0
  21. package/src/overlay/index.tsx +71 -0
  22. package/src/overlay/lib/cms-bridge.ts +33 -0
  23. package/src/overlay/lib/dom-walker.ts +61 -0
  24. package/src/overlay/lib/manifest-fetch.ts +35 -0
  25. package/src/overlay/lib/notes-fetch.ts +121 -0
  26. package/src/overlay/lib/range-anchor.ts +87 -0
  27. package/src/overlay/lib/url-mode.ts +43 -0
  28. package/src/overlay/styles.css +526 -0
  29. package/src/overlay/types.ts +66 -0
  30. package/src/storage/id-gen.ts +32 -0
  31. package/src/storage/json-store.ts +196 -0
  32. package/src/storage/slug.ts +35 -0
  33. package/src/storage/types.ts +100 -0
  34. package/src/tsconfig.json +6 -0
  35. package/src/types.ts +50 -0
@@ -0,0 +1,526 @@
1
+ /**
2
+ * @nuasite/notes overlay styles.
3
+ *
4
+ * Imported via Vite's `?inline` query and injected into a shadow root, so
5
+ * these styles never leak into the host page (and the host page can't
6
+ * accidentally style notes UI). Variables on `:host` are the only knobs.
7
+ */
8
+
9
+ :host {
10
+ --notes-bg: #ffffff;
11
+ --notes-fg: #0f172a;
12
+ --notes-muted: #64748b;
13
+ --notes-border: #e2e8f0;
14
+ --notes-accent: #f59e0b;
15
+ --notes-accent-fg: #1f2937;
16
+ --notes-danger: #dc2626;
17
+ --notes-success: #16a34a;
18
+ --notes-shadow: 0 10px 30px rgba(15, 23, 42, 0.18);
19
+ --notes-radius: 10px;
20
+ --notes-sidebar-w: 360px;
21
+ --notes-z: 2147483600;
22
+
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
24
+ font-size: 14px;
25
+ line-height: 1.45;
26
+ color: var(--notes-fg);
27
+ -webkit-font-smoothing: antialiased;
28
+ }
29
+
30
+ * {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ button {
35
+ font: inherit;
36
+ cursor: pointer;
37
+ border: none;
38
+ background: none;
39
+ color: inherit;
40
+ padding: 0;
41
+ }
42
+
43
+ input,
44
+ textarea {
45
+ font: inherit;
46
+ color: inherit;
47
+ }
48
+
49
+ .notes-root {
50
+ position: fixed;
51
+ inset: 0;
52
+ pointer-events: none;
53
+ z-index: var(--notes-z);
54
+ }
55
+
56
+ /* Toolbar (top, full width) */
57
+ .notes-toolbar {
58
+ position: fixed;
59
+ top: 0;
60
+ left: 0;
61
+ right: 0;
62
+ height: 44px;
63
+ background: var(--notes-bg);
64
+ border-bottom: 1px solid var(--notes-border);
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: space-between;
68
+ padding: 0 16px;
69
+ box-shadow: var(--notes-shadow);
70
+ pointer-events: auto;
71
+ }
72
+
73
+ .notes-toolbar__brand {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 8px;
77
+ font-weight: 600;
78
+ }
79
+
80
+ .notes-toolbar__dot {
81
+ width: 8px;
82
+ height: 8px;
83
+ border-radius: 50%;
84
+ background: var(--notes-accent);
85
+ box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
86
+ }
87
+
88
+ .notes-toolbar__page {
89
+ color: var(--notes-muted);
90
+ font-weight: 400;
91
+ margin-left: 12px;
92
+ font-size: 13px;
93
+ }
94
+
95
+ .notes-toolbar__actions {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 8px;
99
+ }
100
+
101
+ .notes-btn {
102
+ padding: 6px 12px;
103
+ border-radius: 6px;
104
+ background: #f1f5f9;
105
+ color: var(--notes-fg);
106
+ font-size: 13px;
107
+ font-weight: 500;
108
+ transition: background 0.15s;
109
+ }
110
+
111
+ .notes-btn:hover {
112
+ background: #e2e8f0;
113
+ }
114
+
115
+ .notes-btn--primary {
116
+ background: var(--notes-accent);
117
+ color: var(--notes-accent-fg);
118
+ }
119
+
120
+ .notes-btn--primary:hover {
121
+ background: #d97706;
122
+ color: #ffffff;
123
+ }
124
+
125
+ .notes-btn--ghost {
126
+ background: transparent;
127
+ color: var(--notes-muted);
128
+ }
129
+
130
+ .notes-btn--ghost:hover {
131
+ color: var(--notes-fg);
132
+ background: #f1f5f9;
133
+ }
134
+
135
+ .notes-btn--danger {
136
+ color: var(--notes-danger);
137
+ }
138
+
139
+ /* Sidebar (right side) */
140
+ .notes-sidebar {
141
+ position: fixed;
142
+ top: 44px;
143
+ right: 0;
144
+ bottom: 0;
145
+ width: var(--notes-sidebar-w);
146
+ background: var(--notes-bg);
147
+ border-left: 1px solid var(--notes-border);
148
+ box-shadow: var(--notes-shadow);
149
+ display: flex;
150
+ flex-direction: column;
151
+ pointer-events: auto;
152
+ }
153
+
154
+ .notes-sidebar__header {
155
+ padding: 16px 16px 12px;
156
+ border-bottom: 1px solid var(--notes-border);
157
+ }
158
+
159
+ .notes-sidebar__title {
160
+ font-size: 16px;
161
+ font-weight: 600;
162
+ margin: 0 0 4px;
163
+ }
164
+
165
+ .notes-sidebar__meta {
166
+ font-size: 12px;
167
+ color: var(--notes-muted);
168
+ }
169
+
170
+ .notes-sidebar__list {
171
+ flex: 1;
172
+ overflow-y: auto;
173
+ padding: 12px;
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 10px;
177
+ }
178
+
179
+ .notes-sidebar__empty {
180
+ padding: 24px 16px;
181
+ text-align: center;
182
+ color: var(--notes-muted);
183
+ font-size: 13px;
184
+ line-height: 1.5;
185
+ }
186
+
187
+ .notes-sidebar__hint {
188
+ font-size: 12px;
189
+ color: var(--notes-muted);
190
+ margin-top: 6px;
191
+ }
192
+
193
+ /* Item card */
194
+ .notes-item {
195
+ border: 1px solid var(--notes-border);
196
+ border-radius: var(--notes-radius);
197
+ padding: 12px;
198
+ background: #ffffff;
199
+ transition: border-color 0.15s, box-shadow 0.15s;
200
+ }
201
+
202
+ .notes-item:hover {
203
+ border-color: #cbd5e1;
204
+ }
205
+
206
+ .notes-item--active {
207
+ border-color: var(--notes-accent);
208
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.12);
209
+ }
210
+
211
+ .notes-item--resolved {
212
+ opacity: 0.55;
213
+ }
214
+
215
+ .notes-item__head {
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: space-between;
219
+ margin-bottom: 6px;
220
+ }
221
+
222
+ .notes-item__author {
223
+ font-weight: 600;
224
+ font-size: 13px;
225
+ }
226
+
227
+ .notes-item__time {
228
+ font-size: 11px;
229
+ color: var(--notes-muted);
230
+ }
231
+
232
+ .notes-item__snippet {
233
+ font-size: 12px;
234
+ color: var(--notes-muted);
235
+ background: #f8fafc;
236
+ border-left: 3px solid var(--notes-border);
237
+ padding: 6px 8px;
238
+ margin-bottom: 6px;
239
+ border-radius: 0 4px 4px 0;
240
+ white-space: pre-wrap;
241
+ word-break: break-word;
242
+ }
243
+
244
+ .notes-item__body {
245
+ font-size: 13px;
246
+ white-space: pre-wrap;
247
+ word-break: break-word;
248
+ }
249
+
250
+ .notes-item__actions {
251
+ display: flex;
252
+ gap: 6px;
253
+ margin-top: 10px;
254
+ padding-top: 10px;
255
+ border-top: 1px solid #f1f5f9;
256
+ }
257
+
258
+ .notes-item__badge {
259
+ display: inline-block;
260
+ font-size: 10px;
261
+ font-weight: 600;
262
+ text-transform: uppercase;
263
+ letter-spacing: 0.04em;
264
+ padding: 2px 6px;
265
+ border-radius: 4px;
266
+ margin-right: 6px;
267
+ }
268
+
269
+ .notes-item__badge--comment {
270
+ background: #dbeafe;
271
+ color: #1e40af;
272
+ }
273
+
274
+ .notes-item__badge--suggestion {
275
+ background: #fef3c7;
276
+ color: #92400e;
277
+ }
278
+
279
+ .notes-item__badge--resolved {
280
+ background: #dcfce7;
281
+ color: #166534;
282
+ }
283
+
284
+ /* Element highlight ring */
285
+ .notes-highlight {
286
+ position: fixed;
287
+ pointer-events: none;
288
+ border: 2px solid var(--notes-accent);
289
+ border-radius: 4px;
290
+ box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.18);
291
+ transition: all 0.08s ease-out;
292
+ }
293
+
294
+ .notes-highlight--persistent {
295
+ border-color: rgba(245, 158, 11, 0.6);
296
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.12);
297
+ }
298
+
299
+ /* Comment popover */
300
+ .notes-popover {
301
+ position: fixed;
302
+ width: 320px;
303
+ background: var(--notes-bg);
304
+ border: 1px solid var(--notes-border);
305
+ border-radius: var(--notes-radius);
306
+ box-shadow: var(--notes-shadow);
307
+ padding: 14px;
308
+ pointer-events: auto;
309
+ display: flex;
310
+ flex-direction: column;
311
+ gap: 10px;
312
+ }
313
+
314
+ .notes-popover__title {
315
+ font-size: 13px;
316
+ font-weight: 600;
317
+ margin: 0;
318
+ }
319
+
320
+ .notes-popover__snippet {
321
+ font-size: 12px;
322
+ color: var(--notes-muted);
323
+ background: #f8fafc;
324
+ border-left: 3px solid var(--notes-border);
325
+ padding: 6px 8px;
326
+ border-radius: 0 4px 4px 0;
327
+ max-height: 60px;
328
+ overflow: hidden;
329
+ }
330
+
331
+ .notes-popover textarea {
332
+ width: 100%;
333
+ min-height: 80px;
334
+ resize: vertical;
335
+ border: 1px solid var(--notes-border);
336
+ border-radius: 6px;
337
+ padding: 8px 10px;
338
+ background: #ffffff;
339
+ outline: none;
340
+ }
341
+
342
+ .notes-popover textarea:focus {
343
+ border-color: var(--notes-accent);
344
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15);
345
+ }
346
+
347
+ .notes-popover input[type='text'] {
348
+ border: 1px solid var(--notes-border);
349
+ border-radius: 6px;
350
+ padding: 6px 10px;
351
+ background: #ffffff;
352
+ outline: none;
353
+ }
354
+
355
+ .notes-popover input[type='text']:focus {
356
+ border-color: var(--notes-accent);
357
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15);
358
+ }
359
+
360
+ .notes-popover__row {
361
+ display: flex;
362
+ justify-content: space-between;
363
+ align-items: center;
364
+ gap: 8px;
365
+ }
366
+
367
+ /* Selection tooltip */
368
+ .notes-selection-tooltip {
369
+ position: fixed;
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 6px;
373
+ padding: 4px 6px;
374
+ background: #0f172a;
375
+ color: #ffffff;
376
+ border-radius: 8px;
377
+ box-shadow: var(--notes-shadow);
378
+ pointer-events: auto;
379
+ z-index: calc(var(--notes-z) + 5);
380
+ }
381
+
382
+ .notes-selection-tooltip .notes-btn {
383
+ background: rgba(255, 255, 255, 0.08);
384
+ color: #ffffff;
385
+ font-size: 12px;
386
+ padding: 4px 8px;
387
+ }
388
+
389
+ .notes-selection-tooltip .notes-btn--primary {
390
+ background: var(--notes-accent);
391
+ color: var(--notes-accent-fg);
392
+ }
393
+
394
+ .notes-selection-tooltip .notes-btn--ghost {
395
+ color: #e2e8f0;
396
+ }
397
+
398
+ .notes-selection-tooltip .notes-btn--ghost:hover {
399
+ background: rgba(255, 255, 255, 0.16);
400
+ color: #ffffff;
401
+ }
402
+
403
+ /* Suggest popover variations */
404
+ .notes-popover--suggest {
405
+ width: 360px;
406
+ }
407
+
408
+ .notes-popover__label {
409
+ display: block;
410
+ font-size: 11px;
411
+ font-weight: 600;
412
+ text-transform: uppercase;
413
+ letter-spacing: 0.04em;
414
+ color: var(--notes-muted);
415
+ margin-bottom: 4px;
416
+ }
417
+
418
+ .notes-popover__original {
419
+ background: #fef3c7;
420
+ border: 1px solid #fde68a;
421
+ border-radius: 6px;
422
+ padding: 8px 10px;
423
+ font-size: 13px;
424
+ color: #78350f;
425
+ max-height: 80px;
426
+ overflow-y: auto;
427
+ white-space: pre-wrap;
428
+ word-break: break-word;
429
+ }
430
+
431
+ .notes-strikethrough {
432
+ text-decoration: line-through;
433
+ text-decoration-color: rgba(220, 38, 38, 0.6);
434
+ text-decoration-thickness: 2px;
435
+ }
436
+
437
+ /* Diff preview in sidebar */
438
+ .notes-diff {
439
+ font-size: 13px;
440
+ border-radius: 6px;
441
+ background: #f8fafc;
442
+ border: 1px solid var(--notes-border);
443
+ overflow: hidden;
444
+ margin-bottom: 8px;
445
+ }
446
+
447
+ .notes-diff__row {
448
+ padding: 6px 10px;
449
+ display: flex;
450
+ gap: 6px;
451
+ white-space: pre-wrap;
452
+ word-break: break-word;
453
+ }
454
+
455
+ .notes-diff__row--del {
456
+ background: #fef2f2;
457
+ color: #7f1d1d;
458
+ border-bottom: 1px solid #fee2e2;
459
+ }
460
+
461
+ .notes-diff__row--ins {
462
+ background: #f0fdf4;
463
+ color: #14532d;
464
+ }
465
+
466
+ .notes-diff__marker {
467
+ font-weight: 700;
468
+ font-family: ui-monospace, SFMono-Regular, monospace;
469
+ width: 12px;
470
+ flex-shrink: 0;
471
+ text-align: center;
472
+ }
473
+
474
+ .notes-item__rationale {
475
+ font-size: 12px;
476
+ color: var(--notes-muted);
477
+ margin-bottom: 6px;
478
+ font-style: italic;
479
+ }
480
+
481
+ .notes-item__rationale-label {
482
+ font-style: normal;
483
+ font-weight: 600;
484
+ color: var(--notes-fg);
485
+ margin-right: 4px;
486
+ }
487
+
488
+ /* Stale warning */
489
+ .notes-stale {
490
+ display: flex;
491
+ align-items: center;
492
+ gap: 6px;
493
+ background: #fef3c7;
494
+ border: 1px solid #fde68a;
495
+ color: #92400e;
496
+ font-size: 12px;
497
+ padding: 6px 8px;
498
+ border-radius: 6px;
499
+ margin-bottom: 8px;
500
+ }
501
+
502
+ .notes-stale__icon {
503
+ font-size: 14px;
504
+ }
505
+
506
+ /* Persistent highlight variant for suggestion ranges */
507
+ .notes-highlight--suggestion {
508
+ border-color: rgba(250, 204, 21, 0.9);
509
+ box-shadow: 0 0 0 3px rgba(250, 204, 21, 0.18);
510
+ background: rgba(254, 243, 199, 0.25);
511
+ }
512
+
513
+ /* Banner shown when notes API fails to load */
514
+ .notes-banner {
515
+ position: fixed;
516
+ top: 56px;
517
+ left: 16px;
518
+ right: calc(var(--notes-sidebar-w) + 16px);
519
+ padding: 10px 14px;
520
+ background: #fef2f2;
521
+ border: 1px solid #fecaca;
522
+ color: #991b1b;
523
+ border-radius: var(--notes-radius);
524
+ font-size: 13px;
525
+ pointer-events: auto;
526
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Client-side types used by the Preact overlay.
3
+ *
4
+ * The overlay only knows what comes back from the dev API, so we duplicate
5
+ * the storage types here in their JSON-friendly form (no Date objects, no
6
+ * file handles). Keeping a separate copy lets the overlay bundle ship
7
+ * without pulling in node-only modules through `src/storage`.
8
+ */
9
+
10
+ export type NoteType = 'comment' | 'suggestion'
11
+ export type NoteStatus = 'open' | 'resolved' | 'applied' | 'rejected' | 'stale'
12
+
13
+ export interface NoteRange {
14
+ anchorText: string
15
+ originalText: string
16
+ suggestedText: string
17
+ rationale?: string
18
+ }
19
+
20
+ export interface NoteReply {
21
+ id: string
22
+ author: string
23
+ body: string
24
+ createdAt: string
25
+ }
26
+
27
+ export interface NoteItem {
28
+ id: string
29
+ type: NoteType
30
+ targetCmsId: string
31
+ targetSourcePath?: string
32
+ targetSourceLine?: number
33
+ targetSnippet?: string
34
+ range: NoteRange | null
35
+ body: string
36
+ author: string
37
+ createdAt: string
38
+ updatedAt?: string
39
+ status: NoteStatus
40
+ replies: NoteReply[]
41
+ }
42
+
43
+ export interface NotesPageFile {
44
+ page: string
45
+ lastUpdated: string
46
+ items: NoteItem[]
47
+ }
48
+
49
+ /** Lightweight subset of the CMS manifest entry the overlay needs. */
50
+ export interface CmsManifestEntry {
51
+ tag?: string
52
+ text?: string
53
+ sourcePath?: string
54
+ sourceLine?: number
55
+ sourceSnippet?: string
56
+ }
57
+
58
+ export interface CmsPageManifest {
59
+ page: string
60
+ entries?: Record<string, CmsManifestEntry>
61
+ }
62
+
63
+ /** Author info — Phase 2 reads it from localStorage with a default. */
64
+ export interface NotesAuthor {
65
+ name: string
66
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ID generators for notes and replies.
3
+ *
4
+ * Format: `n-YYYY-MM-DD-xxxxxx` for items and `r-YYYY-MM-DD-xxxxxx` for replies.
5
+ * The date prefix makes JSON files trivially scannable in a text editor; the
6
+ * 6-character random suffix is enough for collision-free local-first usage.
7
+ */
8
+
9
+ const ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789'
10
+
11
+ function todayIsoDate(now: Date = new Date()): string {
12
+ const y = now.getUTCFullYear()
13
+ const m = String(now.getUTCMonth() + 1).padStart(2, '0')
14
+ const d = String(now.getUTCDate()).padStart(2, '0')
15
+ return `${y}-${m}-${d}`
16
+ }
17
+
18
+ function randomSuffix(length = 6): string {
19
+ let out = ''
20
+ for (let i = 0; i < length; i++) {
21
+ out += ALPHABET[Math.floor(Math.random() * ALPHABET.length)]
22
+ }
23
+ return out
24
+ }
25
+
26
+ export function generateNoteId(now: Date = new Date()): string {
27
+ return `n-${todayIsoDate(now)}-${randomSuffix()}`
28
+ }
29
+
30
+ export function generateReplyId(now: Date = new Date()): string {
31
+ return `r-${todayIsoDate(now)}-${randomSuffix()}`
32
+ }