@designfever/web-review-kit 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +77 -179
  2. package/dist/{chunk-U5K2YGGL.js → chunk-I76WEDLA.js} +2248 -2722
  3. package/dist/chunk-I76WEDLA.js.map +1 -0
  4. package/dist/index.cjs +2346 -2603
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +10 -6
  7. package/dist/index.d.ts +10 -6
  8. package/dist/index.js +220 -7
  9. package/dist/index.js.map +1 -1
  10. package/dist/react-shell.cjs +8953 -6632
  11. package/dist/react-shell.cjs.map +1 -1
  12. package/dist/react-shell.d.cts +8 -3
  13. package/dist/react-shell.d.ts +8 -3
  14. package/dist/react-shell.js +5956 -3636
  15. package/dist/react-shell.js.map +1 -1
  16. package/dist/{types-D_mNjOHx.d.cts → types-Cf2x5ky6.d.cts} +8 -14
  17. package/dist/{types-D_mNjOHx.d.ts → types-Cf2x5ky6.d.ts} +8 -14
  18. package/dist/vite.cjs +186 -0
  19. package/dist/vite.cjs.map +1 -0
  20. package/dist/vite.d.cts +16 -0
  21. package/dist/vite.d.ts +16 -0
  22. package/dist/vite.js +161 -0
  23. package/dist/vite.js.map +1 -0
  24. package/docs/README.md +21 -30
  25. package/docs/adaptor.sample.ts +182 -0
  26. package/docs/architecture.md +125 -0
  27. package/docs/db-setup.md +253 -0
  28. package/docs/figma-overlay.md +52 -0
  29. package/docs/grid-overlay.md +38 -0
  30. package/docs/installation.md +108 -40
  31. package/package.json +22 -6
  32. package/dist/chunk-U5K2YGGL.js.map +0 -1
  33. package/docs/adapter-handoff.md +0 -146
  34. package/docs/concept.md +0 -102
  35. package/docs/df-sheet-adapter.md +0 -336
  36. package/docs/df-sheet-next.md +0 -222
  37. package/docs/initial-plan.md +0 -226
  38. package/docs/package-split-checkpoint.md +0 -79
  39. package/docs/presence-handoff.md +0 -138
  40. package/docs/review-feedback-2026-06-20.md +0 -267
  41. package/docs/smoke-baseline-2026-06-20.md +0 -41
  42. package/docs/stabilize-ui-work-guide.md +0 -243
  43. package/docs/supabase-presence.md +0 -198
  44. package/docs/supabase-review-items.md +0 -365
  45. package/docs/supabase.md +0 -205
@@ -0,0 +1,182 @@
1
+ import {
2
+ REVIEW_WORKFLOW_STATUS_OPTIONS,
3
+ type ReviewItem,
4
+ type ReviewItemQuery,
5
+ type ReviewItemStatus,
6
+ type ReviewSource,
7
+ type WebReviewKitAdapter,
8
+ } from '@designfever/web-review-kit';
9
+ import type {
10
+ ReviewShellAdapter,
11
+ } from '@designfever/web-review-kit/react-shell';
12
+
13
+ type RemoteReviewAdapterOptions = {
14
+ baseUrl: string;
15
+ projectId: string;
16
+ source?: ReviewSource;
17
+ token?: string;
18
+ };
19
+
20
+ type RemoteReviewItemResponse = ReviewItem | { item: ReviewItem };
21
+
22
+ /*
23
+ * WebReviewKitAdapter is the core storage contract.
24
+ *
25
+ * ReviewItem is the full QA payload. Persist it as structured JSON so marker,
26
+ * anchor, selection, viewport, scroll, status, and external issue fields survive.
27
+ *
28
+ * ReviewItemQuery is used by the shell for current-page lists and sitemap counts.
29
+ * A remote backend should support at least projectId, routeKey, status, source,
30
+ * and pageId when the host project needs page-level grouping.
31
+ *
32
+ * Keep private tokens, admin keys, numbering, and permission checks in your
33
+ * backend. Browser code should only call browser-safe endpoints.
34
+ */
35
+ export function createRemoteReviewAdapter(
36
+ options: RemoteReviewAdapterOptions
37
+ ): WebReviewKitAdapter {
38
+ const source = options.source ?? 'remote';
39
+
40
+ return {
41
+ async get(id) {
42
+ return readReviewItem(
43
+ await requestJson<RemoteReviewItemResponse>(
44
+ `/review-items/${encodeURIComponent(id)}`,
45
+ options
46
+ )
47
+ );
48
+ },
49
+
50
+ async list(query) {
51
+ const rows = await requestJson<RemoteReviewItemResponse[]>(
52
+ `/review-items?${createListParams(query, source).toString()}`,
53
+ options
54
+ );
55
+
56
+ return rows.map(readReviewItem);
57
+ },
58
+
59
+ async create(item) {
60
+ return readReviewItem(
61
+ await requestJson<RemoteReviewItemResponse>('/review-items', options, {
62
+ method: 'POST',
63
+ body: JSON.stringify({
64
+ project_id: options.projectId,
65
+ source,
66
+ item,
67
+ }),
68
+ })
69
+ );
70
+ },
71
+
72
+ async update(id, patch) {
73
+ return readReviewItem(
74
+ await requestJson<RemoteReviewItemResponse>(
75
+ `/review-items/${encodeURIComponent(id)}`,
76
+ options,
77
+ {
78
+ method: 'PATCH',
79
+ body: JSON.stringify({ patch }),
80
+ }
81
+ )
82
+ );
83
+ },
84
+
85
+ async remove(id) {
86
+ await requestJson<void>(
87
+ `/review-items/${encodeURIComponent(id)}`,
88
+ options,
89
+ { method: 'DELETE' }
90
+ );
91
+ },
92
+ };
93
+ }
94
+
95
+ /*
96
+ * ReviewShellAdapter is the React shell wiring.
97
+ *
98
+ * label becomes the URL source, for example /review?source=remote.
99
+ * create controls whether the shell can write to this adapter.
100
+ * canWrite can be true or limited to ['dom', 'note', 'area'].
101
+ * updateStatus drives the status buttons in the QA panel.
102
+ * remove enables delete actions for this source.
103
+ */
104
+ export function createRemoteReviewShellAdapter(
105
+ options: RemoteReviewAdapterOptions
106
+ ): ReviewShellAdapter {
107
+ const adapter = createRemoteReviewAdapter(options);
108
+
109
+ return {
110
+ label: options.source ?? 'remote',
111
+ get: (id) => adapter.get(id),
112
+ list: (query) => adapter.list(query),
113
+ create: (item) => adapter.create(item),
114
+ canWrite: true,
115
+ statusOptions: REVIEW_WORKFLOW_STATUS_OPTIONS,
116
+ updateStatus: ({ id, status }) =>
117
+ adapter.update(id, { status: normalizeRemoteStatus(status) }),
118
+ remove: (id) => adapter.remove(id),
119
+ };
120
+ }
121
+
122
+ function appendParam(
123
+ params: URLSearchParams,
124
+ key: string,
125
+ value: string | undefined
126
+ ) {
127
+ if (value) params.set(key, value);
128
+ }
129
+
130
+ function createListParams(query: ReviewItemQuery, source: ReviewSource) {
131
+ const params = new URLSearchParams();
132
+ params.set('project_id', query.projectId);
133
+ params.set('source', query.source ?? source);
134
+ appendParam(params, 'page_id', query.pageId);
135
+ appendParam(params, 'route_key', query.routeKey ?? query.normalizedPath);
136
+ appendParam(params, 'status', query.status);
137
+ return params;
138
+ }
139
+
140
+ async function requestJson<T>(
141
+ path: string,
142
+ options: RemoteReviewAdapterOptions,
143
+ init: RequestInit = {}
144
+ ) {
145
+ const url = new URL(path, ensureTrailingSlash(options.baseUrl));
146
+ const headers = new Headers(init.headers);
147
+
148
+ headers.set('Accept', 'application/json');
149
+ if (init.body && !headers.has('Content-Type')) {
150
+ headers.set('Content-Type', 'application/json');
151
+ }
152
+ if (options.token) {
153
+ headers.set('Authorization', `Bearer ${options.token}`);
154
+ }
155
+
156
+ const response = await fetch(url, {
157
+ ...init,
158
+ headers,
159
+ credentials: 'include',
160
+ });
161
+
162
+ if (!response.ok) {
163
+ throw new Error(`remote review adapter request failed: ${response.status}`);
164
+ }
165
+ if (response.status === 204) return undefined as T;
166
+
167
+ return response.json() as Promise<T>;
168
+ }
169
+
170
+ function readReviewItem(response: RemoteReviewItemResponse) {
171
+ return 'item' in response ? response.item : response;
172
+ }
173
+
174
+ function ensureTrailingSlash(value: string) {
175
+ return value.endsWith('/') ? value : `${value}/`;
176
+ }
177
+
178
+ function normalizeRemoteStatus(status: ReviewItemStatus) {
179
+ if (status === 'open') return 'todo';
180
+ if (status === 'resolved') return 'done';
181
+ return status;
182
+ }
@@ -0,0 +1,125 @@
1
+ # Architecture and Runtime Logic
2
+
3
+ `df-web-review-kit` has two main runtime surfaces:
4
+
5
+ - `core`: a vanilla DOM runtime that can mount review overlays on a same-origin target page.
6
+ - `react-shell`: a React review app that hosts pages in an iframe and controls the core runtime.
7
+
8
+ This split keeps the target-page overlay independent from React while allowing the review shell to provide richer workflow UI.
9
+
10
+ ## High-Level Flow
11
+
12
+ ```txt
13
+ React shell
14
+ -> renders topbar, QA panel, iframe, ruler, settings
15
+ -> creates core runtime with createWebReviewKit()
16
+ -> passes iframe target geometry through getReviewKitTarget()
17
+
18
+ Core runtime
19
+ -> mounts a shadow DOM overlay
20
+ -> handles note/area/DOM selection
21
+ -> creates markers and highlights over the target viewport
22
+ -> persists ReviewItem records through the configured adapter
23
+ ```
24
+
25
+ In the React shell, core is created with:
26
+
27
+ ```ts
28
+ createWebReviewKit({
29
+ target: () => getReviewKitTarget({ frameScrollRef, iframeRef }),
30
+ ui: {
31
+ panel: false,
32
+ },
33
+ });
34
+ ```
35
+
36
+ `ui.panel: false` means the React shell owns the side panel and toolbar. Core still owns target overlays such as note pins, area selection boxes, DOM hover outlines, saved item markers, and highlights.
37
+
38
+ ## Core Modules
39
+
40
+ - `web.review.kit.app.ts`: controller lifecycle, state transitions, adapter calls, item creation, restore flow.
41
+ - `web.review.kit.view.ts`: vanilla DOM renderer for core overlay UI.
42
+ - `dom.anchor.ts`: selector candidate generation, anchor rebinding, text fingerprint matching.
43
+ - `geometry.ts`: target-space and host-space coordinate conversion.
44
+ - `review/item.ts`: marker, selection, highlight, and fallback resolution.
45
+ - `review/scope.ts`: viewport scope grouping and numbering.
46
+ - `review/format.ts`: compact item/draft metadata labels.
47
+ - `scroll.ts`: scroll restore helpers.
48
+ - `location.ts`: public URL and route key helpers.
49
+
50
+ ## Coordinate Spaces
51
+
52
+ Core uses two coordinate spaces:
53
+
54
+ - **Target space**: coordinates relative to the iframe or target page viewport.
55
+ - **Host space**: coordinates relative to the review shell page that contains the iframe.
56
+
57
+ Persisted review data uses target-space viewport values plus optional anchor-relative values. Rendering converts target-space markers and selections into host-space overlay positions.
58
+
59
+ ## Anchor and Restore Logic
60
+
61
+ When a user creates a DOM or area item, core tries to capture:
62
+
63
+ - an explicit configured anchor such as `data-qa-id`
64
+ - a meaningful `id`
65
+ - a meaningful class
66
+ - a DOM path fallback
67
+ - a short text fingerprint
68
+
69
+ Restore prefers anchor-relative coordinates because absolute viewport coordinates drift after layout changes. If an anchor cannot be resolved, core falls back to the original viewport coordinate adjusted by saved scroll position.
70
+
71
+ ## Adapter Boundary
72
+
73
+ Core never owns persistence storage directly. It only calls the configured `WebReviewKitAdapter`:
74
+
75
+ - `list`
76
+ - `get`
77
+ - `create`
78
+ - `update`
79
+ - `remove`
80
+
81
+ The default local adapter is for draft/local review work. Supabase is optional host wiring, not a required backend.
82
+
83
+ ## React Shell Boundary
84
+
85
+ `react-shell` owns reviewer workflow UI:
86
+
87
+ - iframe target routing
88
+ - QA list and item actions
89
+ - viewport presets
90
+ - sitemap modal
91
+ - settings modal
92
+ - ruler UI
93
+ - presence UI
94
+ - host overlay toggles such as grid and Figma
95
+
96
+ React shell should call the core controller instead of duplicating target overlay logic.
97
+
98
+ ## Figma Overlay Direction
99
+
100
+ Figma overlay work should stay outside core unless it needs target runtime primitives.
101
+
102
+ Preferred direction:
103
+
104
+ ```txt
105
+ src/react-shell/figma/
106
+ controls.tsx
107
+ sync.ts
108
+ overlay.state.ts
109
+
110
+ src/figma/
111
+ matching.ts
112
+ types.ts
113
+ ```
114
+
115
+ Shared coordinate math can reuse `core/geometry.ts` or move to a future shared module if both core and Figma need it heavily.
116
+
117
+ Avoid turning `core` into a feature bucket. Core should stay focused on target review runtime behavior.
118
+
119
+ ## Extension Rules
120
+
121
+ - Put target overlay primitives in `core`.
122
+ - Put reviewer workflow UI in `react-shell`.
123
+ - Put feature-specific integration logic, such as Figma matching, in its own module.
124
+ - Keep adapter contracts storage-agnostic.
125
+ - Prefer anchor-relative data for anything that must survive layout changes.
@@ -0,0 +1,253 @@
1
+ # DB Setup
2
+
3
+ Supabase is an optional backend adapter. A host project may use it for canonical QA items and Realtime Presence, but the public package does not own the Supabase project or any operator secrets.
4
+
5
+ ## Environment
6
+
7
+ ```env
8
+ VITE_REVIEW_PROJECT_ID=df-web-review-kit
9
+ VITE_REVIEW_SUPABASE_URL=https://your-project.supabase.co
10
+ VITE_REVIEW_SUPABASE_ANON_KEY=
11
+ VITE_REVIEW_SUPABASE_TABLE=review_items
12
+ VITE_REVIEW_SUPABASE_PRESENCE_PRIVATE=false
13
+ ```
14
+
15
+ Use only an `anon` key in browser env. Keep `service_role`, OpenClaw `KUKU_*`, and future admin keys in an operator layer or backend service.
16
+
17
+ ## Review Item Schema
18
+
19
+ Run this in the Supabase SQL editor to create the tables, indexes, RPC, and grants.
20
+
21
+ ```sql
22
+ create table if not exists public.review_items (
23
+ id text primary key,
24
+ project_id text not null,
25
+ route_key text not null,
26
+ source text not null default 'supabase',
27
+ review_number integer,
28
+ status text not null default 'todo',
29
+ item jsonb not null,
30
+ created_at timestamptz not null default now(),
31
+ updated_at timestamptz not null default now()
32
+ );
33
+
34
+ create unique index if not exists review_items_project_review_number_idx
35
+ on public.review_items (project_id, source, review_number)
36
+ where review_number is not null;
37
+
38
+ create index if not exists review_items_project_route_updated_idx
39
+ on public.review_items (project_id, source, route_key, updated_at desc);
40
+
41
+ create index if not exists review_items_project_status_idx
42
+ on public.review_items (project_id, source, status);
43
+
44
+ create table if not exists public.review_project_counters (
45
+ project_id text not null,
46
+ source text not null default 'supabase',
47
+ next_review_number integer not null default 1,
48
+ updated_at timestamptz not null default now(),
49
+ primary key (project_id, source),
50
+ constraint review_project_counters_next_review_number_check
51
+ check (next_review_number > 0)
52
+ );
53
+
54
+ create or replace function public.create_review_item(
55
+ p_id text,
56
+ p_project_id text,
57
+ p_route_key text,
58
+ p_source text,
59
+ p_status text,
60
+ p_item jsonb
61
+ )
62
+ returns public.review_items
63
+ language plpgsql
64
+ security invoker
65
+ set search_path = public
66
+ as $$
67
+ declare
68
+ v_review_number integer;
69
+ v_now timestamptz := now();
70
+ v_row public.review_items;
71
+ begin
72
+ insert into public.review_project_counters (
73
+ project_id,
74
+ source,
75
+ next_review_number,
76
+ updated_at
77
+ )
78
+ select
79
+ p_project_id,
80
+ p_source,
81
+ coalesce(max(review_number), 0) + 2,
82
+ v_now
83
+ from public.review_items
84
+ where project_id = p_project_id
85
+ and source = p_source
86
+ on conflict (project_id, source) do update
87
+ set next_review_number = greatest(
88
+ public.review_project_counters.next_review_number + 1,
89
+ excluded.next_review_number
90
+ ),
91
+ updated_at = excluded.updated_at
92
+ returning next_review_number - 1 into v_review_number;
93
+
94
+ insert into public.review_items (
95
+ id,
96
+ project_id,
97
+ route_key,
98
+ source,
99
+ review_number,
100
+ status,
101
+ item,
102
+ created_at,
103
+ updated_at
104
+ )
105
+ values (
106
+ p_id,
107
+ p_project_id,
108
+ p_route_key,
109
+ p_source,
110
+ v_review_number,
111
+ p_status,
112
+ p_item || jsonb_build_object(
113
+ 'id', p_id,
114
+ 'reviewNumber', v_review_number,
115
+ 'projectId', p_project_id,
116
+ 'routeKey', p_route_key,
117
+ 'normalizedPath', coalesce(nullif(p_item->>'normalizedPath', ''), p_route_key),
118
+ 'status', p_status,
119
+ 'externalIssueId', p_id,
120
+ 'submittedAt', coalesce(p_item->>'submittedAt', v_now::text),
121
+ 'submitStatus', coalesce(p_item->>'submitStatus', 'submitted'),
122
+ 'createdAt', v_now::text,
123
+ 'updatedAt', v_now::text
124
+ ),
125
+ v_now,
126
+ v_now
127
+ )
128
+ returning * into v_row;
129
+
130
+ return v_row;
131
+ end;
132
+ $$;
133
+
134
+ grant execute on function public.create_review_item(
135
+ text,
136
+ text,
137
+ text,
138
+ text,
139
+ text,
140
+ jsonb
141
+ ) to anon;
142
+
143
+ grant select, insert, update, delete on public.review_items to anon;
144
+ grant select, insert, update on public.review_project_counters to anon;
145
+ ```
146
+
147
+ ## RLS Policies
148
+
149
+ Run this after the schema SQL. Replace `df-web-review-kit` if `VITE_REVIEW_PROJECT_ID` uses a different value.
150
+
151
+ ```sql
152
+ alter table public.review_items enable row level security;
153
+ alter table public.review_project_counters enable row level security;
154
+
155
+ drop policy if exists review_items_sample_read on public.review_items;
156
+ create policy review_items_sample_read
157
+ on public.review_items
158
+ for select
159
+ to anon
160
+ using (project_id = 'df-web-review-kit');
161
+
162
+ drop policy if exists review_items_sample_insert on public.review_items;
163
+ create policy review_items_sample_insert
164
+ on public.review_items
165
+ for insert
166
+ to anon
167
+ with check (project_id = 'df-web-review-kit');
168
+
169
+ drop policy if exists review_items_sample_update on public.review_items;
170
+ create policy review_items_sample_update
171
+ on public.review_items
172
+ for update
173
+ to anon
174
+ using (project_id = 'df-web-review-kit')
175
+ with check (project_id = 'df-web-review-kit');
176
+
177
+ drop policy if exists review_items_sample_delete on public.review_items;
178
+ create policy review_items_sample_delete
179
+ on public.review_items
180
+ for delete
181
+ to anon
182
+ using (project_id = 'df-web-review-kit');
183
+
184
+ drop policy if exists review_project_counters_sample_read on public.review_project_counters;
185
+ create policy review_project_counters_sample_read
186
+ on public.review_project_counters
187
+ for select
188
+ to anon
189
+ using (project_id = 'df-web-review-kit');
190
+
191
+ drop policy if exists review_project_counters_sample_insert on public.review_project_counters;
192
+ create policy review_project_counters_sample_insert
193
+ on public.review_project_counters
194
+ for insert
195
+ to anon
196
+ with check (project_id = 'df-web-review-kit');
197
+
198
+ drop policy if exists review_project_counters_sample_update on public.review_project_counters;
199
+ create policy review_project_counters_sample_update
200
+ on public.review_project_counters
201
+ for update
202
+ to anon
203
+ using (project_id = 'df-web-review-kit')
204
+ with check (project_id = 'df-web-review-kit');
205
+ ```
206
+
207
+ ## Adapter Behavior
208
+
209
+ - `list`: reads by `project_id`, `source`, optional `route_key`, optional `status`.
210
+ - `create`: discards local draft number and calls `create_review_item` for a canonical `review_number`.
211
+ - `update`: updates row columns and the nested `item` JSON.
212
+ - `remove`: deletes the row.
213
+
214
+ Remote numbers are canonical. Local draft numbers can overlap between browsers and are not reused as remote numbers.
215
+
216
+ ## Presence
217
+
218
+ Supabase Presence is optional session state. It is not QA item storage.
219
+
220
+ Default topic:
221
+
222
+ ```txt
223
+ review-presence-<projectId>
224
+ ```
225
+
226
+ The adapter normalizes unsafe topic characters. `private=false` is enough for quick dev validation. `private=true` requires an authenticated Supabase session and Realtime authorization policies for `realtime.messages`.
227
+
228
+ Use project-level channels instead of page-level channels so the sitemap can show who is on each page.
229
+
230
+ ## Security
231
+
232
+ The RLS policies above are sample/dev policies. Anyone with the anon key and allowed `project_id` can create, update, and delete QA rows.
233
+
234
+ For production, use one of these instead:
235
+
236
+ - Supabase Auth plus project member table based RLS
237
+ - Supabase Edge Function
238
+ - host project backend proxy
239
+ - private admin service
240
+
241
+ Never put a `service_role` key in browser env.
242
+
243
+ ## Validation
244
+
245
+ 1. Open `/review?source=supabase`.
246
+ 2. Create a local item.
247
+ 3. Submit it to remote.
248
+ 4. Confirm the local draft disappears.
249
+ 5. Confirm remote list/get works.
250
+ 6. Change status.
251
+ 7. Delete the remote item.
252
+ 8. Create another remote item and confirm the number is not reused.
253
+ 9. If presence is enabled, open two browser tabs and confirm each user appears on the active page or sitemap.
@@ -0,0 +1,52 @@
1
+ # Figma Overlay
2
+
3
+ The Figma overlay button in the review shell toggles a host-page helper. The package does not fetch Figma data by itself and does not own a server-side Figma token.
4
+
5
+ ## User Flow
6
+
7
+ - Open `/review`.
8
+ - Click the settings button.
9
+ - Enter a Figma token if the host helper requires one.
10
+ - Click the Figma overlay button or press `f`.
11
+
12
+ The token is stored in browser localStorage with this key:
13
+
14
+ ```txt
15
+ figma-token
16
+ ```
17
+
18
+ Treat this as a dev/debug convenience. Do not use it for private server tokens.
19
+
20
+ ## Availability
21
+
22
+ The shell currently enables the Figma overlay on viewport presets whose `kind` is:
23
+
24
+ - `mobile`
25
+ - `wide`
26
+
27
+ Other viewport kinds show the unavailable message.
28
+
29
+ ## Host Requirements
30
+
31
+ The target page inside the iframe must already support the Figma helper.
32
+
33
+ Expected behavior:
34
+
35
+ - The target page reacts to a `KeyboardEvent` with `code: 'KeyF'`.
36
+ - The target page mounts a visible Figma helper layer.
37
+ - The helper root uses one of these selectors:
38
+ - `.helper-figma-root`
39
+ - `.helper-figma-loading-backdrop`
40
+
41
+ The review shell uses those selectors only to detect whether the overlay is active.
42
+
43
+ ## Troubleshooting
44
+
45
+ If the button does nothing:
46
+
47
+ - Confirm the iframe target page is same-origin and can receive dispatched keyboard events.
48
+ - Confirm the host helper listens for `KeyF`.
49
+ - Confirm the helper renders `.helper-figma-root` or `.helper-figma-loading-backdrop`.
50
+ - Confirm the current viewport preset is `mobile` or `wide`.
51
+
52
+ If the overlay needs a private Figma integration, move that work to a backend/admin service and expose only browser-safe state to the host page.
@@ -0,0 +1,38 @@
1
+ # Grid Overlay
2
+
3
+ The grid overlay button in the review shell toggles a host-page layout/helper overlay. It is for visual alignment while reviewing pages.
4
+
5
+ ## User Flow
6
+
7
+ - Open `/review`.
8
+ - Click the grid button or press `g`.
9
+ - The target page toggles its own grid/helper layer.
10
+
11
+ ## Host Requirements
12
+
13
+ The target page inside the iframe must already support the grid helper.
14
+
15
+ Expected behavior:
16
+
17
+ - The target page reacts to a `KeyboardEvent` with `code: 'KeyG'`.
18
+ - The target page toggles a visible grid/helper layer.
19
+ - The active state is reflected by one of these:
20
+ - `document.body.classList.contains('is-help')`
21
+ - `.helper.onShow`
22
+
23
+ The review shell uses those signals to keep the toolbar button state in sync.
24
+
25
+ ## Notes
26
+
27
+ - The package does not prescribe the grid design.
28
+ - The grid overlay is not persisted as QA data.
29
+ - Use project-specific CSS/helper code in the host page when the grid differs by brand or design system.
30
+
31
+ ## Troubleshooting
32
+
33
+ If the icon state does not change:
34
+
35
+ - Confirm the target page helper is mounted.
36
+ - Confirm it listens for `KeyG`.
37
+ - Confirm it sets `body.is-help` or `.helper.onShow`.
38
+ - Confirm the iframe is same-origin and keyboard events are not blocked.