notemend 0.1.1

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.
@@ -0,0 +1,3 @@
1
+ import{readFileSync as E,writeFileSync as v,existsSync as x,mkdirSync as b,renameSync as J,rmSync as B}from"node:fs";import{dirname as M,join as p}from"node:path";import{categoryOf as T}from"./categories.mjs";import{CATEGORIES as Q,categoryOf as U}from"./categories.mjs";import{stateOf as W,STATE_LABEL as X}from"./status.mjs";const w=r=>{const t=String(r??"").split(/[/\\]/).pop();return/^[\w-][\w.-]*\.(png|jpe?g|webp|gif)$/i.test(t)?t:""},y=r=>Math.max(r.nextId||0,r.comments.reduce((t,s)=>Math.max(t,s.id),0)+1);function L(r,{route:t,selector:s,rx:i,ry:o,text:l,label:g,viewport:I,breakpoint:N,browser:$,device:D,tag:A,classes:n,nth:u,category:d}){const e={id:y(r),route:t,selector:s,rx:i,ry:o,text:l,tag:A||"",classes:n||[],nth:Number.isInteger(u)?u:null,label:g||"",viewport:I||null,breakpoint:N||"desktop",browser:$||"",device:D||"",category:T(d).id,status:"open",note:"",replies:[],doneBy:null,images:[],createdAt:new Date().toISOString(),updatedAt:null};return r.comments.push(e),e}function C(r,t,s,i){const o=r.comments.find(l=>l.id===Number(t));return o?((o.replies||=[]).push({author:s,text:i,at:new Date().toISOString()}),o.updatedAt=new Date().toISOString(),o):null}function G(r,t,s,i){const o=r.comments.find(g=>g.id===Number(t)),l=s?o?.replies?.find(g=>g.at===s&&g.author==="you"):o;return l?(l.text=i,o.updatedAt=new Date().toISOString(),o):null}function R(r,t,s){const i=r.comments.find(o=>o.id===Number(t));return i?(Object.assign(i,s,{updatedAt:new Date().toISOString()}),i):null}function H(r){const t=p(r,"feedback","comments.json"),s=p(r,"feedback","images"),i=p(r,"feedback","archive"),o=p(i,"images"),l=n=>`feedback/images/${n}`;return{root:r,file:t,imageDir:s,archiveDir:i,archiveImageDir:o,load:()=>x(t)?JSON.parse(E(t,"utf8")):{comments:[]},save:n=>{b(M(t),{recursive:!0}),v(`${t}.tmp`,JSON.stringify(n,null,2)+`
2
+ `),J(`${t}.tmp`,t)},attach:(n,u,d)=>{const e=n.comments.find(O=>O.id===Number(u)),c=/^data:image\/(png|jpeg|webp|gif);base64,(.+)$/s.exec(d||"");if(!e||!c)return null;b(s,{recursive:!0});const f=c[1]==="jpeg"?"jpg":c[1];let a=Date.now();for(;x(p(s,`${e.id}-${a}.${f}`));)a++;const S=`${e.id}-${a}.${f}`;return v(p(s,S),Buffer.from(c[2],"base64")),(e.images||=[]).push(l(S)),e.updatedAt=new Date().toISOString(),l(S)},detach:(n,u,d)=>{const e=n.comments.find(a=>a.id===Number(u)),c=w(d),f=c&&e?.images?.find(a=>w(a)===c);return f?(B(p(s,c),{force:!0}),e.images=e.images.filter(a=>a!==f),e.updatedAt=new Date().toISOString(),!0):!1},remove:(n,u)=>{const d=n.comments.find(e=>e.id===Number(u));if(!d)return!1;for(const e of d.images||[])w(e)&&B(p(s,w(e)),{force:!0});return n.comments=n.comments.filter(e=>e.id!==Number(u)),!0},add:L,patch:R,reply:C,edit:G,nextId:y,prune:(n,{dry:u=!1,date:d=new Date().toISOString().slice(0,10)}={})=>{const e=n.comments.filter(m=>m.status==="done"),c=n.comments.filter(m=>m.status!=="done"),f=p(i,`comments-${d}.json`);if(u||!e.length)return{moving:e,keeping:c,archiveFile:f,images:e.flatMap(m=>m.images||[])};b(o,{recursive:!0});const a=[];for(const m of e)m.images=(m.images||[]).map(k=>{const h=w(k);if(!h)return k;const j=p(s,h),F=p(o,h);return x(j)&&J(j,F),a.push(h),`feedback/archive/images/${h}`});const S=x(f)?JSON.parse(E(f,"utf8")).comments||[]:[],O=new Set(S.map(m=>m.id));return v(f,JSON.stringify({comments:[...S,...e.filter(m=>!O.has(m.id))]},null,2)+`
3
+ `),n.nextId=y(n),n.comments=c,{moving:e,keeping:c,archiveFile:f,images:a}}}}export{Q as CATEGORIES,X as STATE_LABEL,L as add,U as categoryOf,H as createStore,G as edit,y as nextId,R as patch,C as reply,w as shotName,W as stateOf};
@@ -0,0 +1 @@
1
+ const o=e=>e.status!=="done"?"open":e.doneBy==="claude"?"claude":"done",d={open:"Open",done:"Resolved",claude:"Resolved by Claude"};export{d as STATE_LABEL,o as stateOf};
@@ -0,0 +1 @@
1
+ const m=e=>{const r=[{author:"you",text:e.text,at:e.createdAt,by:e.by,byName:e.byName,images:[]},...e.note?[{author:"claude",text:e.note,at:e.updatedAt,images:[]}]:[],...(e.replies||[]).map(t=>({...t,images:[]}))];for(const t of e.images||[]){const a=Number(/-(\d+)\.\w+$/.exec(t)?.[1]);let n=0;r.forEach((o,s)=>{Date.parse(o.at)<=a&&(n=s)}),r[n].images.push(t)}return r},i=e=>e.nickname||e.name||e.email,u=(e,r)=>{const t=String(e||"").toLowerCase();return t.includes("@")?[...new Set(r.filter(a=>[a.name,a.nickname,a.email].some(n=>{if(!n)return!1;const o=t.indexOf("@"+n.toLowerCase());return o!==-1&&!/[\p{L}\p{N}_.-]/u.test(t[o+n.length+1]??"")})).map(a=>a.email))]:[]};export{i as handleOf,u as mentionsIn,m as threadOf};
package/thread.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type Mentionable = { email: string; name?: string | null; nickname?: string | null };
2
+ /** What a pick from the @ list writes: the nickname, else the name, else the email */
3
+ export function handleOf(p: Mentionable): string;
4
+ /** Everyone a text @mentions, by email */
5
+ export function mentionsIn(text: string, people: Mentionable[]): string[];
package/types.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ export interface Breakpoint {
2
+ id: string;
3
+ label: string;
4
+ /** the band, in CSS px; `max: Infinity` for the widest, which has no canvas — it is your own window */
5
+ min: number;
6
+ max: number;
7
+ /** the width the canvas opens at */
8
+ ideal: number;
9
+ /** SVG paths for the switcher glyph (viewBox 0 0 20 20, stroked); defaults by id */
10
+ icon?: string;
11
+ }
12
+
13
+ /** The body drawn around the screen */
14
+ export type Shell = 'island' | 'home' | 'punch' | 'tablet' | 'macbook' | 'laptop' | 'monitor';
15
+ /** The browser chrome drawn on the screen — it decides the height the page gets */
16
+ export type Browser = 'safari-ios' | 'chrome-android' | 'safari-ipad' | 'safari-mac' | 'chrome-windows';
17
+
18
+ export interface Device {
19
+ id: string;
20
+ label: string;
21
+ /** menu section: 'Phone' | 'Tablet' | 'Laptop' | 'Desktop' | anything */
22
+ group: string;
23
+ /** the device's CSS screen size; the page gets what the browser chrome leaves of it */
24
+ w: number;
25
+ h: number;
26
+ shell: Shell;
27
+ browser: Browser;
28
+ /** the row's tooltip in the menu */
29
+ note?: string;
30
+ }
31
+
32
+ export interface NotemendOptions {
33
+ /** `false` keeps the bar out entirely — the switch Playwright flips */
34
+ enabled?: boolean;
35
+ /** bands of the breakpoint switcher; defaults to desktop ≥992 / tablet / landscape / portrait */
36
+ breakpoints?: Breakpoint[];
37
+ /** the preview shelf; defaults to the eight screens most visitors use */
38
+ devices?: Device[];
39
+ /** route -> [menu name, group]; unlisted pages fall back to their <title>. Also how an
40
+ * address a dynamic route serves (/blog/some-post) gets into the menu */
41
+ pages?: Record<string, [name: string, group?: string]>;
42
+ /** group order in the page menu; unlisted pages land in the last one. Without it the
43
+ * menu groups pages by folder */
44
+ groups?: string[];
45
+ /** show the toggle that hides Sanity's visual-editing overlay */
46
+ sanity?: boolean;
47
+ /** single-key shortcuts (C, G, 1–5…) and their list in the bar; `false` turns both off */
48
+ shortcuts?: boolean;
49
+ /** extra selectors the inspectors and comment picker must ignore */
50
+ ignore?: string[];
51
+ /** class names the grid overlay borrows from the project's own layout */
52
+ grid?: { container?: string; grid?: string; columns?: number };
53
+ /** base z-index of the bar; overlays stack around it */
54
+ zIndex?: number;
55
+ }
56
+
57
+ /** Everything but `enabled` can also live in `notemend.config.mjs` at the project root
58
+ * (named exports or a default object), re-read on every page load. Options passed
59
+ * here win over the file. */
60
+ export default function notemend(options?: NotemendOptions): any;