@artymclabin/qa-review 0.3.7 → 0.3.9
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/CHANGELOG.md +19 -0
- package/README.md +177 -177
- package/dist/client/QAReviewOverlay.d.ts +25 -1
- package/dist/client/QAReviewOverlay.js +122 -13
- package/dist/client/revisit.d.ts +27 -4
- package/dist/client/revisit.js +24 -7
- package/package.json +61 -61
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.8 - 2026-08-09
|
|
4
|
+
|
|
5
|
+
- BACK TO REVIEW IS AN UNDO, NOT A RESTART: the finish panel's "Back to review"
|
|
6
|
+
sent the reviewer to item 1. You press it having just finished, because of
|
|
7
|
+
something about the item you just judged, so on a forty-item board getting
|
|
8
|
+
back to where you were meant clicking Next thirty-nine times. It now opens the
|
|
9
|
+
LAST item of the round.
|
|
10
|
+
- COLLECT SEVERAL REFERENCES FOR ONE PASTE: right-clicking "Copy ref" ADDS this
|
|
11
|
+
item's reference line to the ones already collected and puts the whole list on
|
|
12
|
+
the clipboard, so a sweep of a board can be pasted into a chat in one go. A
|
|
13
|
+
plain click still copies just this item, and also ends the collection, so
|
|
14
|
+
there is a way out of a half-built list. The button reports how many are
|
|
15
|
+
collected. Right-clicking the same item twice is idempotent.
|
|
16
|
+
It does NOT call navigator.clipboard.readText(): appending to "whatever is on
|
|
17
|
+
the clipboard" would need a permission Firefox never grants a page, and on a
|
|
18
|
+
grant it would happily append a password manager's payload or whatever the
|
|
19
|
+
reviewer copied a minute earlier. The list lives in the panel, so the button
|
|
20
|
+
can only ever append references it produced itself.
|
|
21
|
+
|
|
3
22
|
## 0.3.7 - 2026-07-30
|
|
4
23
|
|
|
5
24
|
- MOBILE PREVIEW SCROLLS TO THE ITEM: the phone-frame iframe loaded each page
|
package/README.md
CHANGED
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
# qa-review
|
|
2
|
-
|
|
3
|
-
Interactive on-page QA review for React apps: a spotlight walkthrough overlay
|
|
4
|
-
that steps a reviewer through the elements of a real rendered page
|
|
5
|
-
(approve / reject / note / undo / design-variation picking), plus a durable
|
|
6
|
-
server-side verdict ledger with a self-provisioning Postgres store.
|
|
7
|
-
|
|
8
|
-
Built for "founder reviews the page element by element" workflows: the reviewer
|
|
9
|
-
opens the live page, the overlay dims everything except the current item, and
|
|
10
|
-
every verdict is persisted immediately - refreshes, storage wipes, and device
|
|
11
|
-
switches never lose progress.
|
|
12
|
-
|
|
13
|
-
## Features
|
|
14
|
-
|
|
15
|
-
- **Spotlight walkthrough** - dims the page except the current `[data-qa]`
|
|
16
|
-
target; card shows title/subtitle + Approve/Reject/Prev/Next.
|
|
17
|
-
- **Round-based review** - the set of actionable (not-yet-approved) items is
|
|
18
|
-
frozen at load with a fixed denominator ("1 of N"), so approved items never
|
|
19
|
-
re-appear mid-round.
|
|
20
|
-
- **Durable verdict ledger** - verdicts are saved per item as you go
|
|
21
|
-
(localStorage cache + server ledger). No bulk reset exists; a single item is
|
|
22
|
-
re-queued by invalidating just it (`verdict: null`).
|
|
23
|
-
- **Undo, Peek (3s clean-page view), element-picker notes, keyboard driving**
|
|
24
|
-
(A/R/arrows/U/Esc), draggable panel.
|
|
25
|
-
- **Design variations** - an item can expose N live-swappable variants; the
|
|
26
|
-
chosen variant is recorded with the approval.
|
|
27
|
-
- **Session snapshots** - optional "Save review to database" submit that stores
|
|
28
|
-
the full run (summary + per-item results) for auditability.
|
|
29
|
-
- **BYO auth** - the server handlers take an `authorize(req)` callback; plug in
|
|
30
|
-
any gate (SSO, password, none for local tools). Fail-closed.
|
|
31
|
-
- **Self-provisioning Postgres storage** - the adapter creates its own
|
|
32
|
-
`qa_review_*` tables on first use (versioned, advisory-locked). A `site`
|
|
33
|
-
scope column lets one database serve many installs.
|
|
34
|
-
- **Cross-page journey** - an ordered multi-page review: finishing a page
|
|
35
|
-
navigates immediately to the next page with pending items (activation query
|
|
36
|
-
params survive the hop); a completion panel shows only when the whole
|
|
37
|
-
journey is clean.
|
|
38
|
-
- **Task items** - selectorless items render as a centered card with an
|
|
39
|
-
optional action-link button, for visit-this-page checks and decisions.
|
|
40
|
-
- **NOT-ALTERED poka-yoke** - every verdict stores a content fingerprint; a
|
|
41
|
-
re-shown rejected item whose content still hashes identical gets a
|
|
42
|
-
system-computed "NOT ALTERED since your rejection" badge.
|
|
43
|
-
- **Device-split approvals** - items can require per-device sign-off
|
|
44
|
-
(PC/mobile); approved only when every required device approved. Plain
|
|
45
|
-
historical approvals are grandfathered as fully approved.
|
|
46
|
-
- **Sub-highlights** - `highlightWords` marks specific words inside the
|
|
47
|
-
spotlighted element, replacing "where to look" prose.
|
|
48
|
-
- **Codenames** - a deterministic two-word codename per item ("red-apple")
|
|
49
|
-
with a Copy-ref button and an exported resolver, so humans and agents can
|
|
50
|
-
reference items by name. Included in state GET responses.
|
|
51
|
-
- **Minimize bubble** - the panel collapses to a draggable floating bubble
|
|
52
|
-
(mouse + touch); tap to restore.
|
|
53
|
-
- **No CSS toolchain required** - the overlay injects its own stylesheet;
|
|
54
|
-
brand colors come from a small theme prop.
|
|
55
|
-
|
|
56
|
-
## Install
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
npm install @artymclabin/qa-review
|
|
60
|
-
# or
|
|
61
|
-
pnpm add @artymclabin/qa-review
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Install straight from git if you want an unreleased commit:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
npm install github:ArtyMcLabin/qa-review
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
For CI environments without registry access, vendor a tarball:
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
# in this repo
|
|
74
|
-
npm pack # -> artymclabin-qa-review-<version>.tgz
|
|
75
|
-
# in the consumer
|
|
76
|
-
pnpm add ./vendor/artymclabin-qa-review-<version>.tgz
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Peer dependencies: `react`, `react-dom`, `lucide-react`.
|
|
80
|
-
|
|
81
|
-
## Quick start (Next.js App Router)
|
|
82
|
-
|
|
83
|
-
### 1. Server: mount the handlers
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
// src/lib/qa-review.ts
|
|
87
|
-
import { createQAReviewHandlers } from "@artymclabin/qa-review/server";
|
|
88
|
-
|
|
89
|
-
export const qaHandlers = createQAReviewHandlers({
|
|
90
|
-
site: "example-site", // scope for this install (one DB can serve many)
|
|
91
|
-
authorize: async (req) => {
|
|
92
|
-
const user = await verifyMySession(req); // your gate; null -> 401
|
|
93
|
-
return user ? { reviewer: user.name, displayName: user.name } : null;
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
// src/app/api/qa/state/route.ts
|
|
100
|
-
import { qaHandlers } from "@/lib/qa-review";
|
|
101
|
-
export const runtime = "nodejs";
|
|
102
|
-
export const dynamic = "force-dynamic";
|
|
103
|
-
export const GET = qaHandlers.stateGET;
|
|
104
|
-
export const POST = qaHandlers.statePOST;
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Mount `submitPOST`, `sessionsGET`, and `accessGET` the same way on their own
|
|
108
|
-
routes as needed.
|
|
109
|
-
|
|
110
|
-
### 2. Database
|
|
111
|
-
|
|
112
|
-
Set the connection string (any Postgres - local, Neon, Supabase, RDS):
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
QA_REVIEW_DATABASE_URL=postgresql://user:password@db.example.com/mydb
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
No migrations to write: on the first request the adapter provisions
|
|
119
|
-
`qa_review_state` (the per-item ledger), `qa_review_sessions` (session
|
|
120
|
-
snapshots), and `qa_review_migrations` (its own version bookkeeping).
|
|
121
|
-
|
|
122
|
-
### 3. Client: mark targets and mount the overlay
|
|
123
|
-
|
|
124
|
-
```tsx
|
|
125
|
-
// Any element you want reviewed gets a stable [data-qa] anchor:
|
|
126
|
-
<section data-qa="hero">...</section>
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
```tsx
|
|
130
|
-
"use client";
|
|
131
|
-
import { QAReviewOverlay } from "@artymclabin/qa-review";
|
|
132
|
-
|
|
133
|
-
const ITEMS = [
|
|
134
|
-
{ id: "hero", title: "Hero headline", selector: '[data-qa="hero"]' },
|
|
135
|
-
{ id: "pricing", title: "Pricing table", sub: "Check the currency.", selector: '[data-qa="pricing"]' },
|
|
136
|
-
];
|
|
137
|
-
|
|
138
|
-
export function PageQA() {
|
|
139
|
-
return (
|
|
140
|
-
<QAReviewOverlay
|
|
141
|
-
items={ITEMS}
|
|
142
|
-
target="example-site:/pricing" // one ledger bucket per reviewed surface
|
|
143
|
-
gateParam="qaReview" // omit if activation is gated upstream
|
|
144
|
-
submitUrl="/api/qa/submit" // omit to hide the session-save button
|
|
145
|
-
theme={{ accent: "#ffde4d" }}
|
|
146
|
-
/>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## API sketch
|
|
152
|
-
|
|
153
|
-
```ts
|
|
154
|
-
// client
|
|
155
|
-
import {
|
|
156
|
-
QAReviewOverlay, // the overlay component
|
|
157
|
-
createQAStore, // per-target verdict store (localStorage + server mirror)
|
|
158
|
-
targetFromLocation, // ?target= override helper (E2E isolation)
|
|
159
|
-
} from "@artymclabin/qa-review";
|
|
160
|
-
|
|
161
|
-
// server
|
|
162
|
-
import {
|
|
163
|
-
createQAReviewHandlers, // { stateGET, statePOST, submitPOST, sessionsGET, accessGET }
|
|
164
|
-
createPostgresStorage, // default storage; implement QAReviewStorage to swap
|
|
165
|
-
} from "@artymclabin/qa-review/server";
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
State endpoint semantics (the ledger):
|
|
169
|
-
|
|
170
|
-
- `GET ?target=...` -> `{ ok, verdicts: { itemId: { verdict, note, variant } } }`
|
|
171
|
-
- `POST { target, itemId, verdict | note | variant }` -> merge-upsert one item
|
|
172
|
-
- `POST { target, itemId, verdict: null }` -> delete that one item
|
|
173
|
-
(re-queues it on the next round). There is deliberately **no reset-all**.
|
|
174
|
-
|
|
175
|
-
## License
|
|
176
|
-
|
|
177
|
-
MIT
|
|
1
|
+
# qa-review
|
|
2
|
+
|
|
3
|
+
Interactive on-page QA review for React apps: a spotlight walkthrough overlay
|
|
4
|
+
that steps a reviewer through the elements of a real rendered page
|
|
5
|
+
(approve / reject / note / undo / design-variation picking), plus a durable
|
|
6
|
+
server-side verdict ledger with a self-provisioning Postgres store.
|
|
7
|
+
|
|
8
|
+
Built for "founder reviews the page element by element" workflows: the reviewer
|
|
9
|
+
opens the live page, the overlay dims everything except the current item, and
|
|
10
|
+
every verdict is persisted immediately - refreshes, storage wipes, and device
|
|
11
|
+
switches never lose progress.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Spotlight walkthrough** - dims the page except the current `[data-qa]`
|
|
16
|
+
target; card shows title/subtitle + Approve/Reject/Prev/Next.
|
|
17
|
+
- **Round-based review** - the set of actionable (not-yet-approved) items is
|
|
18
|
+
frozen at load with a fixed denominator ("1 of N"), so approved items never
|
|
19
|
+
re-appear mid-round.
|
|
20
|
+
- **Durable verdict ledger** - verdicts are saved per item as you go
|
|
21
|
+
(localStorage cache + server ledger). No bulk reset exists; a single item is
|
|
22
|
+
re-queued by invalidating just it (`verdict: null`).
|
|
23
|
+
- **Undo, Peek (3s clean-page view), element-picker notes, keyboard driving**
|
|
24
|
+
(A/R/arrows/U/Esc), draggable panel.
|
|
25
|
+
- **Design variations** - an item can expose N live-swappable variants; the
|
|
26
|
+
chosen variant is recorded with the approval.
|
|
27
|
+
- **Session snapshots** - optional "Save review to database" submit that stores
|
|
28
|
+
the full run (summary + per-item results) for auditability.
|
|
29
|
+
- **BYO auth** - the server handlers take an `authorize(req)` callback; plug in
|
|
30
|
+
any gate (SSO, password, none for local tools). Fail-closed.
|
|
31
|
+
- **Self-provisioning Postgres storage** - the adapter creates its own
|
|
32
|
+
`qa_review_*` tables on first use (versioned, advisory-locked). A `site`
|
|
33
|
+
scope column lets one database serve many installs.
|
|
34
|
+
- **Cross-page journey** - an ordered multi-page review: finishing a page
|
|
35
|
+
navigates immediately to the next page with pending items (activation query
|
|
36
|
+
params survive the hop); a completion panel shows only when the whole
|
|
37
|
+
journey is clean.
|
|
38
|
+
- **Task items** - selectorless items render as a centered card with an
|
|
39
|
+
optional action-link button, for visit-this-page checks and decisions.
|
|
40
|
+
- **NOT-ALTERED poka-yoke** - every verdict stores a content fingerprint; a
|
|
41
|
+
re-shown rejected item whose content still hashes identical gets a
|
|
42
|
+
system-computed "NOT ALTERED since your rejection" badge.
|
|
43
|
+
- **Device-split approvals** - items can require per-device sign-off
|
|
44
|
+
(PC/mobile); approved only when every required device approved. Plain
|
|
45
|
+
historical approvals are grandfathered as fully approved.
|
|
46
|
+
- **Sub-highlights** - `highlightWords` marks specific words inside the
|
|
47
|
+
spotlighted element, replacing "where to look" prose.
|
|
48
|
+
- **Codenames** - a deterministic two-word codename per item ("red-apple")
|
|
49
|
+
with a Copy-ref button and an exported resolver, so humans and agents can
|
|
50
|
+
reference items by name. Included in state GET responses.
|
|
51
|
+
- **Minimize bubble** - the panel collapses to a draggable floating bubble
|
|
52
|
+
(mouse + touch); tap to restore.
|
|
53
|
+
- **No CSS toolchain required** - the overlay injects its own stylesheet;
|
|
54
|
+
brand colors come from a small theme prop.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install @artymclabin/qa-review
|
|
60
|
+
# or
|
|
61
|
+
pnpm add @artymclabin/qa-review
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Install straight from git if you want an unreleased commit:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install github:ArtyMcLabin/qa-review
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For CI environments without registry access, vendor a tarball:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# in this repo
|
|
74
|
+
npm pack # -> artymclabin-qa-review-<version>.tgz
|
|
75
|
+
# in the consumer
|
|
76
|
+
pnpm add ./vendor/artymclabin-qa-review-<version>.tgz
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Peer dependencies: `react`, `react-dom`, `lucide-react`.
|
|
80
|
+
|
|
81
|
+
## Quick start (Next.js App Router)
|
|
82
|
+
|
|
83
|
+
### 1. Server: mount the handlers
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// src/lib/qa-review.ts
|
|
87
|
+
import { createQAReviewHandlers } from "@artymclabin/qa-review/server";
|
|
88
|
+
|
|
89
|
+
export const qaHandlers = createQAReviewHandlers({
|
|
90
|
+
site: "example-site", // scope for this install (one DB can serve many)
|
|
91
|
+
authorize: async (req) => {
|
|
92
|
+
const user = await verifyMySession(req); // your gate; null -> 401
|
|
93
|
+
return user ? { reviewer: user.name, displayName: user.name } : null;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// src/app/api/qa/state/route.ts
|
|
100
|
+
import { qaHandlers } from "@/lib/qa-review";
|
|
101
|
+
export const runtime = "nodejs";
|
|
102
|
+
export const dynamic = "force-dynamic";
|
|
103
|
+
export const GET = qaHandlers.stateGET;
|
|
104
|
+
export const POST = qaHandlers.statePOST;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Mount `submitPOST`, `sessionsGET`, and `accessGET` the same way on their own
|
|
108
|
+
routes as needed.
|
|
109
|
+
|
|
110
|
+
### 2. Database
|
|
111
|
+
|
|
112
|
+
Set the connection string (any Postgres - local, Neon, Supabase, RDS):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
QA_REVIEW_DATABASE_URL=postgresql://user:password@db.example.com/mydb
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
No migrations to write: on the first request the adapter provisions
|
|
119
|
+
`qa_review_state` (the per-item ledger), `qa_review_sessions` (session
|
|
120
|
+
snapshots), and `qa_review_migrations` (its own version bookkeeping).
|
|
121
|
+
|
|
122
|
+
### 3. Client: mark targets and mount the overlay
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
// Any element you want reviewed gets a stable [data-qa] anchor:
|
|
126
|
+
<section data-qa="hero">...</section>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
"use client";
|
|
131
|
+
import { QAReviewOverlay } from "@artymclabin/qa-review";
|
|
132
|
+
|
|
133
|
+
const ITEMS = [
|
|
134
|
+
{ id: "hero", title: "Hero headline", selector: '[data-qa="hero"]' },
|
|
135
|
+
{ id: "pricing", title: "Pricing table", sub: "Check the currency.", selector: '[data-qa="pricing"]' },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
export function PageQA() {
|
|
139
|
+
return (
|
|
140
|
+
<QAReviewOverlay
|
|
141
|
+
items={ITEMS}
|
|
142
|
+
target="example-site:/pricing" // one ledger bucket per reviewed surface
|
|
143
|
+
gateParam="qaReview" // omit if activation is gated upstream
|
|
144
|
+
submitUrl="/api/qa/submit" // omit to hide the session-save button
|
|
145
|
+
theme={{ accent: "#ffde4d" }}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## API sketch
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// client
|
|
155
|
+
import {
|
|
156
|
+
QAReviewOverlay, // the overlay component
|
|
157
|
+
createQAStore, // per-target verdict store (localStorage + server mirror)
|
|
158
|
+
targetFromLocation, // ?target= override helper (E2E isolation)
|
|
159
|
+
} from "@artymclabin/qa-review";
|
|
160
|
+
|
|
161
|
+
// server
|
|
162
|
+
import {
|
|
163
|
+
createQAReviewHandlers, // { stateGET, statePOST, submitPOST, sessionsGET, accessGET }
|
|
164
|
+
createPostgresStorage, // default storage; implement QAReviewStorage to swap
|
|
165
|
+
} from "@artymclabin/qa-review/server";
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
State endpoint semantics (the ledger):
|
|
169
|
+
|
|
170
|
+
- `GET ?target=...` -> `{ ok, verdicts: { itemId: { verdict, note, variant } } }`
|
|
171
|
+
- `POST { target, itemId, verdict | note | variant }` -> merge-upsert one item
|
|
172
|
+
- `POST { target, itemId, verdict: null }` -> delete that one item
|
|
173
|
+
(re-queues it on the next round). There is deliberately **no reset-all**.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -4,7 +4,31 @@ import { type QAJourneyConfig } from "./journey.js";
|
|
|
4
4
|
/** Was the pointer gesture a click (vs a drag)? Exported for tests. */
|
|
5
5
|
export declare function isClickGesture(dx: number, dy: number): boolean;
|
|
6
6
|
/** Note-reference text for a picked element: « label » (truncated) or tag. */
|
|
7
|
-
export
|
|
7
|
+
export interface PickedElementDescriptor {
|
|
8
|
+
textContent?: string | null;
|
|
9
|
+
tagName: string;
|
|
10
|
+
/** aria-label, or the aria-labelledby target's text. */
|
|
11
|
+
ariaLabel?: string | null;
|
|
12
|
+
/** Text of an associated <label>, whether wrapping or `for=`-linked. */
|
|
13
|
+
labelText?: string | null;
|
|
14
|
+
placeholder?: string | null;
|
|
15
|
+
alt?: string | null;
|
|
16
|
+
title?: string | null;
|
|
17
|
+
/** Current value, for a control the reviewer has already filled in. */
|
|
18
|
+
value?: string | null;
|
|
19
|
+
name?: string | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 🚨 A form control has no `textContent`, so reading only that made every input,
|
|
23
|
+
* textarea and select come out as its bare tag name: a reviewer picking seven link
|
|
24
|
+
* fields got seven « input » refs with no way to say which row they meant. The
|
|
25
|
+
* order below is "what a human would call it" - its own text, then the accessible
|
|
26
|
+
* name, then the label a designer wrote, then what it holds - and the tag name only
|
|
27
|
+
* when the element genuinely has no name at all.
|
|
28
|
+
*/
|
|
29
|
+
export declare function pickedElementRef(el: PickedElementDescriptor): string;
|
|
30
|
+
/** Read a live DOM element into the descriptor above. */
|
|
31
|
+
export declare function describePickedElement(el: Element): PickedElementDescriptor;
|
|
8
32
|
/**
|
|
9
33
|
* Element-pick mode semantics (0.3.1): LEFT click picks and exits the mode;
|
|
10
34
|
* RIGHT click picks and STAYS for multi-select (context menu suppressed).
|
|
@@ -52,11 +52,63 @@ const CLICK_DRAG_THRESHOLD_PX = 6;
|
|
|
52
52
|
export function isClickGesture(dx, dy) {
|
|
53
53
|
return Math.hypot(dx, dy) < CLICK_DRAG_THRESHOLD_PX;
|
|
54
54
|
}
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 🚨 A form control has no `textContent`, so reading only that made every input,
|
|
57
|
+
* textarea and select come out as its bare tag name: a reviewer picking seven link
|
|
58
|
+
* fields got seven « input » refs with no way to say which row they meant. The
|
|
59
|
+
* order below is "what a human would call it" - its own text, then the accessible
|
|
60
|
+
* name, then the label a designer wrote, then what it holds - and the tag name only
|
|
61
|
+
* when the element genuinely has no name at all.
|
|
62
|
+
*/
|
|
63
|
+
export function pickedElementRef(el) {
|
|
64
|
+
const clean = (v) => (v || "").replace(/\s+/g, " ").trim();
|
|
65
|
+
const label = [
|
|
66
|
+
clean(el.textContent),
|
|
67
|
+
clean(el.ariaLabel),
|
|
68
|
+
clean(el.labelText),
|
|
69
|
+
clean(el.placeholder),
|
|
70
|
+
clean(el.alt),
|
|
71
|
+
clean(el.title),
|
|
72
|
+
clean(el.value),
|
|
73
|
+
clean(el.name),
|
|
74
|
+
]
|
|
75
|
+
.find((c) => c.length > 0)
|
|
76
|
+
?.slice(0, 48) || el.tagName.toLowerCase();
|
|
58
77
|
return ` «${label}» `;
|
|
59
78
|
}
|
|
79
|
+
/** Read a live DOM element into the descriptor above. */
|
|
80
|
+
export function describePickedElement(el) {
|
|
81
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
82
|
+
const labelledByText = labelledBy
|
|
83
|
+
? labelledBy
|
|
84
|
+
.split(/\s+/)
|
|
85
|
+
.map((id) => el.ownerDocument?.getElementById(id)?.textContent ?? "")
|
|
86
|
+
.join(" ")
|
|
87
|
+
: null;
|
|
88
|
+
// A control's label is either an ancestor <label> or one pointing at its id.
|
|
89
|
+
let labelText = null;
|
|
90
|
+
const id = el.getAttribute("id");
|
|
91
|
+
if (id) {
|
|
92
|
+
const escaped = id.replace(/["\\]/g, "\\$&");
|
|
93
|
+
labelText = el.ownerDocument?.querySelector(`label[for="${escaped}"]`)?.textContent ?? null;
|
|
94
|
+
}
|
|
95
|
+
if (!labelText)
|
|
96
|
+
labelText = el.closest("label")?.textContent ?? null;
|
|
97
|
+
const isControl = ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName);
|
|
98
|
+
return {
|
|
99
|
+
// A control's own textContent is empty anyway; skipping it explicitly keeps the
|
|
100
|
+
// intent readable rather than relying on that emptiness.
|
|
101
|
+
textContent: isControl ? null : el.textContent,
|
|
102
|
+
tagName: el.tagName,
|
|
103
|
+
ariaLabel: el.getAttribute("aria-label") || labelledByText,
|
|
104
|
+
labelText,
|
|
105
|
+
placeholder: el.getAttribute("placeholder"),
|
|
106
|
+
alt: el.getAttribute("alt"),
|
|
107
|
+
title: el.getAttribute("title"),
|
|
108
|
+
value: isControl ? (el.value ?? null) : null,
|
|
109
|
+
name: el.getAttribute("name"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
60
112
|
/**
|
|
61
113
|
* Element-pick mode semantics (0.3.1): LEFT click picks and exits the mode;
|
|
62
114
|
* RIGHT click picks and STAYS for multi-select (context menu suppressed).
|
|
@@ -147,6 +199,8 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
147
199
|
const [finished, setFinished] = React.useState(false);
|
|
148
200
|
const [copied, setCopied] = React.useState(false);
|
|
149
201
|
const [refCopied, setRefCopied] = React.useState(false);
|
|
202
|
+
/** Reference lines collected by right-clicking "Copy ref". See `appendRef`. */
|
|
203
|
+
const [refBuffer, setRefBuffer] = React.useState([]);
|
|
150
204
|
const [save, setSave] = React.useState({ status: "idle" });
|
|
151
205
|
// Minimized-to-bubble state (replaces the old 3s peek).
|
|
152
206
|
const [minimized, setMinimized] = React.useState(false);
|
|
@@ -376,7 +430,7 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
376
430
|
return; // ignore the panel itself
|
|
377
431
|
e.preventDefault();
|
|
378
432
|
e.stopPropagation();
|
|
379
|
-
const ref = pickedElementRef(el
|
|
433
|
+
const ref = pickedElementRef(describePickedElement(el));
|
|
380
434
|
const ta = noteRef.current;
|
|
381
435
|
if (ta) {
|
|
382
436
|
const s = ta.selectionStart ?? note.length;
|
|
@@ -701,17 +755,56 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
701
755
|
window.prompt("Copy QA results:", json);
|
|
702
756
|
});
|
|
703
757
|
}, [buildPayload]);
|
|
758
|
+
/**
|
|
759
|
+
* Write `text` to the clipboard, falling back to a prompt when the API is
|
|
760
|
+
* unavailable (insecure origin, permission denied). One place, so the single
|
|
761
|
+
* and the collected copy cannot diverge.
|
|
762
|
+
*/
|
|
763
|
+
const writeClipboard = React.useCallback((text, label) => {
|
|
764
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
765
|
+
setRefCopied(true);
|
|
766
|
+
window.setTimeout(() => setRefCopied(false), 1500);
|
|
767
|
+
}, () => {
|
|
768
|
+
window.prompt(label, text);
|
|
769
|
+
});
|
|
770
|
+
}, []);
|
|
704
771
|
const copyRef = React.useCallback(() => {
|
|
705
772
|
if (!current)
|
|
706
773
|
return;
|
|
707
774
|
const line = formatQARef(target, current.id, current.title);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
775
|
+
// A plain click REPLACES, and therefore also ends a collection run - so the
|
|
776
|
+
// way out of a half-built list is the same button you started with.
|
|
777
|
+
setRefBuffer([]);
|
|
778
|
+
writeClipboard(line, "Copy QA reference:");
|
|
779
|
+
}, [current, target, writeClipboard]);
|
|
780
|
+
/**
|
|
781
|
+
* 🚨 COLLECT SEVERAL REFERENCES, PASTE THEM ONCE (Arty 2026-08-09: "qa
|
|
782
|
+
* interactive component - should have right click option on 'copy ref' button,
|
|
783
|
+
* to include an 'append to current clipboard'. Which should pretty much let us
|
|
784
|
+
* collect multiple references together to be able to paste them all in one.
|
|
785
|
+
* This way I would be able to copy multiple references in a single sweep and
|
|
786
|
+
* then paste them all together into claude code").
|
|
787
|
+
*
|
|
788
|
+
* 🚨 IT ACCUMULATES IN THE PANEL, NOT BY READING THE CLIPBOARD.
|
|
789
|
+
* `navigator.clipboard.readText()` is the obvious implementation and the wrong
|
|
790
|
+
* one: it needs a separate permission that Firefox does not grant to a page at
|
|
791
|
+
* all, and on a grant it would append to WHATEVER is on the clipboard -
|
|
792
|
+
* including a password manager's payload or the pasta the operator copied a
|
|
793
|
+
* minute ago off this very board. Keeping the list in the panel means the
|
|
794
|
+
* button can only ever append references it produced itself.
|
|
795
|
+
*/
|
|
796
|
+
const appendRef = React.useCallback(() => {
|
|
797
|
+
if (!current)
|
|
798
|
+
return;
|
|
799
|
+
const line = formatQARef(target, current.id, current.title);
|
|
800
|
+
setRefBuffer((prev) => {
|
|
801
|
+
// Right-clicking the same item twice is a slip, not a request for a
|
|
802
|
+
// duplicate - so it is idempotent rather than additive.
|
|
803
|
+
const next = prev.includes(line) ? prev : [...prev, line];
|
|
804
|
+
writeClipboard(next.join("\n"), "Copy QA references:");
|
|
805
|
+
return next;
|
|
713
806
|
});
|
|
714
|
-
}, [current, target]);
|
|
807
|
+
}, [current, target, writeClipboard]);
|
|
715
808
|
// Persist the completed run as a session snapshot via the submit endpoint.
|
|
716
809
|
const saveToDb = React.useCallback(async () => {
|
|
717
810
|
if (!submitUrl)
|
|
@@ -767,8 +860,15 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
767
860
|
? "Saved to database ✓"
|
|
768
861
|
: "Save review to database"] }), save.status === "error" && _jsx("p", { className: "qar-warn", children: save.message })] })), _jsxs("button", { type: "button", onClick: copyResults, className: "qar-btn-outline-accent", "data-qatip": "Copy all verdicts of this page as JSON", children: [_jsx(ClipboardCopy, { size: 16, "aria-hidden": true }), copied ? "Copied!" : "Copy JSON"] }), roundTotal > 0 && (_jsx("button", { type: "button", onClick: () => {
|
|
769
862
|
setFinished(false);
|
|
770
|
-
|
|
771
|
-
|
|
863
|
+
// 🚨 THE LAST ITEM, NOT THE FIRST (Arty 2026-08-09: "Clicking
|
|
864
|
+
// on 'Back to review' in the QA interactive panel should go to
|
|
865
|
+
// the last item and not to the first item - as if it was undo
|
|
866
|
+
// instead of restart"). You press this having just finished,
|
|
867
|
+
// because of something about the item you just judged. Landing
|
|
868
|
+
// on item 1 of forty makes the button a restart, and getting
|
|
869
|
+
// back to where you were means clicking Next until you arrive.
|
|
870
|
+
setIndex(Math.max(0, roundTotal - 1));
|
|
871
|
+
}, className: "qar-btn-outline", "data-qatip": "Go back to the last item you reviewed", children: "Back to review" })), _jsx("button", { type: "button", onClick: exit, className: "qar-btn-ghost", children: "Exit QA mode" })] })] }) }), document.body);
|
|
772
872
|
}
|
|
773
873
|
if (!current)
|
|
774
874
|
return null;
|
|
@@ -821,7 +921,16 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
821
921
|
: { top: 24, left: 16 }),
|
|
822
922
|
width: CARD_W,
|
|
823
923
|
height: CARD_H,
|
|
824
|
-
}, children: [_jsxs("div", { className: "qar-card-header", onPointerDown: onCardDragStart, style: { cursor: "move" }, children: [_jsxs("span", { className: "qar-counter", "data-qatip": `Position in this page's review round${inJourney ? " · journey progress across the reviewed pages" : ""}. Drag this bar to move the panel.`, children: [index + 1, " of ", roundTotal, " to review", inJourney && (_jsxs("span", { className: "qar-muted", children: [" ", "\u00B7 page ", journeyIdx + 1, "/", journey.pages.length] }))] }), _jsxs("span", { className: "qar-header-tools", children: [current.device && (_jsx("span", { "data-qatip": DEVICE_EMOJI_TIP[current.device] ?? "target viewport(s)", children: current.device })), !currentIsTask && (_jsx("button", { type: "button", onClick: jump, "aria-label": "Jump to the highlighted section", "data-qatip": "Jump: scroll to the highlighted section", className: "qar-tool-btn", children: _jsx(Locate, { size: 14 }) })), _jsx("button", { type: "button", onClick: undo, disabled: !undoStack.length, "aria-label": "Undo the last approve/reject", "data-qatip": "Undo the last approve/reject (U or Ctrl+Z)", className: "qar-tool-btn", children: _jsx(Undo2, { size: 14 }) }), _jsx("button", { type: "button", onClick: () => setMobilePreview((v) => !v), "aria-label": "Toggle the mobile preview", "data-qatip": "Mobile preview: view this page in a phone-sized frame (for Approve Mobile on desktop)", className: `qar-tool-btn${mobilePreview ? " qar-picking" : ""}`, children: _jsx(Smartphone, { size: 14 }) }), _jsx("button", { type: "button", onClick: () => setMinimized(true), "aria-label": "Minimize to a floating bubble", "data-qatip": "Minimize: collapse into a floating bubble (M). Tap the bubble to restore.", className: "qar-tool-btn", children: _jsx(Minimize2, { size: 14 }) }), _jsx("button", { type: "button", onClick: exit, "aria-label": "Exit QA mode", "data-qatip": "Exit QA mode (Esc)", className: "qar-close-btn", children: _jsx(X, { size: 16 }) })] })] }), _jsxs("div", { className: "qar-ref-row", children: [_jsx("span", { className: "qar-codename", "data-qatip": "Stable codename for this item - say or paste it to reference the item in chat", children: codename }), _jsxs("button", { type: "button", onClick: copyRef,
|
|
924
|
+
}, children: [_jsxs("div", { className: "qar-card-header", onPointerDown: onCardDragStart, style: { cursor: "move" }, children: [_jsxs("span", { className: "qar-counter", "data-qatip": `Position in this page's review round${inJourney ? " · journey progress across the reviewed pages" : ""}. Drag this bar to move the panel.`, children: [index + 1, " of ", roundTotal, " to review", inJourney && (_jsxs("span", { className: "qar-muted", children: [" ", "\u00B7 page ", journeyIdx + 1, "/", journey.pages.length] }))] }), _jsxs("span", { className: "qar-header-tools", children: [current.device && (_jsx("span", { "data-qatip": DEVICE_EMOJI_TIP[current.device] ?? "target viewport(s)", children: current.device })), !currentIsTask && (_jsx("button", { type: "button", onClick: jump, "aria-label": "Jump to the highlighted section", "data-qatip": "Jump: scroll to the highlighted section", className: "qar-tool-btn", children: _jsx(Locate, { size: 14 }) })), _jsx("button", { type: "button", onClick: undo, disabled: !undoStack.length, "aria-label": "Undo the last approve/reject", "data-qatip": "Undo the last approve/reject (U or Ctrl+Z)", className: "qar-tool-btn", children: _jsx(Undo2, { size: 14 }) }), _jsx("button", { type: "button", onClick: () => setMobilePreview((v) => !v), "aria-label": "Toggle the mobile preview", "data-qatip": "Mobile preview: view this page in a phone-sized frame (for Approve Mobile on desktop)", className: `qar-tool-btn${mobilePreview ? " qar-picking" : ""}`, children: _jsx(Smartphone, { size: 14 }) }), _jsx("button", { type: "button", onClick: () => setMinimized(true), "aria-label": "Minimize to a floating bubble", "data-qatip": "Minimize: collapse into a floating bubble (M). Tap the bubble to restore.", className: "qar-tool-btn", children: _jsx(Minimize2, { size: 14 }) }), _jsx("button", { type: "button", onClick: exit, "aria-label": "Exit QA mode", "data-qatip": "Exit QA mode (Esc)", className: "qar-close-btn", children: _jsx(X, { size: 16 }) })] })] }), _jsxs("div", { className: "qar-ref-row", children: [_jsx("span", { className: "qar-codename", "data-qatip": "Stable codename for this item - say or paste it to reference the item in chat", children: codename }), _jsxs("button", { type: "button", onClick: copyRef, onContextMenu: (e) => {
|
|
925
|
+
e.preventDefault();
|
|
926
|
+
appendRef();
|
|
927
|
+
}, className: "qar-pick-btn", "data-qatip": "Click: copy this item's reference line. Right-click: ADD it to the ones already collected, so several can be pasted at once.", children: [_jsx(ClipboardCopy, { size: 12 }), " ", refCopied
|
|
928
|
+
? refBuffer.length > 1
|
|
929
|
+
? `Copied ${refBuffer.length}!`
|
|
930
|
+
: "Copied!"
|
|
931
|
+
: refBuffer.length > 0
|
|
932
|
+
? `Copy ref (${refBuffer.length} collected)`
|
|
933
|
+
: "Copy ref"] })] }), _jsxs("div", { className: "qar-card-body", children: [!rect && !currentIsTask && (_jsxs("p", { className: "qar-warn", children: ["Target not on this viewport (", current.selector, ") - may be a mobile-only element."] })), _jsx("h3", { className: "qar-item-title", children: current.title }), current.sub && (_jsx("div", { className: "qar-item-sub", children: current.sub
|
|
825
934
|
.split("\n")
|
|
826
935
|
.map((line) => line.trim())
|
|
827
936
|
.filter(Boolean)
|
package/dist/client/revisit.d.ts
CHANGED
|
@@ -6,23 +6,46 @@ export interface RevisitInfo {
|
|
|
6
6
|
prevVerdict?: string;
|
|
7
7
|
/** The reviewer's note from that prior verdict. */
|
|
8
8
|
prevNote?: string;
|
|
9
|
+
/** Review round the prior verdict was given in. Absent on pre-0.4.0 rows. */
|
|
10
|
+
prevRound?: number;
|
|
9
11
|
}
|
|
10
12
|
export interface RevisitDisplay {
|
|
11
13
|
/** Prominent "Back for review: ..." headline (reason, or generic change note). */
|
|
12
14
|
headline: string | null;
|
|
13
|
-
/** "Your last verdict: reject -
|
|
15
|
+
/** "Your last verdict: reject" - WITHOUT the note (see priorNote). */
|
|
14
16
|
prior: string | null;
|
|
17
|
+
/**
|
|
18
|
+
* The prior note, kept separate so the card can render it COLLAPSED. It used
|
|
19
|
+
* to be inlined into `prior` and repeated again under the NOT-ALTERED badge,
|
|
20
|
+
* so a reviewer read their own note up to three times per card (it also sits
|
|
21
|
+
* in the textarea below). Arty, 2026-08-04: "don't show a copy of it there
|
|
22
|
+
* since the note appears at the bottom anyway ... make the previous rejection
|
|
23
|
+
* note expandable in case I still want to review it."
|
|
24
|
+
*/
|
|
25
|
+
priorNote: string | null;
|
|
15
26
|
/** Show the prominent NOT-ALTERED badge (prior REJECT + identical content). */
|
|
16
27
|
notAltered: boolean;
|
|
17
28
|
}
|
|
18
29
|
/**
|
|
19
30
|
* Pure display logic for a re-queued item. Rules:
|
|
31
|
+
* - SAME ROUND -> nothing at all. Walking back over your own verdicts inside
|
|
32
|
+
* one pass is navigation, not a revisit.
|
|
20
33
|
* - a provided reason headlines as "Back for review: <reason>";
|
|
21
34
|
* - no reason but a CHANGED fingerprint -> generic "Content changed since
|
|
22
35
|
* your last review.";
|
|
23
|
-
* - a prior verdict renders as context ("Your last verdict:
|
|
24
|
-
*
|
|
36
|
+
* - a prior verdict renders as context ("Your last verdict: reject"), with the
|
|
37
|
+
* note handed back separately for collapsed rendering;
|
|
25
38
|
* - prior REJECT + UNCHANGED fingerprint keeps the system-computed
|
|
26
39
|
* NOT-ALTERED badge (reason or not - the reviewer must see nothing moved).
|
|
40
|
+
*
|
|
41
|
+
* `currentRound` is the round being reviewed right now. When either side is
|
|
42
|
+
* unknown the comparison is skipped and the old behaviour stands, so ledgers
|
|
43
|
+
* written before 0.4.0 keep working.
|
|
27
44
|
*/
|
|
28
|
-
export declare function describeRevisit(info: RevisitInfo | undefined, fpStatus: FingerprintStatus | null): RevisitDisplay | null;
|
|
45
|
+
export declare function describeRevisit(info: RevisitInfo | undefined, fpStatus: FingerprintStatus | null, currentRound?: number): RevisitDisplay | null;
|
|
46
|
+
/**
|
|
47
|
+
* True when a stored verdict belongs to the round being reviewed right now.
|
|
48
|
+
* Unknown on either side = "cannot tell", which must NOT suppress the badge - a
|
|
49
|
+
* missing round means a pre-0.4.0 row, and those are genuinely from earlier.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isSameRound(prevRound?: number, currentRound?: number): boolean;
|
package/dist/client/revisit.js
CHANGED
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
/**
|
|
3
3
|
* Pure display logic for a re-queued item. Rules:
|
|
4
|
+
* - SAME ROUND -> nothing at all. Walking back over your own verdicts inside
|
|
5
|
+
* one pass is navigation, not a revisit.
|
|
4
6
|
* - a provided reason headlines as "Back for review: <reason>";
|
|
5
7
|
* - no reason but a CHANGED fingerprint -> generic "Content changed since
|
|
6
8
|
* your last review.";
|
|
7
|
-
* - a prior verdict renders as context ("Your last verdict:
|
|
8
|
-
*
|
|
9
|
+
* - a prior verdict renders as context ("Your last verdict: reject"), with the
|
|
10
|
+
* note handed back separately for collapsed rendering;
|
|
9
11
|
* - prior REJECT + UNCHANGED fingerprint keeps the system-computed
|
|
10
12
|
* NOT-ALTERED badge (reason or not - the reviewer must see nothing moved).
|
|
13
|
+
*
|
|
14
|
+
* `currentRound` is the round being reviewed right now. When either side is
|
|
15
|
+
* unknown the comparison is skipped and the old behaviour stands, so ledgers
|
|
16
|
+
* written before 0.4.0 keep working.
|
|
11
17
|
*/
|
|
12
|
-
export function describeRevisit(info, fpStatus) {
|
|
18
|
+
export function describeRevisit(info, fpStatus, currentRound) {
|
|
13
19
|
if (!info)
|
|
14
20
|
return null;
|
|
21
|
+
if (isSameRound(info.prevRound, currentRound))
|
|
22
|
+
return null;
|
|
15
23
|
const headline = info.reason
|
|
16
24
|
? `Back for review: ${info.reason}`
|
|
17
25
|
: fpStatus === "changed"
|
|
18
26
|
? "Content changed since your last review."
|
|
19
27
|
: null;
|
|
20
|
-
const prior = info.prevVerdict
|
|
21
|
-
|
|
22
|
-
: null;
|
|
28
|
+
const prior = info.prevVerdict ? `Your last verdict: ${info.prevVerdict}` : null;
|
|
29
|
+
const priorNote = info.prevNote ?? null;
|
|
23
30
|
const notAltered = fpStatus === "unchanged" && info.prevVerdict === "reject";
|
|
24
31
|
if (!headline && !prior && !notAltered)
|
|
25
32
|
return null;
|
|
26
|
-
return { headline, prior, notAltered };
|
|
33
|
+
return { headline, prior, priorNote, notAltered };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True when a stored verdict belongs to the round being reviewed right now.
|
|
37
|
+
* Unknown on either side = "cannot tell", which must NOT suppress the badge - a
|
|
38
|
+
* missing round means a pre-0.4.0 row, and those are genuinely from earlier.
|
|
39
|
+
*/
|
|
40
|
+
export function isSameRound(prevRound, currentRound) {
|
|
41
|
+
if (typeof prevRound !== "number" || typeof currentRound !== "number")
|
|
42
|
+
return false;
|
|
43
|
+
return prevRound === currentRound;
|
|
27
44
|
}
|
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@artymclabin/qa-review",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Interactive on-page QA review overlay (spotlight walkthrough, approve/reject verdict ledger) with a framework-agnostic server handler factory and a self-provisioning Postgres store.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/ArtyMcLabin/qa-review.git"
|
|
9
|
-
},
|
|
10
|
-
"type": "module",
|
|
11
|
-
"sideEffects": false,
|
|
12
|
-
"main": "./dist/client/index.js",
|
|
13
|
-
"types": "./dist/client/index.d.ts",
|
|
14
|
-
"exports": {
|
|
15
|
-
".": {
|
|
16
|
-
"types": "./dist/client/index.d.ts",
|
|
17
|
-
"default": "./dist/client/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./client": {
|
|
20
|
-
"types": "./dist/client/index.d.ts",
|
|
21
|
-
"default": "./dist/client/index.js"
|
|
22
|
-
},
|
|
23
|
-
"./server": {
|
|
24
|
-
"types": "./dist/server/index.d.ts",
|
|
25
|
-
"default": "./dist/server/index.js"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"publishConfig": {
|
|
29
|
-
"access": "public"
|
|
30
|
-
},
|
|
31
|
-
"files": [
|
|
32
|
-
"dist",
|
|
33
|
-
"README.md",
|
|
34
|
-
"CHANGELOG.md",
|
|
35
|
-
"LICENSE"
|
|
36
|
-
],
|
|
37
|
-
"scripts": {
|
|
38
|
-
"build": "tsc -p tsconfig.build.json",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"prepare": "npm run build"
|
|
41
|
-
},
|
|
42
|
-
"peerDependencies": {
|
|
43
|
-
"lucide-react": ">=0.400.0",
|
|
44
|
-
"react": ">=18",
|
|
45
|
-
"react-dom": ">=18"
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"postgres": "^3.4.5"
|
|
49
|
-
},
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"@types/node": "^22",
|
|
52
|
-
"@types/react": "^19",
|
|
53
|
-
"@types/react-dom": "^19",
|
|
54
|
-
"jsdom": "^26",
|
|
55
|
-
"lucide-react": "^1.25.0",
|
|
56
|
-
"react": "^19.2.0",
|
|
57
|
-
"react-dom": "^19.2.0",
|
|
58
|
-
"typescript": "^5",
|
|
59
|
-
"vitest": "^3"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@artymclabin/qa-review",
|
|
3
|
+
"version": "0.3.9",
|
|
4
|
+
"description": "Interactive on-page QA review overlay (spotlight walkthrough, approve/reject verdict ledger) with a framework-agnostic server handler factory and a self-provisioning Postgres store.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ArtyMcLabin/qa-review.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"main": "./dist/client/index.js",
|
|
13
|
+
"types": "./dist/client/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/client/index.d.ts",
|
|
17
|
+
"default": "./dist/client/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./client": {
|
|
20
|
+
"types": "./dist/client/index.d.ts",
|
|
21
|
+
"default": "./dist/client/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./server": {
|
|
24
|
+
"types": "./dist/server/index.d.ts",
|
|
25
|
+
"default": "./dist/server/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.build.json",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"prepare": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"lucide-react": ">=0.400.0",
|
|
44
|
+
"react": ">=18",
|
|
45
|
+
"react-dom": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"postgres": "^3.4.5"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22",
|
|
52
|
+
"@types/react": "^19",
|
|
53
|
+
"@types/react-dom": "^19",
|
|
54
|
+
"jsdom": "^26",
|
|
55
|
+
"lucide-react": "^1.25.0",
|
|
56
|
+
"react": "^19.2.0",
|
|
57
|
+
"react-dom": "^19.2.0",
|
|
58
|
+
"typescript": "^5",
|
|
59
|
+
"vitest": "^3"
|
|
60
|
+
}
|
|
61
|
+
}
|