@01.works/visual-review 0.8.4 → 0.9.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 CHANGED
@@ -53,8 +53,9 @@ export default defineConfig({
53
53
  });
54
54
  ```
55
55
 
56
- Vite와 Next.js build는 upload token이 없어도 JSX element에
57
- `data-review-source="src/App.tsx:42:7"`를 기본으로 심습니다. 리뷰어의 코멘트는 그대로
56
+ Vite와 Next.js development/preview build는 upload token이 없어도 JSX element에
57
+ `data-review-source="src/App.tsx:42:7"`를 기본으로 심습니다. production build는
58
+ 명시적으로 `sourceMetadata: true`를 주지 않는 한 경로를 심지 않습니다. 리뷰어의 코멘트는 그대로
58
59
  파일 위치를 가지고 저장됩니다. Vite dev server(`vite dev`)도 같은 값을 내보내므로
59
60
  staging preview가 dev server여도 source 위치가 남습니다.
60
61
 
@@ -66,11 +67,7 @@ import { VisualReview } from '@01.works/visual-review/react';
66
67
 
67
68
  export function App() {
68
69
  return (
69
- <VisualReview
70
- enabled={import.meta.env.VITE_VISUAL_REVIEW_ENABLED === 'true'}
71
- developerTools={{ reactGrab: import.meta.env.DEV }}
72
- build={{ mode: 'preview' }}
73
- />
70
+ <VisualReview developerTools={{ codeContextCopy: import.meta.env.DEV }} />
74
71
  );
75
72
  }
76
73
  ```
@@ -103,10 +100,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
103
100
  <html lang="ko">
104
101
  <body>
105
102
  {children}
106
- <VisualReview
107
- enabled={process.env.NEXT_PUBLIC_VISUAL_REVIEW_ENABLED === 'true'}
108
- build={{ mode: 'preview' }}
109
- />
103
+ <VisualReview />
110
104
  </body>
111
105
  </html>
112
106
  );
@@ -118,9 +112,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
118
112
  ```ts
119
113
  import { installVisualReview } from '@01.works/visual-review';
120
114
 
121
- const review = installVisualReview({
122
- enabled: import.meta.env.VITE_VISUAL_REVIEW_ENABLED === 'true',
123
- });
115
+ const review = installVisualReview();
124
116
 
125
117
  // Host-originated History API navigation is observed automatically.
126
118
  // 필요한 경우 await review.refresh();
@@ -139,36 +131,30 @@ dirty worktree, Git 미설치, 유효하지 않은 SHA에서는 commit을 추측
139
131
  자동 탐색을 명시적으로 끕니다. 명시한 `buildId`는 서로 다른 deployment output에
140
132
  배포마다 고유해야 합니다.
141
133
  commit은 full 40/64자리 SHA만 허용합니다.
142
- build adapter는 staging/production mode 추측하지 않으므로 review deployment의
143
- component에는 `build={{ mode: 'preview' }}`를 명시합니다.
134
+ build adapter는 Vite mode 알려진 hosting provider context로 provenance mode를
135
+ 판단합니다. 별도 staging provider에서는 `visualReview({ mode: 'preview' })` 또는
136
+ `withVisualReview(config, { mode: 'preview' })`로 명시합니다.
144
137
 
145
138
  ## Staging/Preview build
146
139
 
147
- overlay flag는 browser에 공개되는 load gate입니다. production 환경에는 설정하지
148
- 말고, 실제로 review할 Preview/Staging build에만 정확히 `true`로 넣습니다.
140
+ 별도 enable 환경변수는 필요하지 않습니다. Launcher는 invitation fragment 또는 검증된
141
+ resume marker가 없으면 DOM, history listener, auth, network와 runtime import 전에
142
+ 종료합니다. 실제 활성화는 project의 exact origin 연결과 invitation 검증이 결정합니다.
149
143
 
150
144
  ```bash
151
- # Vite: .env.staging의 VITE_VISUAL_REVIEW_ENABLED=true를 build에 반영
152
145
  npm run build -- --mode staging
153
146
  npm run preview
154
-
155
- # Next.js: hosting provider의 Preview/Staging environment에만 설정
156
- NEXT_PUBLIC_VISUAL_REVIEW_ENABLED=true npm run build
157
- npm run start
158
147
  ```
159
148
 
160
- Vite `--mode staging`은 staging 값으로 **build**한 `vite preview`로 결과를
161
- 확인하기 위한 예시입니다. `vite preview` 실행 시점에 env를 추가해도 이미 생성된
162
- bundle은 바뀌지 않습니다. Next.js의 `NEXT_PUBLIC_*` 값도 build 시점에 고정되므로
163
- Preview deployment별로 별도 build해야 합니다. 이 공개 flag만으로 데이터 접근
164
- 권한이 생기지는 않습니다.
149
+ `enabled={false}`는 integrator가 있는 긴급 kill switch일 정상 설치 계약이
150
+ 아닙니다. Origin 연결 해제는 이미 열린 reviewer permission까지 닫는 운영 kill switch입니다.
165
151
 
166
152
  ## 실시간 협업
167
153
 
168
154
  `collaboration`의 두 기능은 기본적으로 꺼져 있습니다.
169
155
 
170
156
  ```tsx
171
- <VisualReview enabled collaboration={{ cursors: true, chat: true }} />
157
+ <VisualReview collaboration={{ cursors: true, chat: true }} />
172
158
  ```
173
159
 
174
160
  `cursors`는 저장하지 않는 presence만 사용하고, `chat` 메시지는 현재 review page에
@@ -179,14 +165,32 @@ Preview deployment별로 별도 build해야 합니다. 이 공개 flag만으로
179
165
  invitation과 같은 deny-by-default project 권한을 전제로 하며 browser flag 자체는
180
166
  권한을 부여하지 않습니다.
181
167
 
168
+ ## 위젯 위치
169
+
170
+ 도크는 기본적으로 우측 하단에 표시됩니다. 자유 좌표 대신 여덟 개 anchor를 사용하고,
171
+ 필요한 경우 CSS pixel 단위 offset을 더할 수 있습니다.
172
+
173
+ ```tsx
174
+ <VisualReview
175
+ position="top-left"
176
+ offset={{ x: 8, y: 12 }}
177
+ />
178
+ ```
179
+
180
+ 지원 값은 `top-left`, `top`, `top-right`, `right`, `bottom-right`, `bottom`,
181
+ `bottom-left`, `left`입니다. 양수 `x`는 오른쪽, 양수 `y`는 아래쪽으로 이동합니다.
182
+ safe-area inset은 모든 anchor에 적용됩니다. 너비 640px 이하에서는 상단·측면 배치가
183
+ 콘텐츠와 겹치지 않도록 요청한 방향에 가까운 좌측 또는 우측 하단 모서리로 자동
184
+ 보정됩니다.
185
+
182
186
  ## Activation
183
187
 
184
- `enabled`가 생략됐거나 정확히 `true`가 아니면 DOM, storage, auth, dynamic runtime
185
- import와 network bootstrap을 시작하지 않습니다. production에는 flag를 설정하지
186
- 말고 Preview/Staging environment에만 설정합니다.
188
+ Invitation fragment, 검증된 resume marker 또는 명시적 local runtime이 없으면 한 번의
189
+ namespaced storage 확인 DOM, history listener, auth, dynamic runtime import와 network
190
+ bootstrap을 시작하지 않습니다. `enabled={false}`는 긴급 kill switch입니다.
187
191
 
188
- 처음에는 `?visual-review=<invitation-id>` 초대 링크로 접근합니다. hosted service가
189
- project 단일 `allowedOrigin`과 현재 pathname을 activation gate로 확인합니다.
192
+ 처음에는 `#visual-review=<invitation-id>` 초대 링크로 접근합니다. hosted service가
193
+ project 연결된 exact Origin 목록과 현재 pathname을 activation gate로 확인합니다.
190
194
  허용된 origin에서 아직 등록되지 않은 첫 pathname은 bootstrap 시 page로 자동
191
195
  등록됩니다. bootstrap 응답은 public Instant App ID만 반환하며 reviewer session이나
192
196
  이메일을 포함하지 않습니다. 이후 magic-code login을 완료한 browser가 accept
@@ -195,7 +199,7 @@ endpoint에 refresh token을 보냅니다. Server는
195
199
  정확한 page를 선택해 Instant subscription과 overlay를 mount합니다. `revokedAt`,
196
200
  expiry와 claim 시 membership/project mismatch는 fail closed합니다.
197
201
 
198
- public `enabled` flag는 load gate이고 allowed origin/current pathname은 hosted
202
+ Invitation candidate는 load gate이고 allowed origin/current pathname은 hosted
199
203
  activation/page-selection gate입니다. 지속 InstantDB data capability는 active,
200
204
  accepted, unexpired, non-tombstoned invitation이 선택한 project 범위입니다.
201
205
  reviewer membership은 claim 검증과 comment/reply author identity에 사용되며,
@@ -207,7 +211,6 @@ integration에서는 명시적으로 service URL을 바꿀 수 있습니다.
207
211
 
208
212
  ```tsx
209
213
  <VisualReview
210
- enabled
211
214
  serviceUrl="http://localhost:8787"
212
215
  />
213
216
  ```
@@ -1 +1 @@
1
- const e=new Set([`.next`,`.nuxt`,`.output`,`.svelte-kit`,`.vite`,`_next`,`assets`,`build`,`chunk`,`chunks`,`dist`,`out`]),t=new Set([`app`,`pages`,`src`]),n=new Set([`design-system`,`shared`,`ui`]),r=/\.(?:[cm]?tsx?|jsx|vue|svelte|astro)$/u,i=/\.d\.[cm]?ts$/u;function a(e){let r=o(e),i=r.slice(0,-1),a=r.at(-1)??``;return i.includes(`node_modules`)?`dependency`:s(i,a)?`generated`:i.some(e=>n.has(e))?`shared-ui`:i.some(e=>t.has(e))?`app`:`unknown`}function o(e){return e.replaceAll(`\\`,`/`).toLowerCase().replace(/[?#].*$/u,``).replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/u,``).split(`/`).filter(e=>e!==``&&e!==`.`)}function s(n,a){if(r.test(a)&&!i.test(a))return!1;let o=n.findIndex(t=>e.has(t));if(o===-1)return!1;let s=n.findIndex(e=>t.has(e));return s===-1||o<s}function c(e){let t=e.getAttribute(`data-review-source`)?.trim();if(!t)return null;let n=/^(.*?)(?::(\d+))?(?::(\d+))?$/u.exec(t),r=n?.[1]?.trim();return r?l(r,n?.[2]?Number(n[2]):null,n?.[3]?Number(n[3]):null,e.getAttribute(`data-review-component`)?.trim()||null):null}function l(e,t,n,r){let i=a(e);return{filePath:u(e,i),lineNumber:t,columnNumber:n,componentName:r,origin:i}}function u(n,r=a(n)){let i=n.trim(),o=/^[a-z][a-z0-9+.-]*:\/\//iu.test(i)||i.startsWith(`/`)||/^[a-z]:[\\/]/iu.test(i),s=i.replaceAll(`\\`,`/`).replace(/[?#].*$/u,``).replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/iu,``).replace(/^\/+\.?(?:\/|$)/u,``);if(!o)return s||i;let c=s.split(`/`).filter(e=>e&&e!==`.`),l=r===`dependency`?c.findIndex(e=>e.toLowerCase()===`node_modules`):r===`generated`?c.findIndex(t=>e.has(t.toLowerCase())):c.findIndex(e=>t.has(e.toLowerCase()));return l>=0?c.slice(l).join(`/`):c.at(-1)??i}export{c as n,l as r,a as t};
1
+ const e=new Set([`.next`,`.nuxt`,`.output`,`.svelte-kit`,`.vite`,`_next`,`assets`,`build`,`chunk`,`chunks`,`dist`,`out`]),t=new Set([`app`,`pages`,`src`]),n=new Set([`design-system`,`shared`,`ui`]),r=/\.(?:[cm]?tsx?|jsx|vue|svelte|astro)$/u,i=/\.d\.[cm]?ts$/u;function a(e){let r=o(e),i=r.slice(0,-1),a=r.at(-1)??``;return i.includes(`node_modules`)?`dependency`:s(i,a)?`generated`:i.some(e=>n.has(e))?`shared-ui`:i.some(e=>t.has(e))?`app`:`unknown`}function o(e){return e.replaceAll(`\\`,`/`).toLowerCase().replace(/[?#].*$/u,``).replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/u,``).split(`/`).filter(e=>e!==``&&e!==`.`)}function s(n,a){if(r.test(a)&&!i.test(a))return!1;let o=n.findIndex(t=>e.has(t));if(o===-1)return!1;let s=n.findIndex(e=>t.has(e));return s===-1||o<s}function c(e){let t=e.getAttribute(`data-review-source`)?.trim();if(!t)return null;let n=/^(.*?)(?::(\d+))?(?::(\d+))?$/u.exec(t),r=n?.[1]?.trim();return r?l(r,n?.[2]?Number(n[2]):null,n?.[3]?Number(n[3]):null,e.getAttribute(`data-review-component`)?.trim()||null):null}function l(e,t,n,r){let i=a(e);return{filePath:u(e,i),lineNumber:t,columnNumber:n,componentName:r,origin:i}}function u(n,r=a(n)){let i=n.trim(),o=/^[a-z][a-z0-9+.-]*:\/\//iu.test(i)||i.startsWith(`/`)||/^[a-z]:[\\/]/iu.test(i),s=i.replaceAll(`\\`,`/`).replace(/[?#].*$/u,``).replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/iu,``).replace(/^\/+\.?(?:\/|$)/u,``);if(!o)return s||i;let c=s.split(`/`).filter(e=>e&&e!==`.`),l=r===`dependency`?c.findIndex(e=>e.toLowerCase()===`node_modules`):r===`generated`?c.findIndex(t=>e.has(t.toLowerCase())):c.findIndex(e=>t.has(e.toLowerCase()));return l>=0?c.slice(l).join(`/`):c.at(-1)??i}export{c as n,a as t};
@@ -1 +1 @@
1
- import{n as e}from"./embedded-source-e0DrdxU8.js";function t(t){return{unavailableProvenance:()=>n(t),async resolve(i,a){let o=a?.signal;r(o);let s=i.closest(`[data-review-source]`),c=s?e(s):null;return r(o),{source:c,componentStack:c?[c]:[],provenance:n(t)}}}}function n(e){return{provider:`embedded`,build:e.build,resolvedAt:e.now?.()??Date.now()}}function r(e){if(e?.aborted)throw e.reason??Error(`Source context resolution aborted`)}export{t as createGeneratedSourceContextResolver};
1
+ import{n as e}from"./embedded-source-DSDHbbeU.js";function t(t){return{unavailableProvenance:()=>n(t),async resolve(i,a){let o=a?.signal;r(o);let s=i.closest(`[data-review-source]`),c=s?e(s):null;return r(o),{source:c,componentStack:c?[c]:[],provenance:n(t)}}}}function n(e){return{provider:`embedded`,build:e.build,resolvedAt:e.now?.()??Date.now()}}function r(e){if(e?.aborted)throw e.reason??Error(`Source context resolution aborted`)}export{t as createGeneratedSourceContextResolver};
@@ -0,0 +1,13 @@
1
+ import{r as e}from"./install-7I-VPQeH.js";import{t}from"./embedded-source-DSDHbbeU.js";import{i as n,init as r}from"@instantdb/core";const i=n.schema({entities:{$files:n.entity({path:n.string().unique().indexed(),url:n.string()}),$users:n.entity({email:n.string().unique().indexed()}),projects:n.entity({name:n.string(),slug:n.string().unique().indexed(),allowedOrigin:n.string().optional(),originDisconnectedAt:n.number().optional(),originModelVersion:n.number().optional(),createdAt:n.number().indexed(),updatedAt:n.number()}),projectOrigins:n.entity({projectId:n.string().indexed(),originKey:n.string().unique().indexed(),origin:n.string().indexed(),label:n.string().optional(),createdAt:n.number().indexed(),updatedAt:n.number()}),reviewPages:n.entity({projectId:n.string().indexed(),autoRegistrationSlot:n.string().unique().indexed().optional(),name:n.string(),pathname:n.string(),stagingOrigin:n.string(),createdAt:n.number().indexed(),updatedAt:n.number()}),reviewPageBuilds:n.entity({projectId:n.string().indexed(),pageId:n.string().unique().indexed(),buildId:n.string(),gitCommit:n.string().optional(),generatedAt:n.number().optional(),mode:n.string(),updatedAt:n.number().indexed()}),reviewers:n.entity({authUserId:n.string().indexed(),projectId:n.string().indexed(),email:n.string().indexed(),displayName:n.string(),role:n.string(),createdAt:n.number()}),reviewerIdentityClaims:n.entity({reviewerId:n.string().unique().indexed(),projectId:n.string().indexed(),authUserId:n.string().indexed(),email:n.string(),displayName:n.string(),role:n.string(),createdAt:n.number()}),invitations:n.entity({projectId:n.string().indexed(),reviewerId:n.string().indexed().optional(),email:n.string().indexed(),status:n.string().indexed(),claimRevision:n.number(),claimedByAuthUserId:n.string().indexed().optional(),expiresAt:n.number().indexed(),createdAt:n.number(),acceptedAt:n.number().optional(),revokedAt:n.number().optional()}),comments:n.entity({pageId:n.string().indexed(),pageNumber:n.number().optional(),authorId:n.string().indexed(),authorAuthUserId:n.string().indexed(),authorName:n.string(),body:n.string(),bodyContentMarker:n.string().optional(),anchorSelector:n.string().optional(),anchorOffsetX:n.number().optional(),anchorOffsetY:n.number().optional(),anchorViewportX:n.number().optional(),anchorViewportY:n.number().optional(),targetKind:n.string().optional(),targetPageX:n.number().optional(),targetPageY:n.number().optional(),targetWidth:n.number().optional(),targetHeight:n.number().optional(),targetViewportX:n.number().optional(),targetViewportY:n.number().optional(),targetViewportWidth:n.number().optional(),targetViewportHeight:n.number().optional(),targetPageOrigin:n.string().optional(),targetPagePathname:n.string().optional(),targetPageSearch:n.string().optional(),targetPageHash:n.string().optional(),targetPageTitle:n.string().optional(),targetCapturedAt:n.number().optional(),status:n.string().indexed(),workflowRevision:n.number(),createdAt:n.number().indexed(),updatedAt:n.number().indexed(),resolvedAt:n.number().optional(),resolvedById:n.string().optional()}),commentWorkflowClaims:n.entity({claimKey:n.string().unique().indexed(),commentId:n.string().indexed(),expectedWorkflowRevision:n.number(),createdAt:n.number().indexed()}),commentPageNumberClaims:n.entity({claimKey:n.string().unique().indexed(),pageId:n.string().indexed(),pageNumber:n.number(),commentId:n.string().indexed(),createdAt:n.number().indexed()}),commentTargetElements:n.entity({commentId:n.string().indexed(),slotKey:n.string().unique().indexed().optional(),ordinal:n.number(),selector:n.string(),tagName:n.string(),textPreview:n.string(),pageX:n.number(),pageY:n.number(),width:n.number(),height:n.number(),viewportX:n.number(),viewportY:n.number(),viewportWidth:n.number(),viewportHeight:n.number(),authorAuthUserId:n.string().indexed()}),commentScopeEntries:n.entity({commentId:n.string().indexed(),slotKey:n.string().unique().indexed().optional(),ordinal:n.number(),key:n.string(),valueType:n.string(),stringValue:n.string().optional(),numberValue:n.number().optional(),booleanValue:n.boolean().optional(),authorAuthUserId:n.string().indexed()}),commentSourceContexts:n.entity({commentId:n.string().unique().indexed(),pageId:n.string().indexed(),authorAuthUserId:n.string().indexed(),contextJson:n.string(),createdAt:n.number().indexed()}),replies:n.entity({pageId:n.string().indexed(),commentId:n.string().indexed(),authorId:n.string().indexed(),authorAuthUserId:n.string().indexed(),authorName:n.string(),body:n.string(),bodyContentMarker:n.string().optional(),createdAt:n.number().indexed(),updatedAt:n.number().indexed()}),reviewMessages:n.entity({pageId:n.string().indexed(),pathname:n.string().indexed(),authorId:n.string().indexed(),authorAuthUserId:n.string().indexed(),authorName:n.string(),body:n.string(),bodyContentMarker:n.string(),createdAt:n.number().indexed(),updatedAt:n.number().indexed()})},links:{projectOwners:{forward:{on:`projects`,has:`many`,label:`owners`},reverse:{on:`$users`,has:`many`,label:`ownedProjects`}},projectPages:{forward:{on:`projects`,has:`many`,label:`pages`},reverse:{on:`reviewPages`,has:`one`,label:`project`}},projectOriginProject:{forward:{on:`projects`,has:`many`,label:`origins`},reverse:{on:`projectOrigins`,has:`one`,label:`project`}},pageCurrentBuild:{forward:{on:`reviewPageBuilds`,has:`one`,label:`page`,required:!0,onDelete:`cascade`},reverse:{on:`reviewPages`,has:`one`,label:`currentBuild`}},projectReviewers:{forward:{on:`projects`,has:`many`,label:`reviewers`},reverse:{on:`reviewers`,has:`one`,label:`project`}},reviewerUser:{forward:{on:`reviewers`,has:`one`,label:`$user`},reverse:{on:`$users`,has:`many`,label:`reviewerMemberships`}},projectInvitations:{forward:{on:`projects`,has:`many`,label:`invitations`},reverse:{on:`invitations`,has:`one`,label:`project`}},invitationReviewer:{forward:{on:`invitations`,has:`one`,label:`reviewer`},reverse:{on:`reviewers`,has:`many`,label:`acceptedInvitations`}},invitationActiveUser:{forward:{on:`invitations`,has:`one`,label:`activeUser`},reverse:{on:`$users`,has:`one`,label:`activeReviewInvitation`}},pageComments:{forward:{on:`reviewPages`,has:`many`,label:`comments`},reverse:{on:`comments`,has:`one`,label:`page`}},pageReviewMessages:{forward:{on:`reviewPages`,has:`many`,label:`reviewMessages`},reverse:{on:`reviewMessages`,has:`one`,label:`page`}},commentReplies:{forward:{on:`comments`,has:`many`,label:`replies`},reverse:{on:`replies`,has:`one`,label:`comment`}},commentTargetElements:{forward:{on:`commentTargetElements`,has:`one`,label:`comment`,required:!0,onDelete:`cascade`},reverse:{on:`comments`,has:`many`,label:`targetElements`}},commentScopeEntries:{forward:{on:`commentScopeEntries`,has:`one`,label:`comment`,required:!0,onDelete:`cascade`},reverse:{on:`comments`,has:`many`,label:`scopeEntries`}},commentSourceContext:{forward:{on:`commentSourceContexts`,has:`one`,label:`comment`,required:!0,onDelete:`cascade`},reverse:{on:`comments`,has:`one`,label:`sourceContext`}},pageSourceContexts:{forward:{on:`commentSourceContexts`,has:`one`,label:`page`,required:!0},reverse:{on:`reviewPages`,has:`many`,label:`sourceContexts`}},sourceContextAuthor:{forward:{on:`commentSourceContexts`,has:`one`,label:`author`,required:!0},reverse:{on:`reviewers`,has:`many`,label:`sourceContexts`}},commentAuthor:{forward:{on:`comments`,has:`one`,label:`author`},reverse:{on:`reviewers`,has:`many`,label:`comments`}},replyAuthor:{forward:{on:`replies`,has:`one`,label:`author`},reverse:{on:`reviewers`,has:`many`,label:`replies`}},reviewMessageAuthor:{forward:{on:`reviewMessages`,has:`one`,label:`author`},reverse:{on:`reviewers`,has:`many`,label:`reviewMessages`}}},rooms:{visualReview:{presence:n.entity({reviewerId:n.string(),displayName:n.string(),color:n.string(),active:n.boolean(),pathname:n.string().optional(),cursorX:n.number().optional(),cursorY:n.number().optional(),viewportWidth:n.number().optional(),viewportHeight:n.number().optional(),cursorChat:n.string().optional()})}}}),a=new Uint8Array([107,167,184,17,157,173,17,209,128,180,0,192,79,212,48,200]);function o(e,t,n){return u(e,n),`${e}:${t}:${n}`}async function s(e,t,n){return l(o(e,t,n))}async function c(e){if(!e.trim())throw Error(`Comment ID is required for source context`);return l(`${e}:source-context`)}async function l(e){let t=globalThis.crypto?.subtle;if(!t)throw Error(`Web Crypto is required to persist review capture context`);let n=new TextEncoder().encode(`01works.visual-review/v1/${e}`),r=new Uint8Array(a.length+n.length);r.set(a),r.set(n,a.length);let i=new Uint8Array(await t.digest(`SHA-1`,r)).slice(0,16);i[6]=i[6]&15|80,i[8]=i[8]&63|128;let o=[...i].map(e=>e.toString(16).padStart(2,`0`)).join(``);return`${o.slice(0,8)}-${o.slice(8,12)}-${o.slice(12,16)}-${o.slice(16,20)}-${o.slice(20)}`}function u(e,t){if(!e.trim())throw Error(`Comment ID is required for capture context`);if(!Number.isSafeInteger(t)||t<0)throw Error(`Capture child ordinal must be a non-negative integer`)}const d=131072,f=32768,p=4096,m=1e3;var h=class extends Error{constructor(e,t){super(e,t),this.name=`InstantCaptureIntegrityError`}};function g(e){let t=se(e,`comment target`,!0),n={kind:t.kind,anchor:pe(t.anchor),region:ge(t.region),elements:t.elements.map(e=>({selector:e.selector,tagName:e.tagName,textPreview:e.textPreview,rect:ge(e.rect),source:null,componentStack:[]})),pageUrl:t.pageUrl,pageTitle:t.pageTitle,capturedAt:t.capturedAt},r={elements:t.elements.map(e=>({selector:e.selector,source:e.source?_e(e.source):null,componentStack:e.componentStack.map(_e)})),...t.sourceProvenance?{sourceProvenance:ve(t.sourceProvenance)}:{}};E(n,d,`Public target`);let i=Te(r,65536,`Source context`);return{publicTarget:n,commentFields:te(n),targetElements:n.elements.map(ne),sourceContextJson:i}}function ee(e,t,n){let r=x(e,n),i={selector:S(r.anchorSelector,`${n} anchor selector`,p),offsetX:T(r.anchorOffsetX,`${n} anchor offsetX`),offsetY:T(r.anchorOffsetY,`${n} anchor offsetY`),viewportX:T(r.anchorViewportX,`${n} anchor viewportX`),viewportY:T(r.anchorViewportY,`${n} anchor viewportY`)},a=ie({kind:r.targetKind,anchor:i,region:{pageX:r.targetPageX,pageY:r.targetPageY,width:r.targetWidth,height:r.targetHeight,viewportX:r.targetViewportX,viewportY:r.targetViewportY,viewportWidth:r.targetViewportWidth,viewportHeight:r.targetViewportHeight},elements:[],pageUrl:he({origin:C(r.targetPageOrigin,`${n} page origin`,1024),pathname:C(r.targetPagePathname,`${n} page pathname`,4096),search:S(r.targetPageSearch,`${n} page search`,1024),hash:S(r.targetPageHash,`${n} page hash`,1024)}),pageTitle:r.targetPageTitle,capturedAt:r.targetCapturedAt},n),o=re(t,`${n} elements`),s=a;for(let e of o)try{s=ie({...a,elements:[...s.elements,e]},n)}catch{}return s}function _(e){let t=oe(e);return t?Object.entries(t).map(([e,t],n)=>typeof t==`string`?{ordinal:n,key:e,valueType:`string`,stringValue:t}:typeof t==`number`?{ordinal:n,key:e,valueType:`number`,numberValue:t}:typeof t==`boolean`?{ordinal:n,key:e,valueType:`boolean`,booleanValue:t}:{ordinal:n,key:e,valueType:`null`}):[]}function v(e){if(!Array.isArray(e)||e.length===0)return;let t=[...e].map(e=>{try{let t=x(e,`Comment scope entry`);return{row:t,ordinal:xe(t.ordinal,`Comment scope entry ordinal`,0,19),id:typeof t.id==`string`?t.id:``}}catch{return null}}).filter(e=>e!==null).sort((e,t)=>e.ordinal-t.ordinal||e.id.localeCompare(t.id)),n={},r=new Set;for(let{row:e,ordinal:i}of t)if(!r.has(i)){r.add(i);try{let t=C(e.key,`Comment scope key`,256);if(Object.hasOwn(n,t))continue;switch(e.valueType){case`string`:n[t]=S(e.stringValue,`Comment scope ${t}`,m);break;case`number`:n[t]=w(e.numberValue,`Comment scope ${t}`);break;case`boolean`:if(typeof e.booleanValue!=`boolean`)throw D(`Comment scope ${t} is invalid`);n[t]=e.booleanValue;break;case`null`:n[t]=null;break;default:throw D(`Comment scope ${t} is invalid`)}}catch{}}if(Object.keys(n).length!==0)try{return E(n,f,`Comment scope`),n}catch{return}}function te(e){let t=new URL(e.pageUrl);return{anchorSelector:e.anchor.selector,anchorOffsetX:e.anchor.offsetX,anchorOffsetY:e.anchor.offsetY,anchorViewportX:e.anchor.viewportX,anchorViewportY:e.anchor.viewportY,targetKind:e.kind,targetPageX:e.region.pageX,targetPageY:e.region.pageY,targetWidth:e.region.width,targetHeight:e.region.height,targetViewportX:e.region.viewportX,targetViewportY:e.region.viewportY,targetViewportWidth:e.region.viewportWidth,targetViewportHeight:e.region.viewportHeight,targetPageOrigin:t.origin,targetPagePathname:me(t.pathname),targetPageSearch:t.search.slice(1),targetPageHash:t.hash.slice(1),targetPageTitle:e.pageTitle,targetCapturedAt:e.capturedAt}}function ne(e,t){return{ordinal:t,selector:e.selector,tagName:e.tagName,textPreview:e.textPreview,pageX:e.rect.pageX,pageY:e.rect.pageY,width:e.rect.width,height:e.rect.height,viewportX:e.rect.viewportX,viewportY:e.rect.viewportY,viewportWidth:e.rect.viewportWidth,viewportHeight:e.rect.viewportHeight}}function re(e,t){if(!Array.isArray(e)||e.length===0)return[];let n=[...e].map(e=>{try{let n=x(e,t);return{row:n,ordinal:xe(n.ordinal,`${t} ordinal`,0,7),id:typeof n.id==`string`?n.id:``}}catch{return null}}).filter(e=>e!==null).sort((e,t)=>e.ordinal-t.ordinal||e.id.localeCompare(t.id)),r=[],i=new Set;for(let{row:e,ordinal:a}of n)if(!i.has(a)){i.add(a);try{r.push(ce({selector:e.selector,tagName:e.tagName,textPreview:e.textPreview,rect:{pageX:e.pageX,pageY:e.pageY,width:e.width,height:e.height,viewportX:e.viewportX,viewportY:e.viewportY,viewportWidth:e.viewportWidth,viewportHeight:e.viewportHeight},source:null,componentStack:[]},`${t}[${a}]`,!1))}catch{}}return r}function ie(e,t){return se(e,t,!1)}function y(e,t=`Comment body`){return C(e,t,2e4)}function ae(e,t=`Comment body`){let n=y(e,t);for(let e of n)if(e.trim())return e;throw D(`${t} is empty`)}function oe(e){if(e==null)return;let t=x(e,`Comment scope`);if(Object.keys(t).length>20)throw D(`Comment scope has too many fields`);let n={};for(let[e,r]of Object.entries(t)){if(!e.trim()||e.length>256)throw D(`Comment scope has an invalid key`);if(typeof r==`string`){if(r.length>m)throw D(`Comment scope ${e} is too long`);n[e]=r}else if(typeof r==`number`)n[e]=w(r,`Comment scope ${e}`);else if(typeof r==`boolean`||r===null)n[e]=r;else throw D(`Comment scope ${e} is invalid`)}return E(n,f,`Comment scope`),n}function se(e,t,n){let r=b(e,t,[`kind`,`anchor`,`region`,`elements`,`pageUrl`,`pageTitle`,`capturedAt`,`sourceProvenance`],[`sourceProvenance`]),i=r.kind;if(i!==`element`&&i!==`region`&&i!==`viewport`)throw D(`${t} kind is invalid`);let a=ye(r.elements,`${t} elements`);if(a.length>8)throw D(`${t} contains too many elements`);if(!n&&r.sourceProvenance!=null)throw D(`${t} exposes source provenance`);let o=C(r.pageUrl,`${t} pageUrl`,8192);we(o,`${t} pageUrl`);let s={kind:i,anchor:le(r.anchor,`${t} anchor`),region:ue(r.region,`${t} region`),elements:a.map((e,r)=>ce(e,`${t} elements[${r}]`,n)),pageUrl:o,pageTitle:S(r.pageTitle,`${t} pageTitle`,m),capturedAt:Se(r.capturedAt,`${t} capturedAt`),...n&&r.sourceProvenance!=null?{sourceProvenance:fe(r.sourceProvenance,`${t} provenance`)}:{}};return E(s,n?196608:d,t),s}function ce(e,t,n){let r=b(e,t,[`selector`,`tagName`,`textPreview`,`rect`,`source`,`componentStack`]),i=ye(r.componentStack,`${t} componentStack`);if(i.length>32)throw D(`${t} componentStack is too large`);if(!n&&(r.source!=null||i.length>0))throw D(`${t} exposes source metadata`);return{selector:C(r.selector,`${t} selector`,p),tagName:C(r.tagName,`${t} tagName`,128),textPreview:S(r.textPreview,`${t} textPreview`,m),rect:ue(r.rect,`${t} rect`),source:n&&r.source!=null?de(r.source,`${t} source`):null,componentStack:n?i.map((e,n)=>de(e,`${t} stack[${n}]`)):[]}}function le(e,t){let n=b(e,t,[`selector`,`offsetX`,`offsetY`,`viewportX`,`viewportY`]);return{selector:S(n.selector,`${t} selector`,p),offsetX:T(n.offsetX,`${t} offsetX`),offsetY:T(n.offsetY,`${t} offsetY`),viewportX:T(n.viewportX,`${t} viewportX`),viewportY:T(n.viewportY,`${t} viewportY`)}}function ue(e,t){let n=b(e,t,[`pageX`,`pageY`,`width`,`height`,`viewportX`,`viewportY`,`viewportWidth`,`viewportHeight`]),r={pageX:w(n.pageX,`${t} pageX`),pageY:w(n.pageY,`${t} pageY`),width:w(n.width,`${t} width`),height:w(n.height,`${t} height`),viewportX:w(n.viewportX,`${t} viewportX`),viewportY:w(n.viewportY,`${t} viewportY`),viewportWidth:w(n.viewportWidth,`${t} viewportWidth`),viewportHeight:w(n.viewportHeight,`${t} viewportHeight`)};if(r.width<0||r.height<0||r.viewportWidth<0||r.viewportHeight<0)throw D(`${t} has a negative dimension`);return r}function de(e,n){let r=b(e,n,[`filePath`,`lineNumber`,`columnNumber`,`componentName`,`origin`,`generated`],[`generated`]),i=r.origin;if(i!==`app`&&i!==`shared-ui`&&i!==`dependency`&&i!==`generated`&&i!==`unknown`)throw D(`${n} origin is invalid`);let a=C(r.filePath,`${n} filePath`,p);return{filePath:a,lineNumber:Ce(r.lineNumber,`${n} lineNumber`),columnNumber:Ce(r.columnNumber,`${n} columnNumber`),componentName:r.componentName==null?null:C(r.componentName,`${n} componentName`,512),origin:t(a)}}function fe(e,t){let n=b(e,t,[`provider`,`build`,`resolvedAt`]),r=n.provider;if(r!==`react-grab`&&r!==`embedded`&&r!==`custom`)throw D(`${t} provider is invalid`);let i=b(n.build,`${t} build`,[`buildId`,`gitCommit`,`generatedAt`,`mode`],[`generatedAt`]),a=i.mode;if(a!==`development`&&a!==`preview`&&a!==`production`)throw D(`${t} build mode is invalid`);return{provider:r,build:{buildId:C(i.buildId,`${t} buildId`,512),gitCommit:i.gitCommit==null?null:C(i.gitCommit,`${t} gitCommit`,256),...i.generatedAt==null?{}:{generatedAt:Se(i.generatedAt,`${t} generatedAt`)},mode:a},resolvedAt:Se(n.resolvedAt,`${t} resolvedAt`)}}function pe(e){return{...e}}function me(e){return e.replace(/\/+$/,``)||`/`}function he(e){return`${e.origin}${e.pathname}${e.search?`?${e.search}`:``}${e.hash?`#${e.hash}`:``}`}function ge(e){return{...e}}function _e(e){return{...e}}function ve(e){return{...e,build:{...e.build}}}function b(e,t,n,r=[]){let i=x(e,t);if(Object.keys(i).some(e=>!n.includes(e)))throw D(`${t} contains unknown fields`);for(let e of n)if(!r.includes(e)&&!Object.hasOwn(i,e))throw D(`${t} is missing ${e}`);return i}function x(e,t){if(!e||typeof e!=`object`||Array.isArray(e))throw D(`${t} is missing or invalid`);return e}function ye(e,t){if(!Array.isArray(e))throw D(`${t} is missing or invalid`);return e}function S(e,t,n){if(typeof e!=`string`||be(e,n)>n)throw D(`${t} is invalid or too long`);return e}function be(e,t){let n=0;for(let r of e)if(n+=1,n>t)return n;return n}function C(e,t,n){let r=S(e,t,n);if(!r.trim())throw D(`${t} is empty`);return r}function w(e,t){if(typeof e!=`number`||!Number.isFinite(e))throw D(`${t} is invalid`);return e}function xe(e,t,n,r){if(typeof e!=`number`||!Number.isSafeInteger(e)||e<n||e>r)throw D(`${t} is invalid`);return e}function T(e,t){let n=w(e,t);if(n<0||n>1)throw D(`${t} is outside 0..1`);return n}function Se(e,t){if(typeof e!=`number`||!Number.isSafeInteger(e)||e<0||e>0xe677d21fdbff)throw D(`${t} is invalid`);return e}function Ce(e,t){if(e===null)return null;if(typeof e!=`number`||!Number.isSafeInteger(e)||e<0)throw D(`${t} is invalid`);return e}function we(e,t){let n;try{n=new URL(e)}catch(e){throw D(`${t} is invalid`,e)}if(n.protocol!==`http:`&&n.protocol!==`https:`)throw D(`${t} must use HTTP or HTTPS`)}function E(e,t,n){Te(e,t,n)}function Te(e,t,n){let r;try{r=JSON.stringify(e)}catch(e){throw D(`${n} cannot be serialized`,e)}if(r.length>t)throw D(`${n} is too large`);return r}function D(e,t){return new h(e,{cause:t instanceof Error?t:void 0})}function Ee(e){if(!e.trim())throw Error(`InstantDB app ID is required`);return r({appId:e,schema:i,devtool:!1})}var De=class{db;session;#e;attachScreenshot;constructor(e,t,n={}){this.db=e,this.session=t,this.#e=n.commentTransport,this.#e&&(this.attachScreenshot=(e,t,n)=>this.#e.uploadScreenshot({pageId:e,commentId:t,screenshot:n}))}async getSnapshot(e){let{data:t}=await this.db.queryOnce(Oe(e,this.session.authUserId));return Ae(e,t.comments,t.replies)}async getProjectSnapshot(e){this.#t(e);let{data:t}=await this.db.queryOnce(ke(e,this.session.authUserId));return je(e,t.comments,t.replies)}subscribe(e,t,n){return this.db.subscribeQuery(Oe(e,this.session.authUserId),r=>{if(r.error){n?.(Re(r.error));return}if(r.data)try{t(Ae(e,r.data.comments,r.data.replies))}catch{n?.({type:`error`,code:`data-integrity`,recovery:`retry`,clearSnapshot:!0})}})}subscribeProject(e,t,n){return this.#t(e),this.db.subscribeQuery(ke(e,this.session.authUserId),r=>{if(r.error){n?.(Re(r.error));return}if(r.data)try{t(je(e,r.data.comments,r.data.replies))}catch{n?.({type:`error`,code:`data-integrity`,recovery:`retry`,clearSnapshot:!0})}})}async mutate(e,t){switch(t.type){case`comment.upsert`:{if(t.comment.pageId!==e)throw Error(`Comment page mismatch`);let n=t.comment;if(n.authorId!==this.session.reviewerId)throw Error(`Comment author mismatch`);let r=y(n.body).trim(),i=ae(r),a=M(n.createdAt,`Comment createdAt`),l=M(n.updatedAt,`Comment updatedAt`);if(l<a)throw new h(`Comment updatedAt precedes createdAt`);if(n.createdAt!==n.updatedAt)throw new h(`Comment edits must go through the review service`);if(n.status!==`open`||n.resolvedAt!==void 0||n.resolvedById!==void 0)throw new h(`New comments must start open`);let u=g(n.target);if(!Ie(n.anchor,u.publicTarget.anchor))throw new h(`Comment anchor does not match its target`);if(this.#e){await this.#e.createComment({pageId:e,commentId:n.id,body:r,target:n.target,...n.scope===void 0?{}:{scope:n.scope}});return}let d=_(n.scope),f=await Promise.all(u.targetElements.map(async e=>({element:e,id:await s(n.id,`element`,e.ordinal),slotKey:o(n.id,`element`,e.ordinal)}))),p=await Promise.all(d.map(async e=>({entry:e,id:await s(n.id,`scope`,e.ordinal),slotKey:o(n.id,`scope`,e.ordinal)}))),m=await c(n.id);await this.db.transact([this.db.tx.comments[n.id].update({pageId:e,authorId:this.session.reviewerId,authorAuthUserId:this.session.authUserId,authorName:this.session.displayName,body:r,bodyContentMarker:i,...u.commentFields,status:n.status,workflowRevision:0,createdAt:a,updatedAt:l}).link({page:e,author:this.session.reviewerId}),...f.map(({element:e,id:t,slotKey:r})=>this.db.tx.commentTargetElements[t].update({commentId:n.id,slotKey:r,authorAuthUserId:this.session.authUserId,...e}).link({comment:n.id})),...p.map(({entry:e,id:t,slotKey:r})=>this.db.tx.commentScopeEntries[t].update({commentId:n.id,slotKey:r,authorAuthUserId:this.session.authUserId,...e}).link({comment:n.id})),this.db.tx.commentSourceContexts[m].update({commentId:n.id,pageId:e,authorAuthUserId:this.session.authUserId,contextJson:u.sourceContextJson,createdAt:a}).link({comment:n.id,page:e,author:this.session.reviewerId})]);return}case`comment.delete`:throw new h(`Comment deletion must go through the review service`);case`reply.upsert`:{let n=t.reply;if(n.authorId!==this.session.reviewerId)throw Error(`Reply author mismatch`);let r=y(n.body,`Reply body`).trim(),i=ae(r,`Reply body`),a=M(n.createdAt,`Reply createdAt`),o=M(n.updatedAt,`Reply updatedAt`);if(o<a)throw new h(`Reply updatedAt precedes createdAt`);if(n.createdAt!==n.updatedAt)throw new h(`Reply edits must go through the review service`);if(this.#e){await this.#e.createReply({commentId:n.commentId,replyId:n.id,body:r});return}await this.db.transact(this.db.tx.replies[n.id].update({pageId:e,commentId:n.commentId,authorId:this.session.reviewerId,authorAuthUserId:this.session.authUserId,authorName:this.session.displayName,body:r,bodyContentMarker:i,createdAt:a,updatedAt:o}).link({comment:n.commentId,author:this.session.reviewerId}));return}case`reply.delete`:throw new h(`Reply deletion must go through the review service`)}}#t(e){if(!e.trim()||e!==this.session.projectId)throw Error(`Project does not belong to the reviewer session`)}};function Oe(e,t){return{comments:{$:{where:{pageId:e},order:{createdAt:`asc`}},targetElements:{},scopeEntries:{}},replies:{$:{where:{pageId:e},order:{createdAt:`asc`}}},$users:{$:{where:{id:t}},activeReviewInvitation:{project:{}}}}}function ke(e,t){return{comments:{$:{where:{"page.project.id":e},order:{createdAt:`asc`}},page:{project:{}},targetElements:{},scopeEntries:{}},replies:{$:{where:{"comment.page.project.id":e},order:{createdAt:`asc`}},comment:{page:{project:{}}}},$users:{$:{where:{id:t}},activeReviewInvitation:{project:{}}}}}function Ae(e,t,n){let r=t.map(t=>Ne(t,e));O(r,`comment`);let i=new Set(r.map(({id:e})=>e)),a=n.map(t=>Pe(t,e));O(a,`reply`);for(let e of a)if(!i.has(e.commentId))throw new h(`Reply ${e.id} references a comment outside the page snapshot`);return{pageId:e,comments:r,replies:a}}function je(e,t,n){let r=new Map,i=t.map(t=>{let n=A(t.pageId,`comment pageId`);Me(t.page,n,e,`comment ${String(t.id)}`);let i=k(t.page,`comment ${String(t.id)} page`),a=Ne(t,n),o=`/`;try{o=new URL(a.target.pageUrl).pathname||`/`}catch{}return r.set(n,{id:n,name:typeof i.name==`string`&&i.name.trim()?i.name.trim():a.target.pageTitle.trim()||o,pathname:typeof i.pathname==`string`&&i.pathname.trim()?i.pathname.trim():o}),a});O(i,`comment`);let a=new Map(i.map(e=>[e.id,e])),o=n.map(t=>{let n=A(t.pageId,`reply pageId`),r=k(t.comment,`reply ${String(t.id)} comment`),i=A(t.commentId,`reply ${String(t.id)} commentId`);if(A(r.id,`reply ${String(t.id)} comment relation id`)!==i)throw new h(`Reply ${String(t.id)} has an inconsistent comment relation`);Me(r.page,n,e,`reply ${String(t.id)}`);let o=Pe(t,n),s=a.get(o.commentId);if(!s||s.pageId!==n)throw new h(`Reply ${o.id} references a comment outside the project snapshot`);return o});return O(o,`reply`),{projectId:e,pages:[...r.values()].sort((e,t)=>e.pathname.localeCompare(t.pathname)||e.id.localeCompare(t.id)),comments:i,replies:o}}function Me(e,t,n,r){let i=k(e,`${r} page`);if(A(i.id,`${r} page id`)!==t)throw new h(`${r} escaped its page relation`);if(A(k(i.project,`${r} project`).id,`${r} project id`)!==n)throw new h(`${r} escaped its project scope`)}function Ne(e,t){let n=k(e,`comment`),r=A(n.id,`comment id`);if(A(n.pageId,`comment ${r} pageId`)!==t)throw new h(`Comment ${r} escaped its page scope`);A(n.authorAuthUserId,`comment ${r} authorAuthUserId`);let i=!(Object.hasOwn(n,`anchor`)&&Object.hasOwn(n,`target`)),a=i?ee(n,n.targetElements,`comment ${r} target`):ie(n.target,`comment ${r} target`),o=i?{...a.anchor}:Fe(n.anchor,`comment ${r} anchor`);if(!Ie(o,a.anchor))throw new h(`Comment ${r} has inconsistent anchors`);let s=n.status;if(s!==`open`&&s!==`resolved`)throw new h(`Comment ${r} has an invalid status`);let c=M(n.createdAt,`comment ${r} createdAt`),l=M(n.updatedAt,`comment ${r} updatedAt`);if(l<c)throw new h(`Comment ${r} updatedAt precedes createdAt`);let u=He(n.resolvedAt,`comment ${r} resolvedAt`),d=Ue(n.resolvedById,`comment ${r} resolvedById`);if(s===`open`&&(u!==void 0||d!==void 0))throw new h(`Open comment ${r} contains resolution metadata`);if(s===`resolved`&&(u===void 0||d===void 0||u!==l))throw new h(`Resolved comment ${r} has invalid workflow metadata`);return{id:r,pageId:t,...We(n.pageNumber,`comment ${r} pageNumber`),authorId:A(n.authorId,`comment ${r} authorId`),authorName:A(n.authorName,`comment ${r} authorName`),body:y(n.body,`comment ${r} body`),anchor:o,target:a,...i?Le(v(n.scopeEntries)):n.scope==null?{}:{scope:oe(n.scope)},status:s,workflowRevision:Ve(n.workflowRevision,`comment ${r} workflowRevision`),createdAt:c,updatedAt:l,...u===void 0?{}:{resolvedAt:u},...d===void 0?{}:{resolvedById:d}}}function Pe(e,t){let n=k(e,`reply`),r=A(n.id,`reply id`);if(A(n.pageId,`reply ${r} pageId`)!==t)throw new h(`Reply ${r} escaped its page scope`);A(n.authorAuthUserId,`reply ${r} authorAuthUserId`);let i=M(n.createdAt,`reply ${r} createdAt`),a=M(n.updatedAt,`reply ${r} updatedAt`);if(a<i)throw new h(`Reply ${r} updatedAt precedes createdAt`);return{id:r,commentId:A(n.commentId,`reply ${r} commentId`),authorId:A(n.authorId,`reply ${r} authorId`),authorName:A(n.authorName,`reply ${r} authorName`),body:y(n.body,`reply ${r} body`),createdAt:i,updatedAt:a}}function Fe(e,t){let n=k(e,t);if(Object.keys(n).length!==5||!Object.hasOwn(n,`selector`)||!Object.hasOwn(n,`offsetX`)||!Object.hasOwn(n,`offsetY`)||!Object.hasOwn(n,`viewportX`)||!Object.hasOwn(n,`viewportY`))throw new h(`${t} has invalid fields`);return{selector:Be(n.selector,`${t} selector`),offsetX:j(n.offsetX,`${t} offsetX`),offsetY:j(n.offsetY,`${t} offsetY`),viewportX:j(n.viewportX,`${t} viewportX`),viewportY:j(n.viewportY,`${t} viewportY`)}}function Ie(e,t){return e.selector===t.selector&&e.offsetX===t.offsetX&&e.offsetY===t.offsetY&&e.viewportX===t.viewportX&&e.viewportY===t.viewportY}function Le(e){return e===void 0?{}:{scope:e}}function O(e,t){let n=new Set;for(let r of e){if(n.has(r.id))throw new h(`Duplicate ${t} ${r.id}`);n.add(r.id)}}function Re(e){let t=ze(e).toLowerCase();return t.includes(`expired`)||t.includes(`token`)?{type:`access-lost`,code:`session-expired`,recovery:`reauthenticate`,clearSnapshot:!0}:t.includes(`permission`)||t.includes(`forbidden`)||t.includes(`unauthorized`)?{type:`access-lost`,code:`permission-denied`,recovery:`reauthenticate`,clearSnapshot:!0}:{type:`error`,code:`transport`,recovery:`retry`,clearSnapshot:!0}}function ze(e){return e instanceof Error?e.message:e&&typeof e==`object`&&`message`in e?String(e.message):``}function k(e,t){if(!e||typeof e!=`object`||Array.isArray(e))throw new h(`${t} is missing or invalid`);return e}function Be(e,t){if(typeof e!=`string`)throw new h(`${t} is invalid`);return e}function A(e,t){let n=Be(e,t);if(!n.trim())throw new h(`${t} is empty`);return n}function j(e,t){if(typeof e!=`number`||!Number.isFinite(e)||e<0||e>1)throw new h(`${t} is invalid`);return e}function M(e,t){if(!Number.isSafeInteger(e)||e<0||e>0xe677d21fdbff)throw new h(`${t} is invalid`);return e}function Ve(e,t){if(!Number.isSafeInteger(e)||e<0)throw new h(`${t} is invalid`);return e}function He(e,t){return e==null?void 0:M(e,t)}function Ue(e,t){return e==null?void 0:A(e,t)}function We(e,t){if(e==null)return{};let n=Ve(e,t);if(n<1)throw new h(`${t} is invalid`);return{pageNumber:n}}Object.freeze({field:`createdAt`,direction:`desc`,tieBreaker:`id`,tieDirection:`desc`}),Object.freeze({comments:200,replies:1e3,sourceContexts:200});var Ge=class{db;session;roomCredential;#e=new Map;constructor(e,t,n){this.db=e,this.session=t,this.roomCredential=n}subscribe(e,t,n,r){let i=[],a=[],o=!1,s=[],c=()=>{o||n({pageId:e,peers:structuredClone(i),messages:structuredClone(a)})};if(t.cursors){let n=tt(e,t.roomId),a,l,u=this.roomCredential?.id===t.roomId?this.roomCredential:void 0,d=u??{id:t.roomId,expiresAt:2**53-1,refresh:async()=>void 0},f=t=>{let o=this.db.joinRoom(`visualReview`,t),s=o.subscribePresence({},e=>{if(e.error){r?.(I(e.error));return}i=Object.values(e.peers).map(e=>qe(e)).filter(e=>e!==null).sort((e,t)=>e.displayName.localeCompare(t.displayName)||e.peerId.localeCompare(t.peerId)),c()}),l=a;a=()=>{s(),o.leaveRoom()},this.#e.set(n,{pageId:e,roomId:t,publishPresence:e=>o.publishPresence(e)}),l?.()},p=()=>{if(!u||o)return;let e=Math.max(1e3,d.expiresAt-Date.now()+250);l=setTimeout(()=>{m()},e)},m=async()=>{try{let e=await d.refresh();if(o||!e)return;d={...e,refresh:d.refresh},u&&(u.id=e.id,u.expiresAt=e.expiresAt),f(e.id),p()}catch(e){if(o)return;r?.(I(e)),o||(l=setTimeout(()=>{m()},5e3))}};f(d.id),p(),s.push(()=>{l&&clearTimeout(l),this.#e.delete(n),a?.()})}return t.chat&&s.push(this.db.subscribeQuery(Ke(e,this.session.authUserId),t=>{if(t.error){r?.(I(t.error));return}if(t.data)try{a=t.data.reviewMessages.map(t=>Xe(t,e)).sort(nt),c()}catch{r?.({type:`error`,code:`data-integrity`,recovery:`retry`,clearSnapshot:!0})}})),queueMicrotask(c),()=>{if(!o){o=!0;for(let e of s.reverse())try{e()}catch{}}}}publishPresence(e,t,n){let r=this.#e.get(tt(e,t));r&&r.publishPresence({reviewerId:this.session.reviewerId,displayName:this.session.displayName,color:n.color,active:n.active,pathname:Ye(n.pathname),...n.cursorX===void 0?{}:{cursorX:n.cursorX},...n.cursorY===void 0?{}:{cursorY:n.cursorY},...n.viewportWidth===void 0?{}:{viewportWidth:n.viewportWidth},...n.viewportHeight===void 0?{}:{viewportHeight:n.viewportHeight},cursorChat:Je(n.cursorChat)})}async sendMessage(e,t){if(t.pageId!==e)throw Error(`Review message page mismatch`);if(t.authorId!==this.session.reviewerId)throw Error(`Review message author mismatch`);let n=Ze(t.body),r=Qe(t.pathname),i=F(t.createdAt,`Review message createdAt`),a=F(t.updatedAt,`Review message updatedAt`);if(i!==a)throw Error(`New review messages must be immutable`);await this.db.transact(this.db.tx.reviewMessages[t.id].update({pageId:e,pathname:r,authorId:this.session.reviewerId,authorAuthUserId:this.session.authUserId,authorName:this.session.displayName,body:n,bodyContentMarker:ae(n,`Review message body`),createdAt:i,updatedAt:a}).link({page:e,author:this.session.reviewerId}))}};function Ke(e,t){return{reviewMessages:{$:{where:{pageId:e},order:{createdAt:`asc`}}},$users:{$:{where:{id:t}},activeReviewInvitation:{project:{}}}}}function qe(e){let t=$e(e.reviewerId,128),n=$e(e.displayName,512),r=typeof e.color==`string`&&/^#[0-9a-f]{6}$/iu.test(e.color)?e.color:null;if(!t||!n||!r||typeof e.active!=`boolean`)return null;let i=P(e.cursorX),a=P(e.cursorY),o=et(e.viewportWidth),s=et(e.viewportHeight),c=Je(e.cursorChat),l=Ye(e.pathname);return{peerId:e.peerId,reviewerId:t,displayName:n,color:r,active:e.active,pathname:l,...i===void 0?{}:{cursorX:i},...a===void 0?{}:{cursorY:a},...o===void 0?{}:{viewportWidth:o},...s===void 0?{}:{viewportHeight:s},...c?{cursorChat:c}:{}}}function Je(e){return typeof e==`string`?e.trim().slice(0,280):``}function Ye(e){if(typeof e!=`string`)return`/`;let t=e.trim();return t.startsWith(`/`)?t.slice(0,2e3):`/`}function Xe(e,t){let n=N(e.id,`Review message id`,128);if(N(e.pageId,`Review message pageId`,128)!==t)throw Error(`Review message page mismatch`);return{id:n,pageId:t,pathname:Qe(e.pathname),authorId:N(e.authorId,`Review message authorId`,128),authorName:N(e.authorName,`Review message authorName`,512),body:Ze(e.body),createdAt:F(e.createdAt,`Review message createdAt`),updatedAt:F(e.updatedAt,`Review message updatedAt`)}}function Ze(e){return N(e,`Review message body`,4e3)}function Qe(e){let t=N(e,`Review message pathname`,2e3);if(!t.startsWith(`/`))throw Error(`Review message pathname is invalid`);return t}function N(e,t,n){if(typeof e!=`string`)throw Error(`${t} is invalid`);let r=e.trim();if(!r||r.length>n)throw Error(`${t} is invalid`);return r}function $e(e,t){if(typeof e!=`string`)return null;let n=e.trim();return n&&n.length<=t?n:null}function P(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function et(e){let t=P(e);return t!==void 0&&t>0?t:void 0}function F(e,t){if(!Number.isSafeInteger(e)||e<0)throw Error(`${t} is invalid`);return e}function tt(e,t){return JSON.stringify([e,t])}function nt(e,t){return e.createdAt-t.createdAt||e.id.localeCompare(t.id)}function I(e){let t=e&&typeof e==`object`&&`code`in e?String(e.code):``;if(/^INVITATION_(?:EXPIRED|NOT_FOUND|REQUIRED|REVOKED)$/u.test(t))return{type:`access-lost`,code:t===`INVITATION_EXPIRED`?`session-expired`:`membership-revoked`,recovery:`reauthenticate`,clearSnapshot:!0};let n=String(e instanceof Error||e&&typeof e==`object`&&`message`in e?e.message:e).toLowerCase();return n.includes(`expired`)||n.includes(`token`)?{type:`access-lost`,code:`session-expired`,recovery:`reauthenticate`,clearSnapshot:!0}:n.includes(`permission`)||n.includes(`forbidden`)||n.includes(`unauthorized`)?{type:`access-lost`,code:`permission-denied`,recovery:`reauthenticate`,clearSnapshot:!0}:{type:`error`,code:`transport`,recovery:`retry`,clearSnapshot:!0}}Object.freeze({field:`createdAt`,direction:`desc`,tieBreaker:`id`,tieDirection:`desc`});const L={$default:`false`},R={$default:`false`},z=`$user.activeReviewInvitation.project.id`;function B(e){return[`auth.id != null`,`size(auth.ref('$user.activeReviewInvitation.id')) == 1`,`size(auth.ref('${z}')) == 1`,`size(auth.ref('$user.activeReviewInvitation.status')) == 1`,`size(auth.ref('$user.activeReviewInvitation.expiresAt')) == 1`,`size(auth.ref('$user.activeReviewInvitation.revokedAt')) == 0`,`((request.origin in auth.ref('$user.activeReviewInvitation.project.origins.origin')) || (size(auth.ref('$user.activeReviewInvitation.project.originModelVersion')) == 0 && request.origin in auth.ref('$user.activeReviewInvitation.project.allowedOrigin')))`,`auth.ref('$user.activeReviewInvitation.status')[0] == 'accepted'`,`request.time < timestamp(auth.ref('$user.activeReviewInvitation.expiresAt')[0])`,e].join(` && `)}function V(...e){return e.map(e=>`(${e})`).join(` && `)}function H(...e){return e.map(e=>`(${e})`).join(` || `)}function U(e,t){return`!has(${e}.${t}) || ${e}.${t} == null`}const rt=`actions.data == 'create' && actions.linkedData == 'update'`,it=e=>`${e}.matches('[^ \\t\\n\\r\\f\\u000b\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]')`,at=e=>V(`${e} >= 0`,`${e} <= 253402300799999`,`${e} % 1 == 0`),ot=e=>V(at(e),`(request.time - timestamp(${e})).getSeconds() <= 86400`,`(request.time - timestamp(${e})).getSeconds() >= -300`);function W(e){let t=`data.ref('${e}project.origins.origin')`,n=`data.ref('${e}project.allowedOrigin')`,r=`data.ref('${e}project.originModelVersion')`;return H(`request.origin in ${t}`,V(`size(${r}) == 0`,`request.origin in ${n}`))}const st=B(`auth.ref('${z}')[0] in data.ref('project.id')`);H(`request.origin in data.ref('project.origins.origin')`,V(`size(data.ref('project.originModelVersion')) == 0`,`request.origin in data.ref('project.allowedOrigin')`)),V(`actions.data == 'update' && actions.linkedData == 'create'`,H(`auth.id != null && auth.id in data.ref('project.owners.id')`,st));const ct=B(`auth.ref('${z}')[0] in data.ref('page.project.id')`);W(`page.`),V(rt,H(`auth.id != null && auth.id in data.ref('page.project.owners.id')`,ct),`auth.id != null && auth.id == data.authorAuthUserId && auth.id in data.ref('author.$user.id')`,`data.pageId in data.ref('page.id')`,`data.authorId in data.ref('author.id') && data.authorAuthUserId in data.ref('author.$user.id') && data.authorName in data.ref('author.displayName')`,`data.pageId in data.ref('author.project.pages.id')`),V(rt,`data.pageId == linkedData.id`,`auth.id != null && auth.id == data.authorAuthUserId`,`data.authorId in auth.ref('$user.reviewerMemberships.id')`,`data.authorId in linkedData.ref('project.reviewers.id')`,H(`auth.id != null && auth.id in linkedData.ref('project.owners.id')`,B(`auth.ref('${z}')[0] in linkedData.ref('project.id')`))),H(`auth.id != null && auth.id in data.ref('comment.page.project.owners.id')`,V(B(`auth.ref('${z}')[0] in data.ref('comment.page.project.id')`),W(`comment.page.`))),B(`auth.ref('${z}')[0] in data.ref('comment.page.project.id')`),W(`comment.page.`),B(`auth.ref('${z}')[0] in data.ref('page.project.id')`),W(`page.`);const lt=V(`data.reviewerId != null`,`data.projectId in data.ref('project.id')`,`data.reviewerId in data.ref('reviewer.id')`,`data.projectId in data.ref('reviewer.project.id')`,`auth.id in data.ref('reviewer.$user.id')`,`auth.email in data.ref('reviewer.email')`);V(`auth.id != null`,`data.revokedAt == null`,`data.status in ['pending', 'accepted']`,`data.status == 'pending' || data.claimedByAuthUserId == auth.id`,`request.time < timestamp(data.expiresAt)`,`request.modifiedFields.all(field, field in ['status', 'claimedByAuthUserId', 'acceptedAt', 'claimRevision', 'activeUser'])`,`'status' in request.modifiedFields`,`'claimedByAuthUserId' in request.modifiedFields`,`'acceptedAt' in request.modifiedFields`,`'claimRevision' in request.modifiedFields`,`'activeUser' in request.modifiedFields`,`newData.status == 'accepted'`,`newData.claimedByAuthUserId == auth.id`,`newData.claimRevision == data.claimRevision + 1`,`math.abs((request.time - timestamp(newData.acceptedAt)).getMinutes()) <= 1`,lt),V(`actions.data == 'update'`,`actions.linkedData == 'update'`,`auth.id != null`,`auth.email != null`,`linkedData.id == auth.id`,`linkedData.email == auth.email`,`data.email == auth.email`,`data.revokedAt == null`,`data.status in ['pending', 'accepted']`,`data.status == 'pending' || data.claimedByAuthUserId == auth.id`,`request.time < timestamp(data.expiresAt)`,`newData.status == 'accepted'`,`newData.claimedByAuthUserId == auth.id`,`newData.claimRevision == data.claimRevision + 1`,`math.abs((request.time - timestamp(newData.acceptedAt)).getMinutes()) <= 1`,lt),V(`actions.data == 'update'`,`actions.linkedData == 'update'`,`auth.id != null`,`auth.email != null`,`data.id == auth.id`,`data.email == auth.email`,`linkedData.email == auth.email`,`linkedData.revokedAt == null`,`linkedData.status == 'accepted'`,`linkedData.claimedByAuthUserId == auth.id`,`request.time < timestamp(linkedData.expiresAt)`,`math.abs((request.time - timestamp(linkedData.acceptedAt)).getMinutes()) <= 1`,`linkedData.projectId in linkedData.ref('project.id')`,`linkedData.reviewerId in linkedData.ref('reviewer.id')`,`linkedData.projectId in linkedData.ref('reviewer.project.id')`,`auth.id in linkedData.ref('reviewer.$user.id')`,`auth.email in linkedData.ref('reviewer.email')`),V(`auth.id != null`,`auth.email != null`,`linkedData.id == auth.id`,`linkedData.email == auth.email`,`data.email == auth.email`,`data.claimedByAuthUserId == auth.id`,`auth.id in data.ref('activeUser.id')`),V(`auth.id != null`,`auth.email != null`,`data.id == auth.id`,`data.email == auth.email`,`linkedData.email == auth.email`,`linkedData.claimedByAuthUserId == auth.id`,`auth.id in linkedData.ref('activeUser.id')`),{...L},{...L},{...R},{...R},B(`data.id == auth.ref('${z}')[0]`),{...L},V(`isOwner`,H(V(U(`data`,`allowedOrigin`),U(`newData`,`allowedOrigin`)),`newData.allowedOrigin == data.allowedOrigin`),H(V(U(`data`,`originDisconnectedAt`),U(`newData`,`originDisconnectedAt`)),`newData.originDisconnectedAt == data.originDisconnectedAt`),H(V(U(`data`,`originModelVersion`),U(`newData`,`originModelVersion`)),`newData.originModelVersion == data.originModelVersion`)),{...R},{...L},{...L},V(`isOwner`,H(V(U(`data`,`autoRegistrationSlot`),U(`newData`,`autoRegistrationSlot`)),`newData.autoRegistrationSlot == data.autoRegistrationSlot`)),{...R},{...L},{...R},B(`auth.ref('${z}')[0] in data.ref('project.id')`),{...L},{...R},{...L},{...L},{...R},{...R},{...L},{...R},{...L},{...L},{...L},{...L},{...L},{...L},V(`data.createdAt == data.updatedAt`,ot(`data.createdAt`),`size(data.body) > 0`,`size(data.body) <= 4000`,`size(data.bodyContentMarker) == 1`,it(`data.bodyContentMarker`),`data.body.startsWith(data.bodyContentMarker)`,`size(data.authorName) > 0`,`size(data.authorName) <= 512`,it(`data.authorName`),`size(data.pathname) > 0`,`size(data.pathname) <= 2000`,`request.modifiedFields.all(field, field in ['pageId', 'pathname', 'authorId', 'authorAuthUserId', 'authorName', 'body', 'bodyContentMarker', 'createdAt', 'updatedAt', 'page', 'author'])`),{...L},{...R};var G=class extends Error{code;constructor(e,t){super(t),this.code=e,this.name=`ReviewerCommentSaveError`}};function ut(e,t){let n=e.replace(/\/+$/,``),r=async(e,r)=>{let i=await t();if(!i)throw new G(`AUTH_REQUIRED`,`로그인 세션이 만료되었습니다. 새로고침 후 다시 시도해 주세요.`);let a;try{a=await fetch(`${n}${e}`,{method:`POST`,headers:{authorization:`Bearer ${i}`,"content-type":`application/json`},body:JSON.stringify(r)})}catch{throw new G(`NETWORK`,`리뷰 서버에 연결하지 못했습니다. 연결을 확인하고 다시 시도해 주세요.`)}if(!a.ok)throw new G(...await ft(a))};return{createComment:e=>r(`/v1/comments`,e),createReply:e=>r(`/v1/replies`,e),uploadScreenshot:async({pageId:e,commentId:r,screenshot:i})=>{let a=await t();if(!a)throw new G(`AUTH_REQUIRED`,`로그인 세션이 만료되어 위치 캡처를 저장하지 못했습니다.`);let o;try{o=await fetch(`${n}/v1/comments/${encodeURIComponent(r)}/screenshot`,{method:`PUT`,headers:{authorization:`Bearer ${a}`,"content-type":i.contentType,"x-review-page-id":e,"x-review-image-width":String(i.width),"x-review-image-height":String(i.height)},body:dt(i.bytes)})}catch{throw new G(`NETWORK`,`리뷰 서버에 연결하지 못해 위치 캡처를 저장하지 못했습니다.`)}if(!o.ok)throw new G(...await ft(o))}}}function dt(e){let t=new Uint8Array(e.byteLength);return t.set(e),t.buffer}async function ft(e){let t=e.status>=500?`리뷰 서버가 코멘트를 저장하지 못했습니다. 잠시 후 다시 시도해 주세요.`:`코멘트를 저장하지 못했습니다.`;try{let n=await e.json();if(!n||typeof n!=`object`)return[`UNKNOWN`,t];let r=n;return[typeof r.error==`string`?r.error:`UNKNOWN`,typeof r.message==`string`&&r.message.trim()?r.message:t]}catch{return[`UNKNOWN`,t]}}var K=class extends Error{code;constructor(e,t){super(t),this.code=e,this.name=`ReviewBootstrapError`}};const q=new Map;function pt(e,t={}){let n=e.trim();if(!n)throw Error(`InstantDB app ID is required`);return{async resolve(e){let r=(t.databaseFactory??Ee)(n),i=await _t(n,r,e.authHost,e.signal,e.styleNonce,t.reviewerToken);for(;;)try{await mt(t.acceptInvitation,{refreshToken:i.refresh_token,invitationId:e.invitationId,suggestedDisplayName:At(i.email),authHost:e.authHost,signal:e.signal,styleNonce:e.styleNonce});break}catch(a){if(!jt(a,`INVITATION_EMAIL_MISMATCH`))throw a;await Ct(e.authHost,e.signal,e.styleNonce),i=await vt(n,r,i,e.authHost,e.signal,e.styleNonce,t.reviewerToken)}let{data:a}=await r.queryOnce({reviewers:{$:{where:{authUserId:i.id}}},reviewPages:{},invitations:{}}),o=ht(a.invitations,a.reviewers,a.reviewPages,{invitationId:e.invitationId,authUserId:i.id,email:i.email??``,location:e.location,now:Date.now()});if(!o.ok)throw new K(o.code,It(o.code));let s={authUserId:i.id,reviewerId:o.reviewer.id,projectId:o.reviewer.projectId,invitationId:e.invitationId,email:o.reviewer.email||i.email||``,displayName:o.reviewer.displayName||i.email||`Reviewer`,role:o.reviewer.role===`agency`?`agency`:`reviewer`},c=o.page.id,l=e.collaboration?.scopeId,u=(e.collaboration?.cursors===!0||e.collaboration?.chat===!0)&&t.resolveCollaborationRoom?await t.resolveCollaborationRoom({refreshToken:i.refresh_token,invitationId:e.invitationId,pageId:o.page.id,scopeId:e.collaboration.scopeId,signal:e.signal}):void 0,d=u&&t.resolveCollaborationRoom?{...u,refresh:async()=>{let n=await r.getAuth();if(!(!n?.refresh_token||l===void 0))return t.resolveCollaborationRoom?.({refreshToken:n.refresh_token,invitationId:e.invitationId,pageId:c,scopeId:l,signal:new AbortController().signal})}}:void 0,f=0;return{pageId:o.page.id,resolvePage:async n=>{let i=++f,a=()=>i!==f||n.signal.aborted,o=(e,t,n)=>({pageId:e,...n&&d?{collaborationRoomId:n.id}:{},commit:()=>{a()||(c=e,t!==void 0&&(l=t),!(!n||!d)&&(d.id=n.id,d.expiresAt=n.expiresAt))}});if(n.location.origin!==e.location.origin)throw new K(`PAGE_NOT_FOUND`,It(`PAGE_NOT_FOUND`));let{data:u}=await r.queryOnce({reviewPages:{$:{where:{projectId:s.projectId}}}});if(a())throw Mt();let p=Pt(n.location.pathname),m=gt(u.reviewPages,s.projectId,n.location.origin,p);if(!m)throw new K(`PAGE_NOT_FOUND`,It(`PAGE_NOT_FOUND`));if(n.collaboration?.cursors!==!0&&n.collaboration?.chat!==!0||d===void 0||t.resolveCollaborationRoom===void 0||!n.collaboration)return o(m.id,n.collaboration?.scopeId);let h=await r.getAuth();if(a())throw Mt();if(!h?.refresh_token)throw Error(`리뷰 세션이 만료되어 실시간 협업 연결을 갱신할 수 없습니다.`);let g=await t.resolveCollaborationRoom({refreshToken:h.refresh_token,invitationId:e.invitationId,pageId:m.id,scopeId:n.collaboration.scopeId,signal:n.signal});if(a())throw Mt();return o(m.id,n.collaboration.scopeId,g)},session:s,repository:new De(r,s,{...t.serviceUrl===void 0?{}:{commentTransport:ut(t.serviceUrl,async()=>(await r.getAuth())?.refresh_token??null)}}),...e.collaboration?.cursors===!0||e.collaboration?.chat===!0?{collaborationRepository:new Ge(r,s,d)}:{},...(e.collaboration?.cursors===!0||e.collaboration?.chat===!0)&&d?{collaborationRoomId:d.id}:{},allowedStagingHosts:[e.location.hostname]}}}}async function mt(e,t){if(!e)return;let n={refreshToken:t.refreshToken,invitationId:t.invitationId,signal:t.signal};try{await e(n);return}catch(e){if(!jt(e,`DISPLAY_NAME_REQUIRED`))throw e}let r=await St(t.authHost,t.signal,t.suggestedDisplayName,t.styleNonce);await e({...n,displayName:r})}function ht(e,t,n,r){let i=e.find(({id:e})=>e===r.invitationId);if(!i||Ft(i.email)!==Ft(r.email))return{ok:!1,code:`INVITATION_REQUIRED`};if(i.status===`revoked`)return{ok:!1,code:`INVITATION_REVOKED`};if(i.status===`expired`||i.expiresAt<=r.now)return{ok:!1,code:`INVITATION_EXPIRED`};if(i.status!==`accepted`)return{ok:!1,code:`INVITATION_NOT_ACCEPTED`};let a=t.find(e=>e.authUserId===r.authUserId&&e.projectId===i.projectId);if(!a)return{ok:!1,code:`MEMBERSHIP_REQUIRED`};let o=Pt(r.location.pathname),s=gt(n,i.projectId,r.location.origin,o);return s?{ok:!0,invitation:i,reviewer:a,page:s}:{ok:!1,code:`PAGE_NOT_FOUND`}}function gt(e,t,n,r){let i=e.filter(e=>e.projectId===t&&Pt(e.pathname)===r),a=i.filter(({stagingOrigin:e})=>e===n);return(a.length>0?a:i).sort((e,t)=>(e.createdAt??2**53-1)-(t.createdAt??2**53-1)||e.id.localeCompare(t.id))[0]}async function _t(e,t,n,r,i,a){let o=await J(r,()=>t.getAuth());if(o)return o;let s=await yt(e,r);try{return await J(r,()=>t.getAuth())||(a?await J(r,async()=>{await t.auth.signInWithToken(a);let e=await t.getAuth();if(!e)throw Error(`Reviewer session did not establish`);return e}):await xt(t,n,r,i))}finally{s()}}async function vt(e,t,n,r,i,a,o){Y(i);let s=await yt(e,i);try{let e=await J(i,()=>t.getAuth());return e&&e.id!==n.id?e:(e&&await J(i,async()=>{await t.auth.signOut()}),Y(i),o?await J(i,async()=>{await t.auth.signInWithToken(o);let e=await t.getAuth();if(!e)throw Error(`Reviewer session did not establish`);return e}):await xt(t,r,i,a))}finally{s()}}async function yt(e,t){Y(t);let n=q.get(e);return n||(n={held:!1,waiters:[]},q.set(e,n)),n.held?new Promise((e,r)=>{let i,a=()=>{let e=n.waiters.indexOf(i);e<0||(n.waiters.splice(e,1),t.removeEventListener(`abort`,a),r(Z()))};i={signal:t,resolve:e,reject:r,abort:a},n.waiters.push(i),t.addEventListener(`abort`,a,{once:!0}),t.aborted&&a()}):(n.held=!0,bt(e,n))}function bt(e,t){let n=!1;return()=>{if(!n){for(n=!0;t.waiters.length>0;){let n=t.waiters.shift();if(n.signal.removeEventListener(`abort`,n.abort),n.signal.aborted){n.reject(Z());continue}n.resolve(bt(e,t));return}t.held=!1,q.get(e)===t&&q.delete(e)}}}async function J(e,t){Y(e);let n=await t();return Y(e),n}function Y(e){if(e.aborted)throw Z()}function xt(e,t,n,r){try{Y(n)}catch(e){return Promise.reject(e)}let i=Tt(t);i.replaceChildren(Et(i,r),Dt(i));let a=i.querySelector(`form`),o=i.querySelector(`h2`),s=i.querySelector(`.description`),c=a.elements.namedItem(`email`),l=a.elements.namedItem(`code`),u=i.querySelector(`.code`),d=i.querySelector(`button`),f=i.querySelector(`.error`),p=!1;return new Promise((t,r)=>{let m=!1,h=!1,g=!1,ee=()=>{n.removeEventListener(`abort`,v),i.removeEventListener(`keydown`,te),a.removeEventListener(`submit`,ne)},_=e=>{m||(m=!0,ee(),e())},v=()=>{m||(g=!0,ee(),h||_(()=>r(Z())))},te=e=>{let t=e;t.key===`Escape`?(t.preventDefault(),v()):t.key===`Tab`&&Nt(i,t)},ne=async i=>{if(i.preventDefault(),!(m||h)){h=!0,f.textContent=``,d.disabled=!0,a.setAttribute(`aria-busy`,`true`);try{if(Y(n),!p){let t=c.value.trim();await J(n,async()=>{await e.auth.sendMagicCode({email:t})}),p=!0,c.readOnly=!0,u.hidden=!1,l.required=!0,o.textContent=`인증 코드`,s.textContent=`${t}로 보낸 코드를 입력하세요.`,d.textContent=`확인`,l.focus();return}await J(n,async()=>{await e.auth.signInWithMagicCode({email:c.value.trim(),code:l.value.trim()})});let r=await J(n,()=>e.getAuth());if(!r)throw Error(`인증 세션을 확인할 수 없습니다.`);_(()=>t(r))}catch(e){if(m||n.aborted||jt(e,`AUTH_CANCELLED`))return;f.textContent=wt(e)}finally{h=!1,!m&&(g||n.aborted)?_(()=>r(Z())):m||(d.disabled=!1,a.removeAttribute(`aria-busy`))}}};n.addEventListener(`abort`,v,{once:!0}),i.addEventListener(`keydown`,te),a.addEventListener(`submit`,ne)})}function St(e,t,n=``,r){if(t.aborted)return Promise.reject(Z());let i=Tt(e);i.replaceChildren(Et(i,r),Ot(i));let a=i.querySelector(`form`),o=a.elements.namedItem(`displayName`),s=i.querySelector(`button`),c=i.querySelector(`.error`);return o.value=n.trim().slice(0,120),o.focus(),o.select(),new Promise((e,n)=>{let r=!1,l=e=>{r||(r=!0,t.removeEventListener(`abort`,u),i.removeEventListener(`keydown`,p),a.removeEventListener(`submit`,f),e())},u=()=>l(()=>n(Z())),d=()=>{let t=o.value.trim();if(!t){c.textContent=`이름을 입력해 주세요.`,o.focus();return}if(t.length>120){c.textContent=`이름은 120자 이하여야 합니다.`,o.focus();return}c.textContent=``,s.disabled=!0,a.setAttribute(`aria-busy`,`true`),l(()=>e(t))},f=e=>{e.preventDefault(),d()},p=e=>{let t=e;if(t.key===`Escape`){t.preventDefault(),u();return}if(t.key===`Tab`){Nt(i,t);return}t.key===`Enter`&&!t.isComposing&&t.target===o&&(t.preventDefault(),d())};t.addEventListener(`abort`,u,{once:!0}),i.addEventListener(`keydown`,p),a.addEventListener(`submit`,f)})}function Ct(e,t,n){if(t.aborted)return Promise.reject(Z());let r=Tt(e);r.replaceChildren(Et(r,n),kt(r));let i=r.querySelector(`form`),a=r.querySelector(`button`);return a.focus(),new Promise((e,n)=>{let o=!1,s=e=>{o||(o=!0,t.removeEventListener(`abort`,c),r.removeEventListener(`keydown`,u),i.removeEventListener(`submit`,l),e())},c=()=>s(()=>n(Z())),l=t=>{t.preventDefault(),a.disabled=!0,i.setAttribute(`aria-busy`,`true`),s(e)},u=e=>{let t=e;t.key===`Escape`?(t.preventDefault(),c()):t.key===`Tab`&&Nt(r,t)};t.addEventListener(`abort`,c,{once:!0}),r.addEventListener(`keydown`,u),i.addEventListener(`submit`,l)})}function wt(e){if(e&&typeof e==`object`&&`body`in e){let t=e.body;if(t?.message)return t.message}return e instanceof Error?e.message:`로그인에 실패했습니다. 다시 시도해 주세요.`}function Tt(e){return e.shadowRoot??e.attachShadow({mode:`open`})}function Et(e,t){let n=e.ownerDocument.createElement(`style`);return t&&(n.nonce=t),n.textContent=Lt,n}function Dt(e){let t=e.ownerDocument,n=t.createElement(`form`);n.setAttribute(`role`,`dialog`),n.setAttribute(`aria-modal`,`true`),n.setAttribute(`aria-labelledby`,`visual-review-login-title`),n.setAttribute(`aria-describedby`,`visual-review-login-description`);let r=X(t,`p`,`01.works`,`eyebrow`),i=X(t,`h2`,`Visual Review`);i.id=`visual-review-login-title`;let a=X(t,`p`,`초대받은 이메일로 계속하세요.`,`description`);a.id=`visual-review-login-description`;let o=t.createElement(`label`);o.append(`이메일`);let s=t.createElement(`input`);s.name=`email`,s.type=`email`,s.autocomplete=`email`,s.required=!0,o.append(s);let c=t.createElement(`label`);c.className=`code`,c.hidden=!0,c.append(`인증 코드`);let l=t.createElement(`input`);l.name=`code`,l.inputMode=`numeric`,l.autocomplete=`one-time-code`,c.append(l);let u=X(t,`p`,``,`error`);u.setAttribute(`role`,`alert`);let d=X(t,`button`,`코드 받기`);return d.type=`submit`,n.append(r,i,a,o,c,u,d),n}function Ot(e){let t=e.ownerDocument,n=t.createElement(`form`);n.setAttribute(`role`,`dialog`),n.setAttribute(`aria-modal`,`true`),n.setAttribute(`aria-labelledby`,`visual-review-name-title`),n.setAttribute(`aria-describedby`,`visual-review-name-description`);let r=X(t,`p`,`01.works`,`eyebrow`),i=X(t,`h2`,`표시 이름`);i.id=`visual-review-name-title`;let a=X(t,`p`,`피드백에 이 이름이 표시됩니다.`,`description`);a.id=`visual-review-name-description`;let o=X(t,`label`,`이름`);o.htmlFor=`visual-review-display-name`;let s=t.createElement(`input`);s.id=`visual-review-display-name`,s.name=`displayName`,s.autocomplete=`name`,s.maxLength=120,s.required=!0;let c=X(t,`p`,``,`error`);c.setAttribute(`role`,`alert`);let l=X(t,`button`,`리뷰 시작`);return l.type=`submit`,n.append(r,i,a,o,s,c,l),n}function kt(e){let t=e.ownerDocument,n=t.createElement(`form`);n.setAttribute(`role`,`dialog`),n.setAttribute(`aria-modal`,`true`),n.setAttribute(`aria-labelledby`,`visual-review-account-title`),n.setAttribute(`aria-describedby`,`visual-review-account-description`);let r=X(t,`p`,`01.works`,`eyebrow`),i=X(t,`h2`,`계정 확인`);i.id=`visual-review-account-title`;let a=X(t,`p`,`이 초대는 현재 로그인한 계정과 일치하지 않습니다.`,`description`);a.id=`visual-review-account-description`;let o=X(t,`button`,`다른 계정으로 계속`);return o.type=`submit`,n.append(r,i,a,o),n}function X(e,t,n,r){let i=e.createElement(t);return i.textContent=n,r&&(i.className=r),i}function At(e){return e?.split(`@`)[0]?.trim().slice(0,120)??``}function jt(e,t){return!!(e&&typeof e==`object`&&`code`in e&&e.code===t)}function Mt(){return new DOMException(`Aborted`,`AbortError`)}function Z(){return new K(`AUTH_CANCELLED`,`리뷰 로그인이 취소되었습니다.`)}function Nt(e,t){let n=Array.from(e.querySelectorAll(`input:not([disabled]),button:not([disabled])`)).filter(e=>!e.closest(`[hidden]`));if(n.length===0)return;let r=n[0],i=n.at(-1);t.shiftKey&&e.activeElement===r?(t.preventDefault(),i.focus()):!t.shiftKey&&e.activeElement===i&&(t.preventDefault(),r.focus())}function Pt(e){return e.replace(/\/+$/,``)||`/`}function Ft(e){return e.trim().toLowerCase()}function It(e){switch(e){case`INVITATION_REQUIRED`:return`현재 계정과 일치하는 리뷰 초대를 찾을 수 없습니다.`;case`INVITATION_NOT_ACCEPTED`:return`아직 수락되지 않은 리뷰 초대입니다.`;case`INVITATION_REVOKED`:return`취소된 리뷰 초대입니다. 계속 검토하려면 담당자에게 다시 요청하세요.`;case`INVITATION_EXPIRED`:return`만료된 리뷰 초대입니다. 담당자에게 새 링크를 요청하세요.`;case`MEMBERSHIP_REQUIRED`:return`이 프로젝트의 reviewer membership이 없습니다.`;case`PAGE_NOT_FOUND`:return`이 페이지는 아직 리뷰 대상이 아닙니다. 담당자에게 문의하세요.`}}const Lt=`
2
+ :host{all:initial;--vr-panel:#fff;--vr-text:#161616;--vr-muted:#707070;--vr-label:#3f3f3f;--vr-control:#fff;--vr-border:#d9d9d9;--vr-action:#161616;--vr-action-text:#fff;position:fixed;inset:0;z-index:2147483646;display:grid;place-items:center;overflow:auto;padding:8px;box-sizing:border-box;background:rgba(15,15,15,.34);font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:var(--vr-text)}
3
+ form{box-sizing:border-box;width:min(320px,calc(100vw - 16px));padding:20px;border:0;border-radius:14px;background:var(--vr-panel);box-shadow:0 18px 56px rgba(0,0,0,.18)}
4
+ .eyebrow{margin:0 0 10px;color:var(--vr-muted);font-size:10px;font-weight:700;letter-spacing:.09em;text-transform:uppercase}
5
+ h2{margin:0 0 5px;font-size:16px;line-height:1.3;letter-spacing:-.01em}.description{margin:0 0 18px;color:var(--vr-muted);font-size:13px;line-height:1.45}
6
+ label{display:grid;gap:6px;margin-top:12px;color:var(--vr-label);font-size:12px;font-weight:600}input{box-sizing:border-box;width:100%;height:40px;margin-top:6px;padding:0 11px;border:1px solid var(--vr-border);border-radius:9px;background:var(--vr-control);color:var(--vr-text);font:400 13px/1 Inter,ui-sans-serif,system-ui,sans-serif}
7
+ label input{margin-top:0}input:focus{border-color:#999;outline:0;box-shadow:0 0 0 3px rgba(127,127,127,.16)}.error{min-height:17px;margin:8px 0 0;color:#d92d20;font-size:11px;line-height:1.45}
8
+ button{width:100%;height:40px;margin-top:7px;border:0;border-radius:9px;background:var(--vr-action);color:var(--vr-action-text);font:650 13px/1 Inter,ui-sans-serif,system-ui,sans-serif;cursor:pointer}button:focus-visible{outline:2px solid var(--vr-action);outline-offset:2px}button:disabled{opacity:.55;cursor:wait}
9
+ @media (prefers-color-scheme:dark){:host{--vr-panel:#161616;--vr-text:#fff;--vr-muted:#a7a7a7;--vr-label:#d6d6d6;--vr-control:#222;--vr-border:#3a3a3a;--vr-action:#fff;--vr-action-text:#171717}}
10
+ @media (pointer:coarse){input,button{height:44px}}
11
+ @media (max-width:480px){form{padding:18px}input{font-size:16px}}
12
+ @media (prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
13
+ `;var Q=class extends Error{code;status;constructor(e,t,n){super(t),this.code=e,this.status=n,this.name=`HostedBootstrapError`}};function Rt(e={}){return zt(e)}function zt(t={},n={}){let r=e(t.serviceUrl??`https://visual-review.01.works`),i=t.fetch??globalThis.fetch,a=n.instantRuntimeFactory??pt,o=n.now??Date.now;if(typeof i!=`function`)throw Error(`Visual Review bootstrap requires the Fetch API`);return{async resolve(e){let t=Vt(e),n=await Ut(r,i,t,e.signal),s=o(),c=await a(n.instantAppId,{serviceUrl:r,...n.reviewer?{reviewerToken:n.reviewer.token}:{},acceptInvitation:async e=>{let t=await i(`${r}/v1/invitations/accept`,{method:`POST`,headers:{accept:`application/json`,authorization:`Bearer ${e.refreshToken}`,"content-type":`application/json`},body:JSON.stringify({invitationId:e.invitationId,...e.displayName===void 0?{}:{displayName:e.displayName}}),cache:`no-store`,credentials:`omit`,signal:e.signal}),n=await Gt(t);if(!t.ok)throw new Q($(n,`error`)??`INVITATION_ACCEPT_FAILED`,$(n,`message`)??`리뷰 초대 수락에 실패했습니다.`,t.status)},...e.collaboration?.cursors===!0||e.collaboration?.chat===!0?{resolveCollaborationRoom:e=>Ht(r,i,e)}:{}}).resolve(e),l=c.resolvePage;if(!l)return c;let u=async t=>{await Ut(r,i,{invitationId:e.invitationId,origin:t.location.origin,pathname:t.location.pathname},t.signal),s=o()};return{...c,resolvePage:async e=>{o()-s>=3e5&&await u(e);try{return await l(e)}catch(t){if(!Bt(t,`PAGE_NOT_FOUND`))throw t;return await u(e),await l(e)}}}}}}function Bt(e,t){return!!(e&&typeof e==`object`&&`code`in e&&e.code===t)}function Vt(e){return{invitationId:e.invitationId,origin:e.location.origin,pathname:e.location.pathname}}async function Ht(e,t,n){let r=await t(`${e}/v1/collaboration/room`,{method:`POST`,headers:{accept:`application/json`,authorization:`Bearer ${n.refreshToken}`,"content-type":`application/json`},body:JSON.stringify({invitationId:n.invitationId,pageId:n.pageId,scopeId:n.scopeId}),cache:`no-store`,credentials:`omit`,signal:n.signal}),i=await Gt(r);if(!r.ok)throw new Q($(i,`error`)??`COLLABORATION_ROOM_FAILED`,$(i,`message`)??`실시간 협업 연결에 실패했습니다.`,r.status);if(!i||typeof i!=`object`||Array.isArray(i))throw new Q(`INVALID_COLLABORATION_ROOM`,`잘못된 협업 응답입니다.`,502);let a=i,o=$(a,`id`);if(!o||!/^vr_[A-Za-z0-9_-]{43}$/u.test(o)||!Number.isSafeInteger(a.expiresAt))throw new Q(`INVALID_COLLABORATION_ROOM`,`잘못된 협업 응답입니다.`,502);return{id:o,expiresAt:a.expiresAt}}async function Ut(e,t,n,r){let i=await t(`${e}/v1/bootstrap`,{method:`POST`,headers:{accept:`application/json`,"content-type":`application/json`},body:JSON.stringify(n),cache:`no-store`,credentials:`omit`,signal:r}),a=await Gt(i);if(!i.ok)throw new Q($(a,`error`)??`BOOTSTRAP_FAILED`,$(a,`message`)??`Visual Review bootstrap에 실패했습니다.`,i.status);return Wt(a)}function Wt(e){if(!e||typeof e!=`object`||Array.isArray(e))throw new Q(`INVALID_BOOTSTRAP_RESPONSE`,`잘못된 bootstrap 응답입니다.`,502);let t=e,n=$(t,`instantAppId`);if(t.apiVersion!==1||!n||!Kt(n))throw new Q(`INVALID_BOOTSTRAP_RESPONSE`,`잘못된 bootstrap 응답입니다.`,502);let r=t.reviewer;if(r==null)return{apiVersion:1,instantAppId:n};let i=$(r,`token`),a=$(r,`email`);if(!i||!a)throw new Q(`INVALID_BOOTSTRAP_RESPONSE`,`잘못된 bootstrap 응답입니다.`,502);return{apiVersion:1,instantAppId:n,reviewer:{token:i,email:a}}}async function Gt(e){try{return await e.json()}catch{throw new Q(`INVALID_BOOTSTRAP_RESPONSE`,`JSON bootstrap 응답이 필요합니다.`,502)}}function $(e,t){if(!e||typeof e!=`object`||Array.isArray(e))return null;let n=e[t];return typeof n==`string`&&n.trim()?n.trim():null}function Kt(e){return/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(e)}export{Rt as createHostedReviewRuntime};
package/dist/index.d.ts CHANGED
@@ -81,6 +81,8 @@ export interface VisualReviewReviewerSession {
81
81
  export interface VisualReviewComment {
82
82
  id: string;
83
83
  pageId: string;
84
+ /** Immutable, server-assigned identifier within the page. */
85
+ pageNumber?: number;
84
86
  authorId: string;
85
87
  authorName: string;
86
88
  body: string;
@@ -257,16 +259,35 @@ export interface VisualReviewRuntime {
257
259
  resolve(request: ReviewRuntimeRequest): Promise<ReviewRuntimeContext>;
258
260
  }
259
261
 
262
+ export type VisualReviewPosition =
263
+ | 'top-left'
264
+ | 'top'
265
+ | 'top-right'
266
+ | 'right'
267
+ | 'bottom-right'
268
+ | 'bottom'
269
+ | 'bottom-left'
270
+ | 'left';
271
+
272
+ export interface VisualReviewOffset {
273
+ /** Horizontal displacement in CSS pixels. Positive values move the dock right. */
274
+ x?: number;
275
+ /** Vertical displacement in CSS pixels. Positive values move the dock down. */
276
+ y?: number;
277
+ }
278
+
260
279
  export interface InstallVisualReviewOptions {
261
280
  /** Override only for local development or self-hosted integration tests. */
262
281
  serviceUrl?: string;
263
282
  build?: Partial<VisualReviewBuildContext>;
264
283
  sourceContextResolver?: VisualReviewSourceContextResolver;
265
284
  runtime?: VisualReviewRuntime;
266
- /** Explicit deployment gate. The runtime stays dormant unless this is exactly true. */
285
+ /** Emergency kill switch. Omit for invitation-driven activation. */
267
286
  enabled?: boolean;
268
- /** Explicit developer-only tools. Pair this with the consumer build's dev flag. */
287
+ /** Explicit developer-only tools. */
269
288
  developerTools?: {
289
+ codeContextCopy?: boolean;
290
+ /** @deprecated Use codeContextCopy. */
270
291
  reactGrab?: boolean;
271
292
  };
272
293
  /** Opt-in reviewer collaboration. Each capability is dormant by default. */
@@ -274,6 +295,10 @@ export interface InstallVisualReviewOptions {
274
295
  cursors?: boolean;
275
296
  chat?: boolean;
276
297
  };
298
+ /** Dock anchor. Defaults to `bottom-right`; mobile viewports use a bottom corner. */
299
+ position?: VisualReviewPosition;
300
+ /** Optional displacement from the selected dock anchor. */
301
+ offset?: VisualReviewOffset;
277
302
  activationParam?: string;
278
303
  /** CSP nonce for review-owned styles. Defaults to a nonce found on the host document. */
279
304
  styleNonce?: string;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./install-B5CNlFQl.js";export{e as installVisualReview};
1
+ import{t as e}from"./install-7I-VPQeH.js";export{e as installVisualReview};
@@ -0,0 +1 @@
1
+ const e=`https://visual-review.01.works`;function t(e){let t=new URL(e),n=t.hostname===`localhost`||t.hostname===`127.0.0.1`||t.hostname.endsWith(`.test`)||t.hostname.endsWith(`.local`);if(t.protocol!==`https:`&&!(n&&t.protocol===`http:`))throw Error(`Visual Review serviceUrl must use HTTPS outside local development`);if(t.username||t.password||t.search||t.hash)throw Error(`Visual Review serviceUrl must be a plain origin or base path`);return t.toString().replace(/\/+$/,``)}const n=()=>{let e=Promise.resolve(null);return{ready:e,refresh:()=>e,dispose(){}}},r=new Map,i=6e4,a=1e4;function o(e={}){if(e.enabled===!1||typeof window>`u`||typeof document>`u`)return n();let r=!1,i=0,a=null,o=null,p=null,h=null,g=!1,_=null,S=null,E=t(e.serviceUrl??`https://visual-review.01.works`),k=e.activationParam??`visual-review`,M=K(window),P=y(window.location);if(!(D(E,k,P,M,e.runtime!==void 0)??x(C(E,P.origin))))return n();let F=document.baseURI,I=new WeakMap,L=new Map,R=te(e.styleNonce,document),z=v(window.location,k),B=m(window,e=>{let t=v(e,k);if(t===z)return;let n=O(k,e.href);n&&L.set(e.origin,n),z=t,H(e)}),V=e=>{r||!h||e.target!==h||(z=v(window.location,k),J())};return window.addEventListener(`visual-review:reauthenticate-required`,V),{ready:J(),refresh(){return r?Promise.resolve(null):(z=v(window.location,k),H(y(window.location),!0))},dispose(){r||(r=!0,i+=1,G(),h?.remove(),h=null,S=null,_?.remove(),_=null,L.clear(),B.unsubscribe(),window.removeEventListener(`visual-review:reauthenticate-required`,V))}};function H(e,t=!1){return!h||!S||S.origin!==e.origin||O(k,e.href)?J():U(h,S,e,t)}function U(t,n,r,c){let l=++i;a!==o&&a?.abort();let u=new AbortController;a=u;let d=s(e.build,r),f=(async()=>{try{let i=await j(n.resolvePage({location:r,signal:u.signal,collaboration:{cursors:e.collaboration?.cursors===!0,chat:e.collaboration?.chat===!0,scopeId:d}}),u.signal);return Q(l,u)?(i.commit?.(),t.setPage({pageId:i.pageId,collaboration:{roomId:i.collaborationRoomId,scopeId:d}},{reload:c}),t.isConnected||document.body?.append(t),t):$(l)}catch{return Q(l,u)?J():$(l)}})();return p=f,f}function G(){a?.abort(),o?.abort(),a=null,o=null}function J(){let t=++i;G(),h?.remove(),h=null,S=null,_?.remove();let n=new AbortController,r=document.createElement(`div`);r.dataset.agencyReviewRoot=``,a=n,o=n,_=r;let s=Y(t,n,r);return p=s,s.then(r=>{r&&Q(t,n)&&h===r&&e.onReady?.(r)}),s}async function Y(t,n,i){try{let e=document.body??await N(document,n.signal);if(!Q(t,n))return $(t);e.append(i);let r=await X(t,n,i);return Q(t,n)?r:(i.remove(),$(t))}catch(a){if(!Q(t,n))return i.remove(),$(t);if(d(a,`AUTH_CANCELLED`))return i.remove(),_===i&&(_=null),null;let o=I.get(i)?.[3];Z(a,i);let s=ee(a,I.get(i)?.[2]);if(o!==`storage`||g||!f(a)){let{renderBootstrapError:e}=await import(`./bootstrap-error-X9PqxFnS.js`);if(!Q(t,n))return i.remove(),$(t);let o=f(a)?void 0:()=>{r||_!==i||(z=v(window.location,k),J())};e(i,s.message,R,o)}else i.remove(),_===i&&(_=null);return e.onError?.(s),window.dispatchEvent(new CustomEvent(`visual-review:error`,{detail:s})),null}}async function X(t,n,r){let i=y(window.location),a=D(E,k,i,M,e.runtime!==void 0),o=b(L.get(i.origin)),d=x(C(E,i.origin)),f=a?.source===`url`?a:o??d??a;if(!f)return I.delete(r),r.remove(),_===r&&(_=null),null;let p=f.invitationId;I.set(r,[E,i.origin,p,f.source]),f.source===`url`?(q(E,i.origin,M),L.set(i.origin,p),w(E,i.origin,p),B.suppress(()=>A(k))):f.source===`handoff`&&L.set(i.origin,p),z=v(window.location,k);let m=await j((e.runtime??await c(E)).resolve({invitationId:p,location:i,signal:n.signal,authHost:r,styleNonce:R,collaboration:{cursors:e.collaboration?.cursors===!0,chat:e.collaboration?.chat===!0,scopeId:s(e.build,i)}}),n.signal);if(!Q(t,n))return null;T(E,i.origin,p),W(E,p,i,M),L.set(i.origin,p);let O=e.sourceContextResolver??await u(e.build,F,n.signal,R);if(!Q(t,n))return null;let{mountReviewWidget:N}=await l(n.signal);return Q(t,n)?(h=N({...m,collaboration:{cursors:e.collaboration?.cursors===!0&&!!(m.collaborationRepository&&m.collaborationRoomId),chat:e.collaboration?.chat===!0&&!!m.collaborationRepository,repository:m.collaborationRepository,roomId:m.collaborationRoomId,scopeId:s(e.build,i)},developerTools:e.developerTools?.codeContextCopy===!0||e.developerTools?.reactGrab===!0?{codeContextCopy:!0}:void 0,offset:e.offset,onNavigateToFeedback:async t=>{let n=new URL(t,window.location.href);if(n.origin!==window.location.origin)throw Error(`Visual Review route navigation must stay on the staging origin`);if(e.navigateToFeedback){await e.navigateToFeedback(n.href);return}window.location.assign(n.href)},sourceContextResolver:O,styleNonce:R,position:e.position}),S=m.resolvePage?{origin:i.origin,resolvePage:e=>m.resolvePage(e)}:null,r.remove(),_===r&&(_=null),g=!0,window.dispatchEvent(new CustomEvent(`visual-review:ready`,{detail:h})),h):null}function Z(e,t){if(!f(e))return;let n=I.get(t);if(!n)return;let[r,i,a]=n;T(r,i,a),L.get(i)===a&&L.delete(i),q(r,i,M,a)}function Q(e,t){return!r&&i===e&&a===t&&!t.signal.aborted}function $(e){return!r&&i!==e?p:null}}function s(e,t){return JSON.stringify([P(e).buildId,t.pathname,t.search])}async function c(e){let{createHostedReviewRuntime:t}=await import(`./hosted-runtime-1nEC1hjg.js`);return t({serviceUrl:e})}async function l(e){return M(import(`./src-DdAg1F1c.js`),e,a,`리뷰 위젯을 불러오는 시간이 초과되었습니다. 다시 시도해 주세요.`)}async function u(e,t,n,r){let i=P(e),{createGeneratedSourceContextResolver:o}=await M(import(`./generated-source-context-D59o5CKS.js`),n,a,`리뷰 소스 정보를 불러오는 시간이 초과되었습니다. 다시 시도해 주세요.`);return o({build:i})}function d(e,t){return e instanceof Error&&`code`in e&&e.code===t}function f(e){let t=e instanceof Error&&`code`in e?e.code:void 0;return typeof t==`string`&&/^INVITATION_(?:EXPIRED|NOT_FOUND|REVOKED)$/.test(t)}const p=new WeakMap;function m(e,t){let n=p.get(e);if(!n){let t=e.history,r=t.pushState,i=t.replaceState,a=new Set;n={history:t,originalPushStateDescriptor:Object.getOwnPropertyDescriptor(t,`pushState`),originalReplaceStateDescriptor:Object.getOwnPropertyDescriptor(t,`replaceState`),patchedPushState:function(t,n,i){r.call(this,t,n,i),h(e)},patchedReplaceState:function(t,n,r){i.call(this,t,n,r),h(e)},popstateListener:()=>h(e),subscribers:a,suppressionDepth:0},p.set(e,n),g(t,`pushState`,n.patchedPushState),g(t,`replaceState`,n.patchedReplaceState),e.addEventListener(`popstate`,n.popstateListener)}n.subscribers.add(t);let r=!0;return{suppress(e){n.suppressionDepth+=1;try{return e()}finally{--n.suppressionDepth}},unsubscribe(){r&&(r=!1,n.subscribers.delete(t),!(n.subscribers.size>0)&&(e.removeEventListener(`popstate`,n.popstateListener),_(n.history,`pushState`,n.patchedPushState,n.originalPushStateDescriptor),_(n.history,`replaceState`,n.patchedReplaceState,n.originalReplaceStateDescriptor),p.delete(e)))}}}function h(e){let t=p.get(e);if(!t||t.suppressionDepth>0)return;let n=y(e.location);[...t.subscribers].forEach(e=>e(n))}function g(e,t,n){Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:!0,value:n})}function _(e,t,n,r){if(Object.getOwnPropertyDescriptor(e,t)?.value===n){if(r){Object.defineProperty(e,t,r);return}delete e[t]}}function v(e,t){return`${e.origin}\n${e.pathname}\n${O(t,e.href)??``}`}function y(e){return new URL(e.href)}function b(e){return e?{invitationId:e,source:`memory`}:null}function x(e){return e?{invitationId:e,source:`handoff`}:null}function S(e,t){return JSON.stringify([e,t])}function C(e,t,n=Date.now()){let i=S(e,t),a=r.get(i);if(a){if(a.expiresAt>n)return a.invitationId;clearTimeout(a.timeout),r.delete(i)}}function w(e,t,n,a=Date.now()){E(a);let o=S(e,t),s=r.get(o);for(s&&(clearTimeout(s.timeout),r.delete(o));r.size>=32;){let e=r.keys().next().value;if(!e)break;let t=r.get(e);t&&clearTimeout(t.timeout),r.delete(e)}let c=a+i,l=setTimeout(()=>{r.get(o)?.expiresAt===c&&r.delete(o)},i);r.set(o,{invitationId:n,expiresAt:c,timeout:l})}function T(e,t,n){let i=S(e,t),a=r.get(i);a?.invitationId===n&&(clearTimeout(a.timeout),r.delete(i))}function E(e){for(let[t,n]of r)n.expiresAt>e||(clearTimeout(n.timeout),r.delete(t))}function D(e,t,n,r,i){let a=O(t,n.href);if(a)return{invitationId:a,source:`url`};let o=G(r,J(e,n.origin));return o?{invitationId:o,source:`storage`}:i&&Y(n.hostname)?{invitationId:`local-preview`,source:`local-preview`}:null}function O(e,t){let n=new URL(t),r=k(n.hash);return(r?.get(e)??r?.get(`visual-review`)??r?.get(`agency-review`)??r?.get(`agency-review-invite`)??n.searchParams.get(e)??n.searchParams.get(`agency-review`)??n.searchParams.get(`agency-review-invite`))?.trim()||null}function k(e){let t=e.startsWith(`#`)?e.slice(1):e,n=t.startsWith(`?`)?t.slice(1):t;return n.includes(`=`)?new URLSearchParams(n):null}function A(e){let t=new URL(window.location.href),n=new Set([e,`visual-review`,`agency-review`,`agency-review-invite`]),r=!1;n.forEach(e=>{t.searchParams.has(e)&&(t.searchParams.delete(e),r=!0)});let i=k(t.hash);if(i){n.forEach(e=>{i.has(e)&&(i.delete(e),r=!0)});let e=i.toString();t.hash=e?`#${e}`:``}r&&window.history.replaceState(window.history.state,``,`${t.pathname}${t.search}${t.hash}`)}function j(e,t){return new Promise((n,r)=>{let i=()=>r(new DOMException(`Aborted`,`AbortError`));if(t.aborted){i();return}t.addEventListener(`abort`,i,{once:!0}),e.then(e=>{t.removeEventListener(`abort`,i),n(e)},e=>{t.removeEventListener(`abort`,i),r(e)})})}function M(e,t,n,r){return new Promise((i,a)=>{let o=!1,s=e=>{o||(o=!0,clearTimeout(l),t.removeEventListener(`abort`,c),e())},c=()=>{s(()=>a(new DOMException(`Aborted`,`AbortError`)))},l=setTimeout(()=>{s(()=>a(Error(r)))},n);if(t.aborted){c();return}t.addEventListener(`abort`,c,{once:!0}),e.then(e=>s(()=>i(e)),e=>s(()=>a(e)))})}function N(e,t){return e.body?Promise.resolve(e.body):t.aborted?Promise.reject(new DOMException(`Aborted`,`AbortError`)):new Promise((n,r)=>{let i=!1,a=new MutationObserver(()=>c()),o=()=>{e.removeEventListener(`DOMContentLoaded`,c),t.removeEventListener(`abort`,l),a.disconnect()},s=e=>{i||(i=!0,o(),e())},c=()=>{let t=e.body;t&&s(()=>n(t))},l=()=>{s(()=>r(new DOMException(`Aborted`,`AbortError`)))};e.addEventListener(`DOMContentLoaded`,c),t.addEventListener(`abort`,l,{once:!0}),a.observe(e,{childList:!0,subtree:!0}),c()})}function P(e){let t=document.querySelector(`meta[name="visual-review-build"]`),n=document.querySelector(`meta[name="visual-review-commit"]`),r=U(L()),i=U(R());return{buildId:H(e?.buildId)??V(t)??r??`unversioned`,gitCommit:z(e,n,i),generatedAt:e?.generatedAt,mode:e?.mode??I(F())??(Y(window.location.hostname)?`development`:`production`)}}function F(){try{return process.env.VISUAL_REVIEW_PUBLIC_BUILD_MODE}catch{return}}function I(e){return e===`development`||e===`preview`||e===`production`?e:void 0}function L(){try{return process.env.VISUAL_REVIEW_PUBLIC_BUILD_ID}catch{return}}function R(){try{return process.env.VISUAL_REVIEW_PUBLIC_GIT_COMMIT}catch{return}}function z(e,t,n){return e?.gitCommit===null?null:typeof e?.gitCommit==`string`?B(e.gitCommit):B(t?.content)??B(n)}function B(e){if(typeof e!=`string`)return null;let t=e.trim().toLowerCase();return/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(t)?t:null}function V(e){return U(e?.content)}function H(e){let t=typeof e==`string`?e.trim():``;return t.length>0?t:null}function U(e){return typeof e==`string`&&e.length>0?e:null}function W(e,t,n,r){try{r?.setItem(J(e,n.origin),t)}catch{}}function G(e,t){try{return e?.getItem(t)??null}catch{return null}}function K(e){try{return e.localStorage}catch{return null}}function q(e,t,n,r){try{if(!n)return;let i=J(e,t),a=n.getItem(i);if(a===null||r!==void 0&&a!==r)return;n.removeItem(i)}catch{}}function J(e,t){return`visual-review:${e}:${t}:invitation`}function Y(e){return e===`localhost`||e===`127.0.0.1`||e.endsWith(`.test`)||e.endsWith(`.local`)}function ee(e,t){let n=e instanceof Error?e.message:String(e),r=Error(X(n,t));if(e&&typeof e==`object`){let n=`code`in e?e.code:void 0,i=`status`in e?e.status:void 0;typeof n==`string`&&Object.defineProperty(r,"code",{configurable:!0,enumerable:!0,value:X(n,t)}),typeof i==`number`&&Number.isFinite(i)&&Object.defineProperty(r,"status",{configurable:!0,enumerable:!0,value:i})}return r}function X(e,t){if(!t)return e;let n=new Set([t,encodeURIComponent(t)]),r=e;for(let e of n)e&&(r=r.split(e).join(`[redacted]`));return r}function te(e,t){if(e!==void 0)return Q(e);let n=t.currentScript,r=n?Z(n):void 0;if(r)return r;try{for(let e of t.querySelectorAll(`script[nonce],style[nonce]`)){let t=Z(e);if(t)return t}}catch{}}function Z(e){let t=`nonce`in e?e.nonce:void 0;return typeof t==`string`?Q(t):void 0}function Q(e){return e.trim()||void 0}export{e as n,t as r,o as t};
@@ -10,9 +10,11 @@ export interface VisualReviewBuildIdentityOptions {
10
10
  export type VisualReviewReleaseMode = 'development' | 'preview' | 'production';
11
11
 
12
12
  export interface VisualReviewNextOptions extends VisualReviewBuildIdentityOptions {
13
+ /** Public capture provenance. Providers are auto-detected when omitted. */
14
+ mode?: VisualReviewReleaseMode;
13
15
  /**
14
- * Writes `data-review-source="app/page.tsx:12:5"` onto JSX elements by
15
- * default. Set `false` for identity-only output.
16
+ * Writes `data-review-source="app/page.tsx:12:5"` onto JSX elements.
17
+ * Development/preview default to true; production defaults to false.
16
18
  */
17
19
  sourceMetadata?: boolean;
18
20
  }
@@ -83,11 +83,12 @@ function parseNextBuildIdentityRecord(value) {
83
83
  }
84
84
  function withVisualReview(nextConfig = {}, options = {}) {
85
85
  const identity = resolveNextVisualReviewBuildIdentity(options, process.env);
86
- const sourceMetadataEnabled = options.sourceMetadata ?? true;
86
+ const releaseMode = options.mode ?? inferReleaseMode(process.env);
87
+ const sourceMetadataEnabled = options.sourceMetadata ?? releaseMode !== "production";
87
88
  const injected = {
88
89
  [BUILD_ID_LITERAL]: identity.buildId,
89
90
  [GIT_COMMIT_LITERAL]: identity.gitCommit ?? "",
90
- [BUILD_MODE_LITERAL]: inferReleaseMode(process.env)
91
+ [BUILD_MODE_LITERAL]: releaseMode
91
92
  };
92
93
  if (typeof nextConfig === "function") return function visualReviewNextConfig(...arguments_) {
93
94
  const resolved = nextConfig.apply(this, arguments_);
@@ -180,7 +181,7 @@ function appendTurbopackRules(existing, sourceMetadata) {
180
181
  ]) {
181
182
  const pattern = `*.${extension}`;
182
183
  const rule = {
183
- condition: { all: [{ not: "foreign" }, "production"] },
184
+ condition: { all: [{ not: "foreign" }] },
184
185
  loaders: [{
185
186
  loader: sourceMetadata.loader,
186
187
  options: { compile: true }
package/dist/react.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./install-B5CNlFQl.js";import{useEffect as t,useRef as n}from"react";function r({activationParam:r,build:i,collaboration:a,developerTools:o,enabled:s,navigateToFeedback:c,onError:l,onReady:u,runtime:d,serviceUrl:f,sourceContextResolver:p,styleNonce:m}){let h=n(l),g=n(u),_=i?.buildId,v=i?.gitCommit,y=i?.generatedAt,b=i?.mode,x=o?.reactGrab===!0,S=a?.cursors===!0,C=a?.chat===!0;return h.current=l,g.current=u,t(()=>{if(s!==!0)return;let t=e({activationParam:r,build:_===void 0&&v===void 0&&y===void 0&&b===void 0?void 0:{buildId:_,gitCommit:v,generatedAt:y,mode:b},collaboration:S||C?{cursors:S,chat:C}:void 0,developerTools:x?{reactGrab:!0}:void 0,enabled:s,navigateToFeedback:c,onError:e=>h.current?.(e),onReady:e=>g.current?.(e),runtime:d,serviceUrl:f,sourceContextResolver:p,styleNonce:m});return()=>t.dispose()},[r,_,b,C,S,x,s,y,v,c,d,f,p,m]),null}r.displayName=`VisualReview`;export{r as VisualReview};
1
+ import{t as e}from"./install-7I-VPQeH.js";import{useEffect as t,useRef as n}from"react";function r({activationParam:r,build:i,collaboration:a,developerTools:o,enabled:s,navigateToFeedback:c,offset:l,onError:u,onReady:d,position:f,runtime:p,serviceUrl:m,sourceContextResolver:h,styleNonce:g}){let _=n(u),v=n(d),y=i?.buildId,b=i?.gitCommit,x=i?.generatedAt,S=i?.mode,C=o?.codeContextCopy===!0||o?.reactGrab===!0,w=a?.cursors===!0,T=a?.chat===!0;return _.current=u,v.current=d,t(()=>{if(s===!1)return;let t=e({activationParam:r,build:y===void 0&&b===void 0&&x===void 0&&S===void 0?void 0:{buildId:y,gitCommit:b,generatedAt:x,mode:S},collaboration:w||T?{cursors:w,chat:T}:void 0,developerTools:C?{codeContextCopy:!0}:void 0,enabled:s,navigateToFeedback:c,offset:l,onError:e=>_.current?.(e),onReady:e=>v.current?.(e),runtime:p,position:f,serviceUrl:m,sourceContextResolver:h,styleNonce:g});return()=>t.dispose()},[r,y,S,T,w,C,s,x,b,c,l?.x,l?.y,f,p,m,h,g]),null}r.displayName=`VisualReview`;export{r as VisualReview};