@gridd/feedback-stickers 1.0.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 +280 -0
- package/dist/feedback-stickers.min.js +94 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Feedback Stickers
|
|
2
|
+
|
|
3
|
+
A self-contained JavaScript module that lets reviewers place color-coded sticky notes on any HTML page, export them as YAML, and share them with the page author. Stickers pin to the element they are placed on, so they stay in the right position when the page is viewed at a different window size.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
The built script (`dist/feedback-stickers.min.js`) is added to any HTML file with a single `<script>` tag. It injects a floating 📌 panel into the page. No server, no account, no browser extension required — a reviewer only needs a browser.
|
|
10
|
+
|
|
11
|
+
The review round-trip:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Requester Reviewer
|
|
15
|
+
─────────────────────────────────────────────────────
|
|
16
|
+
Share HTML file →
|
|
17
|
+
Open in browser
|
|
18
|
+
Place stickies & add notes
|
|
19
|
+
← Send back .review.yaml
|
|
20
|
+
Import YAML →
|
|
21
|
+
View stickies
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Docusaurus plugin
|
|
27
|
+
|
|
28
|
+
The `plugin/` directory contains `@gridd/docusaurus-plugin-feedback-stickers`, a thin wrapper that injects the sticker panel into every page of a Docusaurus site and handles client-side navigation automatically.
|
|
29
|
+
|
|
30
|
+
### Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# from the repo root — build the core first, then the plugin
|
|
34
|
+
npm run build
|
|
35
|
+
cd plugin && npm run build && cd ..
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
In your Docusaurus project, link or install the plugin:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# local development (from within the Docusaurus project)
|
|
42
|
+
npm install --save-dev /path/to/gridd-docusaurus-feedback-plugin/plugin
|
|
43
|
+
|
|
44
|
+
# or with npm workspaces / yarn workspaces when co-located in a monorepo
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Configuration (`docusaurus.config.js`)
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
module.exports = {
|
|
51
|
+
// ...
|
|
52
|
+
plugins: [
|
|
53
|
+
[
|
|
54
|
+
'@gridd/docusaurus-plugin-feedback-stickers',
|
|
55
|
+
{
|
|
56
|
+
// enabled: true, // set false to disable without removing the entry
|
|
57
|
+
// devOnly: false, // set true to only show stickers in `docusaurus start`
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### How it handles multiple pages
|
|
65
|
+
|
|
66
|
+
Each page's stickers are stored in the browser's `localStorage` keyed by the **full page URL** (via an FNV-32 hash). Navigating between pages in Docusaurus is a client-side SPA transition — the plugin's `onRouteDidUpdate` hook fires after each navigation, calls `window.__feedbackStickers.refresh()`, which:
|
|
67
|
+
|
|
68
|
+
1. Stops reviewing mode (so stickers from the old page aren't accidentally placed on the new one)
|
|
69
|
+
2. Removes all sticker DOM elements for the previous page
|
|
70
|
+
3. Loads and renders stickers for the new URL from localStorage
|
|
71
|
+
|
|
72
|
+
The panel (📌 FAB) stays mounted across navigations — only the sticker layer swaps.
|
|
73
|
+
|
|
74
|
+
### Plugin options
|
|
75
|
+
|
|
76
|
+
| Option | Type | Default | Description |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `enabled` | `boolean` | `true` | Master switch. `false` removes the panel entirely without touching config. |
|
|
79
|
+
| `devOnly` | `boolean` | `false` | `true` restricts injection to `docusaurus start` (development mode). |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Building from source
|
|
84
|
+
|
|
85
|
+
Requirements: Node.js 18+
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm install
|
|
89
|
+
npm run build # → dist/feedback-stickers.min.js (~18 KB)
|
|
90
|
+
npm run dev # watch mode, unminified + source map
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Embedding the script in an HTML file
|
|
96
|
+
|
|
97
|
+
Add one line before `</body>`:
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<script src="path/to/feedback-stickers.min.js"></script>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Testbed** — the file `starting-point/maturity-assessment-deckai.html` already has this set up. Open it directly in a browser after running `npm run build`.
|
|
104
|
+
|
|
105
|
+
### Single-file distribution (recommended for sharing)
|
|
106
|
+
|
|
107
|
+
Inline the script so the HTML is self-contained and can be shared as a single attachment:
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<script>
|
|
111
|
+
/* paste contents of dist/feedback-stickers.min.js here */
|
|
112
|
+
</script>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or automate with a build step:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cat page.html \
|
|
119
|
+
| sed 's|<script src=".*feedback-stickers.min.js"></script>||' \
|
|
120
|
+
> page-review.html
|
|
121
|
+
echo '<script>' >> page-review.html
|
|
122
|
+
cat dist/feedback-stickers.min.js >> page-review.html
|
|
123
|
+
echo '</script>' >> page-review.html
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Panel controls
|
|
129
|
+
|
|
130
|
+
Click the **📌 button** in the bottom-right corner of any page to open the panel.
|
|
131
|
+
|
|
132
|
+
| Control | Purpose |
|
|
133
|
+
|---|---|
|
|
134
|
+
| **Reviewer** field | Your name or initials — shown on every sticky you place |
|
|
135
|
+
| **Color grid** | 12 color categories; click a swatch to select it as the active color |
|
|
136
|
+
| Color labels | Click the small text under any swatch to rename that category; saved globally in the browser |
|
|
137
|
+
| **Start Reviewing** | Activates placement mode (cursor turns to crosshair) |
|
|
138
|
+
| **Stop Reviewing** | Deactivates placement mode; stickies remain visible |
|
|
139
|
+
| **Export YAML** | Downloads all stickies as a `.review.yaml` file |
|
|
140
|
+
| **Import YAML** | Loads a `.review.yaml` file and merges its stickies onto the page |
|
|
141
|
+
| **Clear** | Removes all stickies for the current page (asks for confirmation) |
|
|
142
|
+
|
|
143
|
+
### Color categories (defaults)
|
|
144
|
+
|
|
145
|
+
| Color | Default label | Suggested use |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| 🟡 Yellow | Note | General observation |
|
|
148
|
+
| 🟠 Amber | Flag | Needs attention |
|
|
149
|
+
| 🟧 Orange | Caution | Risk or concern |
|
|
150
|
+
| 🔴 Red | Blocker | Must fix before release |
|
|
151
|
+
| 🌸 Rose | Critical | Severe issue |
|
|
152
|
+
| 💗 Pink | Design | Visual / layout feedback |
|
|
153
|
+
| 🟣 Purple | UX | Interaction / flow feedback |
|
|
154
|
+
| 🔵 Indigo | Dev | Implementation comment |
|
|
155
|
+
| 💙 Blue | Info | Background context |
|
|
156
|
+
| 🩵 Teal | Content | Copy or text feedback |
|
|
157
|
+
| 🟢 Green | OK | Explicitly approved |
|
|
158
|
+
| 🟩 Lime | Approved | Sign-off |
|
|
159
|
+
|
|
160
|
+
Labels are customisable per session. Renamed labels are saved in the browser's local storage and persist across page loads.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Placing and managing stickies
|
|
165
|
+
|
|
166
|
+
1. Open the panel, enter your name, choose a color category.
|
|
167
|
+
2. Click **Start Reviewing** — the cursor changes to a crosshair.
|
|
168
|
+
3. Click anywhere on the page to drop a sticky at that location.
|
|
169
|
+
4. Type a note in the sticky's text area.
|
|
170
|
+
5. **Minimize** a sticky (−) to collapse it to a colored dot; click the dot to expand it again (only works while reviewing is active).
|
|
171
|
+
6. **Delete** a sticky (×) to remove it permanently.
|
|
172
|
+
7. **Drag** a sticky by its header bar to reposition it (only while reviewing is active).
|
|
173
|
+
8. Click **Stop Reviewing** when done — stickies stay visible but cannot be moved or expanded.
|
|
174
|
+
|
|
175
|
+
Stickies are saved to the browser's local storage automatically as you work. They reload the next time the same file is opened in the same browser.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Review workflow — step by step
|
|
180
|
+
|
|
181
|
+
### Step 1 — Requester: prepare the file
|
|
182
|
+
|
|
183
|
+
Build the script and embed it in the HTML page you want reviewed:
|
|
184
|
+
|
|
185
|
+
```html
|
|
186
|
+
<!-- at the end of <body> -->
|
|
187
|
+
<script src="feedback-stickers.min.js"></script>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Or inline the minified script for a single self-contained file (see *Single-file distribution* above).
|
|
191
|
+
|
|
192
|
+
Send the HTML file to the reviewer. If the script is not inlined, include `feedback-stickers.min.js` alongside it in the same folder (or zip archive).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### Step 2 — Reviewer: open the file
|
|
197
|
+
|
|
198
|
+
Open the HTML file in a **desktop browser** (Chrome, Firefox, or Safari). Local files work fine — no web server needed. If the browser blocks local scripts, see the note at the bottom of this section.
|
|
199
|
+
|
|
200
|
+
> **Chrome tip:** If you see a blank panel or the 📌 button does nothing, open DevTools console (F12) to check for errors. For `file://` pages Chrome sometimes requires you to allow local file access: go to `chrome://settings/content/javascript` and make sure JavaScript is enabled.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### Step 3 — Reviewer: annotate the page
|
|
205
|
+
|
|
206
|
+
1. Click the **📌 button** (bottom-right).
|
|
207
|
+
2. Enter your **name or initials** in the Reviewer field.
|
|
208
|
+
3. Pick a **color** matching the type of feedback (customize labels if your team has a shared convention).
|
|
209
|
+
4. Click **Start Reviewing**.
|
|
210
|
+
5. Click on the part of the page you want to annotate — a sticky appears.
|
|
211
|
+
6. Type your note.
|
|
212
|
+
7. Repeat for as many locations as needed. Switch colors between placements.
|
|
213
|
+
8. Click **Stop Reviewing** when finished.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Step 4 — Reviewer: export and send
|
|
218
|
+
|
|
219
|
+
1. In the panel, click **Export YAML**.
|
|
220
|
+
2. A file named `<page-title>.review.yaml` is downloaded.
|
|
221
|
+
3. Send this file back to the requester (email, Slack, PR comment attachment, etc.).
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### Step 5 — Requester: view the review
|
|
226
|
+
|
|
227
|
+
1. Open the same HTML file in a browser.
|
|
228
|
+
2. Click the **📌 button**.
|
|
229
|
+
3. Click **Import YAML** and select the received `.review.yaml` file.
|
|
230
|
+
4. The reviewer's stickies appear on the page, anchored to the same elements they were placed on.
|
|
231
|
+
|
|
232
|
+
If multiple reviewers sent back files, import them one at a time — stickies are merged by ID, so no duplicates are created. Each reviewer's name is shown in the header of their stickies.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### Handling multiple reviewers
|
|
237
|
+
|
|
238
|
+
Collect all `.review.yaml` files and import them in sequence. Because stickies are merged by `id` (UUID), importing the same file twice is safe — it updates in place rather than duplicating.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## YAML export format
|
|
243
|
+
|
|
244
|
+
```yaml
|
|
245
|
+
version: "1.0"
|
|
246
|
+
page:
|
|
247
|
+
url: "file:///path/to/page.html"
|
|
248
|
+
title: "Page Title"
|
|
249
|
+
exportedAt: "2026-05-28T10:00:00.000Z"
|
|
250
|
+
colorLabels:
|
|
251
|
+
yellow: "Note"
|
|
252
|
+
amber: "Flag"
|
|
253
|
+
# ... all 12 colors
|
|
254
|
+
stickers:
|
|
255
|
+
- id: "550e8400-e29b-41d4-a716-446655440000"
|
|
256
|
+
reviewer: "Sandor Nagy"
|
|
257
|
+
comment: "This section needs a clearer heading"
|
|
258
|
+
color: orange
|
|
259
|
+
position:
|
|
260
|
+
xPct: 0.4531 # fallback: fraction of page width
|
|
261
|
+
yPct: 0.2341 # fallback: fraction of page height
|
|
262
|
+
anchorSelector: "#intro > p:nth-of-type(2)" # element the sticky is pinned to
|
|
263
|
+
anchorOffsetXPct: 0.05 # position within that element (fraction of its width)
|
|
264
|
+
anchorOffsetYPct: 0.12 # position within that element (fraction of its height)
|
|
265
|
+
minimized: false
|
|
266
|
+
rotation: -1.2
|
|
267
|
+
createdAt: "2026-05-28T10:00:00.000Z"
|
|
268
|
+
updatedAt: "2026-05-28T10:05:00.000Z"
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Position resolution order:**
|
|
272
|
+
1. If `anchorSelector` is present and the element is found on the page → position relative to that element.
|
|
273
|
+
2. Otherwise fall back to `xPct` / `yPct` (percentage of page scroll dimensions).
|
|
274
|
+
3. Legacy files with `top` / `left` (absolute pixels from older versions) are still accepted on import.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Browser compatibility
|
|
279
|
+
|
|
280
|
+
Requires a modern browser with Shadow DOM v1 support: Chrome 53+, Firefox 63+, Safari 10.1+, Edge 79+. All features used (Shadow DOM, Pointer Events, `crypto.randomUUID`, `CSS.escape`, `URL.createObjectURL`) are available without polyfills in these versions.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";var FeedbackStickers=(()=>{var H=["yellow","amber","orange","red","rose","pink","purple","indigo","blue","teal","green","lime"],R={yellow:{bg:"#FDE047",border:"#CA8A04",text:"#000",defaultLabel:"Note"},amber:{bg:"#FCD34D",border:"#B45309",text:"#000",defaultLabel:"Flag"},orange:{bg:"#FB923C",border:"#C2410C",text:"#000",defaultLabel:"Caution"},red:{bg:"#F87171",border:"#B91C1C",text:"#fff",defaultLabel:"Blocker"},rose:{bg:"#FB7185",border:"#BE123C",text:"#fff",defaultLabel:"Critical"},pink:{bg:"#F472B6",border:"#BE185D",text:"#fff",defaultLabel:"Design"},purple:{bg:"#C084FC",border:"#7E22CE",text:"#fff",defaultLabel:"UX"},indigo:{bg:"#818CF8",border:"#3730A3",text:"#fff",defaultLabel:"Dev"},blue:{bg:"#60A5FA",border:"#1D4ED8",text:"#fff",defaultLabel:"Info"},teal:{bg:"#2DD4BF",border:"#0F766E",text:"#000",defaultLabel:"Content"},green:{bg:"#4ADE80",border:"#15803D",text:"#000",defaultLabel:"OK"},lime:{bg:"#A3E635",border:"#4D7C0F",text:"#000",defaultLabel:"Approved"}};var U="fs_",ce=U+"colorLabels",de=U+"reviewer";function ke(s){let r=2166136261;for(let a=0;a<s.length;a++)r^=s.charCodeAt(a),r=Math.imul(r,16777619)>>>0;return r.toString(36)}function pe(){return U+"s_"+ke(location.href)}function J(s,r){try{let a=localStorage.getItem(s);return a!==null?JSON.parse(a):r}catch{return r}}function G(s,r){try{localStorage.setItem(s,JSON.stringify(r))}catch{}}function V(){return J(pe(),[])}function C(s){G(pe(),s)}function fe(s){return{...s,...J(ce,{})}}function Z(s){G(ce,s)}function ue(){return J(de,"")}function me(s){G(de,s)}function w(s){return'"'+s.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"")+'"'}function ge(s){let r=['version: "1.0"',"page:",` url: ${w(s.page.url)}`,` title: ${w(s.page.title)}`,` exportedAt: ${w(s.page.exportedAt)}`,"colorLabels:",...Object.entries(s.colorLabels).map(([a,p])=>` ${a}: ${w(p)}`),"stickers:"];if(s.stickers.length===0)r.push(" []");else for(let a of s.stickers){let p=a.position;r.push(` - id: ${w(a.id)}`,` reviewer: ${w(a.reviewer)}`,` comment: ${w(a.comment)}`,` color: ${a.color}`," position:",` xPct: ${p.xPct??0}`,` yPct: ${p.yPct??0}`),p.anchorSelector&&r.push(` anchorSelector: ${w(p.anchorSelector)}`,` anchorOffsetXPct: ${p.anchorOffsetXPct??0}`,` anchorOffsetYPct: ${p.anchorOffsetYPct??0}`),r.push(` minimized: ${a.minimized}`,` rotation: ${a.rotation??0}`,` createdAt: ${w(a.createdAt)}`,` updatedAt: ${w(a.updatedAt)}`)}return r.join(`
|
|
2
|
+
`)+`
|
|
3
|
+
`}function be(s){let r={version:"1.0",page:{url:"",title:"",exportedAt:""},colorLabels:{},stickers:[]},a=s.split(`
|
|
4
|
+
`).map(f=>f.trimEnd()),p="root",n=null,b=!1;function v(f){let l=f.trim();if(l==="true")return!0;if(l==="false")return!1;let u=Number(l);return l!==""&&!isNaN(u)?u:l.startsWith("'")&&l.endsWith("'")?l.slice(1,-1).replace(/''/g,"'"):l.startsWith('"')&&l.endsWith('"')?l.slice(1,-1).replace(/\\n/g,`
|
|
5
|
+
`).replace(/\\"/g,'"').replace(/\\\\/g,"\\"):l}function P(f){let l=f.indexOf(":");return l===-1?[f.trim(),""]:[f.slice(0,l).trim(),f.slice(l+1).trimStart()]}for(let f of a){if(!f.trim()||f.trimStart().startsWith("#"))continue;let l=f.length-f.trimStart().length,u=f.trim();if(l===0){n&&(r.stickers.push(n),n=null),b=!1,u==="page:"?p="page":u==="colorLabels:"?p="colorLabels":u==="stickers:"?p="stickers":u.startsWith("version:")&&(r.version=String(v(u.slice(8))));continue}if(p==="page"&&l===2){let[x,m]=P(u);r.page[x]=String(v(m))}else if(p==="colorLabels"&&l===2){let[x,m]=P(u);r.colorLabels[x]=String(v(m))}else if(p==="stickers"){if(l===2&&u.startsWith("- ")){b=!1,n&&r.stickers.push(n),n={};let[x,m]=P(u.slice(2));n[x]=v(m)}else if(l===4&&n)if(u==="position:")b=!0,n.position={xPct:0,yPct:0};else{b=!1;let[x,m]=P(u);n[x]=v(m)}else if(l===6&&b&&n?.position){let[x,m]=P(u);n.position[x]=v(m)}}}return n&&r.stickers.push(n),r}var xe=window;xe.__fsLoaded||(xe.__fsLoaded=!0,we());function we(){let s=Object.fromEntries(H.map(e=>[e,R[e].defaultLabel])),r=!1,a=H[0],p=ue(),n=V(),b=fe(s),v=2147483640,P=document.createElement("style");P.textContent="html.fs-mode,html.fs-mode *{cursor:crosshair!important}",(document.head??document.documentElement).appendChild(P);let f=document.createElement("div");f.dataset.fsHost="";let l=f.attachShadow({mode:"open"});document.documentElement.appendChild(f);let u=document.createElement("style");u.textContent=`
|
|
6
|
+
:host{all:initial}
|
|
7
|
+
*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
8
|
+
.wrap{position:fixed;bottom:16px;right:16px;z-index:2147483647;display:flex;flex-direction:column;align-items:flex-end;gap:8px;pointer-events:none}
|
|
9
|
+
.fab{width:40px;height:40px;border-radius:50%;background:#222;color:#fff;border:none;font-size:18px;cursor:pointer;box-shadow:0 2px 12px rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;pointer-events:auto;user-select:none;transition:background .15s}
|
|
10
|
+
.fab:hover{background:#444}.fab.on{background:#FDE047;color:#000}
|
|
11
|
+
.panel{background:#fff;border-radius:10px;box-shadow:0 6px 28px rgba(0,0,0,.22);width:256px;pointer-events:auto;overflow:hidden}
|
|
12
|
+
.phead{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:#f7f7f7;border-bottom:1px solid #eee}
|
|
13
|
+
.ptitle{font-size:13px;font-weight:700;color:#111}
|
|
14
|
+
.pclose{background:none;border:none;font-size:18px;cursor:pointer;color:#aaa;line-height:1;padding:2px;display:flex;align-items:center}
|
|
15
|
+
.pclose:hover{color:#333}
|
|
16
|
+
.pbody{padding:12px;display:flex;flex-direction:column;gap:10px}
|
|
17
|
+
.lbl{font-size:9px;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:4px}
|
|
18
|
+
.ri{width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:12px;outline:none;color:#111;background:#fff}
|
|
19
|
+
.ri:focus{border-color:#888}
|
|
20
|
+
.cgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
|
|
21
|
+
.cell{display:flex;flex-direction:column;align-items:center;gap:3px;padding:4px 2px;border-radius:5px;border:2px solid transparent;cursor:pointer}
|
|
22
|
+
.cell:hover{background:rgba(0,0,0,.04)}.cell.active{border-color:#111;background:rgba(0,0,0,.05)}
|
|
23
|
+
.sdot{width:22px;height:22px;border-radius:50%;border:2px solid;flex-shrink:0}
|
|
24
|
+
.clbl{font-size:8px;color:#555;outline:none;max-width:56px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:text;text-align:center;border-bottom:1px dashed transparent;line-height:1.4}
|
|
25
|
+
.clbl:focus{border-bottom-color:#aaa;white-space:normal;overflow:visible;text-overflow:clip}
|
|
26
|
+
.mbtn{width:100%;padding:8px;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:background .15s}
|
|
27
|
+
.mbtn.off{background:#222;color:#fff}.mbtn.off:hover{background:#444}
|
|
28
|
+
.mbtn.on{background:#FDE047;color:#000}.mbtn.on:hover{background:#e8cc00}
|
|
29
|
+
.acts{display:flex;gap:5px}
|
|
30
|
+
.abtn{flex:1;padding:6px 3px;border:1px solid #ddd;border-radius:5px;background:#fff;cursor:pointer;font-size:10px;text-align:center;transition:background .1s}
|
|
31
|
+
.abtn:hover{background:#f0f0f0}.abtn.d:hover{background:#fee;border-color:#e88;color:#c00}
|
|
32
|
+
.stat{text-align:center;font-size:10px;color:#bbb}
|
|
33
|
+
`,l.appendChild(u);let x=document.createElement("div");x.className="wrap",l.appendChild(x);let m=document.createElement("div");m.className="panel",m.style.display="none",m.innerHTML=`
|
|
34
|
+
<div class="phead">
|
|
35
|
+
<span class="ptitle">\u{1F4CC} Feedback Stickers</span>
|
|
36
|
+
<button class="pclose" id="pc">\xD7</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="pbody">
|
|
39
|
+
<div>
|
|
40
|
+
<div class="lbl">Reviewer</div>
|
|
41
|
+
<input class="ri" id="ri" type="text" placeholder="Your name\u2026" maxlength="20" />
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<div class="lbl">Color & Label</div>
|
|
45
|
+
<div class="cgrid" id="cg"></div>
|
|
46
|
+
</div>
|
|
47
|
+
<button class="mbtn off" id="mb">Start Reviewing</button>
|
|
48
|
+
<div>
|
|
49
|
+
<div class="lbl">Annotations</div>
|
|
50
|
+
<div class="acts">
|
|
51
|
+
<button class="abtn" id="eb">Export YAML</button>
|
|
52
|
+
<button class="abtn" id="ib">Import YAML</button>
|
|
53
|
+
<button class="abtn d" id="cb">Clear</button>
|
|
54
|
+
</div>
|
|
55
|
+
<input type="file" id="if" accept=".yaml,.yml" style="display:none" />
|
|
56
|
+
</div>
|
|
57
|
+
<div class="stat" id="st">No stickers</div>
|
|
58
|
+
</div>
|
|
59
|
+
`;let E=document.createElement("button");E.className="fab",E.textContent="\u{1F4CC}",E.title="Feedback Stickers",x.appendChild(m),x.appendChild(E);let L=e=>l.querySelector(e),K=L("#ri"),M=L("#cg"),F=L("#mb"),he=L("#st"),Y=L("#if");K.value=p;function Q(){M.innerHTML="";for(let e of H){let t=R[e],o=document.createElement("div");o.className="cell"+(e===a?" active":""),o.dataset.color=e,o.innerHTML=`<div class="sdot" style="background:${t.bg};border-color:${t.border}"></div><div class="clbl" contenteditable="true" data-label="${e}">${b[e]}</div>`,M.appendChild(o)}}Q();function z(){let e=n.length;he.textContent=e===0?"No stickers":e===1?"1 sticker":`${e} stickers`}E.addEventListener("click",()=>{let e=m.style.display!=="none";m.style.display=e?"none":"",E.classList.toggle("on",!e)}),L("#pc").addEventListener("click",()=>{m.style.display="none",E.classList.remove("on")}),K.addEventListener("input",W(()=>{p=K.value.trim()||"User",me(p)},300)),M.addEventListener("click",e=>{let t=e.target;if(t.closest("[contenteditable]"))return;let o=t.closest("[data-color]");if(!o)return;let i=o.dataset.color;!i||!(i in R)||(a=i,M.querySelectorAll(".cell").forEach(h=>h.classList.remove("active")),o.classList.add("active"))}),M.addEventListener("blur",e=>{let t=e.target,o=t.dataset.label;if(!o)return;let i=t.textContent?.trim()||R[o].defaultLabel;t.textContent=i,b[o]=i,Z(b)},!0),M.addEventListener("keydown",e=>{e.key==="Enter"&&(e.target.blur(),e.preventDefault())}),F.addEventListener("click",()=>{r=!r,document.documentElement.classList.toggle("fs-mode",r),r?document.addEventListener("click",j,!0):document.removeEventListener("click",j,!0),F.textContent=r?"Stop Reviewing":"Start Reviewing",F.className="mbtn "+(r?"on":"off"),E.classList.toggle("on",r)}),L("#eb").addEventListener("click",()=>{let e={version:"1.0",page:{url:location.href,title:document.title||location.pathname,exportedAt:new Date().toISOString()},colorLabels:{...b},stickers:[...n]},t=(document.title||"page").replace(/[^a-z0-9_-]/gi,"_")+".review.yaml";Ee(ge(e),t,"text/yaml")}),L("#ib").addEventListener("click",()=>Y.click()),Y.addEventListener("change",()=>{let e=Y.files?.[0];if(!e)return;let t=new FileReader;t.onload=()=>{try{let o=be(t.result);if(!Array.isArray(o.stickers))throw new Error("bad format");for(let i of o.stickers){let h=n.findIndex(D=>D.id===i.id);h>=0?(n[h]=i,document.querySelector(`[data-sticker-id="${i.id}"]`)?.remove()):n.push(i),I(i)}o.colorLabels&&(Object.assign(b,o.colorLabels),Z(b),Q()),C(n),z()}catch{alert("Import failed: invalid YAML format")}},t.readAsText(e),Y.value=""}),L("#cb").addEventListener("click",()=>{confirm("Clear all stickers on this page?")&&(n.length=0,document.querySelectorAll("[data-sticker-id]").forEach(e=>e.remove()),C(n),z())});function I(e){let t=R[e.color]??R[H[0]],o=b[e.color]||t.defaultLabel,i=(e.reviewer||"U").slice(0,3),h=e.rotation??0,D=new Date(e.createdAt).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),d=document.createElement("div");d.dataset.fsHost="",d.dataset.stickerId=e.id;let{top:T,left:N}=te(e);d.style.cssText=`position:absolute;left:${N}px;top:${T}px;z-index:${v++}`;let _=d.attachShadow({mode:"open"}),y=document.createElement("style");y.textContent=`
|
|
60
|
+
:host{all:initial;cursor:default!important}
|
|
61
|
+
*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
62
|
+
.dot{width:32px;height:32px;border-radius:50%;background:${t.bg};border:2px solid ${t.border};
|
|
63
|
+
box-shadow:1px 2px 6px rgba(0,0,0,.3);align-items:center;justify-content:center;
|
|
64
|
+
font-size:10px;font-weight:800;color:${t.text};cursor:pointer;user-select:none}
|
|
65
|
+
.card{width:210px;background:${t.bg}ee;border:2px solid ${t.border};border-radius:6px;
|
|
66
|
+
box-shadow:2px 3px 10px rgba(0,0,0,.25);transform:rotate(${h}deg);overflow:hidden;display:flex;flex-direction:column}
|
|
67
|
+
.hdr{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;
|
|
68
|
+
background:${t.border};color:${t.text};cursor:grab;user-select:none;font-size:11px;font-weight:700;gap:4px}
|
|
69
|
+
.hdr:active{cursor:grabbing}
|
|
70
|
+
.tag{font-size:9px;padding:1px 4px;border-radius:3px;background:rgba(255,255,255,.25);
|
|
71
|
+
text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
|
|
72
|
+
.btns{display:flex;gap:2px;flex-shrink:0}
|
|
73
|
+
.btn{background:none;border:none;color:${t.text};cursor:pointer;font-size:14px;width:20px;
|
|
74
|
+
height:20px;line-height:20px;text-align:center;border-radius:3px;opacity:.7}
|
|
75
|
+
.btn:hover{opacity:1;background:rgba(255,255,255,.2)}
|
|
76
|
+
.body{padding:7px}
|
|
77
|
+
textarea{width:100%;min-height:45px;border:none;background:transparent;resize:vertical;
|
|
78
|
+
font-size:11px;line-height:1.4;color:#1a1a1a;outline:none;font-family:inherit}
|
|
79
|
+
textarea::placeholder{color:rgba(0,0,0,.4)}
|
|
80
|
+
.foot{padding:2px 8px 4px;font-size:9px;color:rgba(0,0,0,.45);text-align:right}
|
|
81
|
+
`;let O=document.createElement("div");O.className="dot",O.textContent=i,O.style.display=e.minimized?"flex":"none";let k=document.createElement("div");k.className="card",k.style.display=e.minimized?"none":"flex",k.innerHTML=`
|
|
82
|
+
<div class="hdr" id="hdr">
|
|
83
|
+
<span style="display:flex;align-items:center;gap:5px;overflow:hidden;min-width:0">
|
|
84
|
+
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${e.reviewer}</span>
|
|
85
|
+
<span class="tag">${o}</span>
|
|
86
|
+
</span>
|
|
87
|
+
<span class="btns">
|
|
88
|
+
<button class="btn" id="mn" title="Minimize">\u2212</button>
|
|
89
|
+
<button class="btn" id="dl" title="Delete">\xD7</button>
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="body"><textarea placeholder="Add note\u2026">${e.comment}</textarea></div>
|
|
93
|
+
<div class="foot">${D}</div>
|
|
94
|
+
`,_.append(y,O,k);let ye=k.querySelector("#hdr"),ne=!1,oe=0,re=0,ie=0,se=0;function ae(c){d.style.left=Math.max(0,ie+c.pageX-oe)+"px",d.style.top=Math.max(0,se+c.pageY-re)+"px"}function le(){ne=!1,document.removeEventListener("pointermove",ae,!0),document.removeEventListener("pointerup",le,!0);let c=n.find(A=>A.id===e.id);if(!c)return;let g=parseFloat(d.style.left),S=parseFloat(d.style.top),q={xPct:$(g/document.documentElement.scrollWidth),yPct:$(S/document.documentElement.scrollHeight)};d.style.display="none";let X=document.elementFromPoint(g-window.scrollX,S-window.scrollY);if(d.style.display="",X&&X.dataset?.fsHost===void 0){let A=X.getBoundingClientRect();A.width>0&&A.height>0&&(q.anchorSelector=ee(X),q.anchorOffsetXPct=$((g-window.scrollX-A.left)/A.width),q.anchorOffsetYPct=$((S-window.scrollY-A.top)/A.height))}c.position=q,c.updatedAt=new Date().toISOString(),C(n)}ye.addEventListener("pointerdown",c=>{c.stopPropagation(),c.preventDefault(),ne=!0,oe=c.pageX,re=c.pageY,ie=parseFloat(d.style.left),se=parseFloat(d.style.top),v++,d.style.zIndex=String(v),document.addEventListener("pointermove",ae,!0),document.addEventListener("pointerup",le,!0)}),k.querySelector("#mn").addEventListener("click",c=>{c.stopPropagation(),k.style.display="none",O.style.display="flex";let g=n.find(S=>S.id===e.id);g&&(g.minimized=!0,g.updatedAt=new Date().toISOString(),C(n))}),O.addEventListener("click",c=>{c.stopPropagation(),O.style.display="none",k.style.display="flex",v++,d.style.zIndex=String(v);let g=n.find(S=>S.id===e.id);g&&(g.minimized=!1,g.updatedAt=new Date().toISOString(),C(n))}),k.querySelector("#dl").addEventListener("click",c=>{c.stopPropagation(),d.remove();let g=n.findIndex(S=>S.id===e.id);g>=0&&n.splice(g,1),C(n),z()});let B=k.querySelector("textarea");B.addEventListener("input",W(()=>{let c=n.find(g=>g.id===e.id);c&&(c.comment=B.value,c.updatedAt=new Date().toISOString(),C(n))},500)),B.addEventListener("pointerdown",c=>c.stopPropagation()),B.addEventListener("click",c=>c.stopPropagation()),document.documentElement.appendChild(d)}function j(e){if(!r||e.composedPath().some(y=>y.dataset?.fsHost!==void 0))return;e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();let o=document.documentElement.scrollWidth,i=document.documentElement.scrollHeight,h=e.pageX-10,D=e.pageY-10,d={xPct:$(h/o),yPct:$(D/i)},T=document.elementFromPoint(e.clientX,e.clientY);if(T&&T.dataset?.fsHost===void 0){let y=T.getBoundingClientRect();y.width>0&&y.height>0&&(d.anchorSelector=ee(T),d.anchorOffsetXPct=$((e.clientX-10-y.left)/y.width),d.anchorOffsetYPct=$((e.clientY-10-y.top)/y.height))}let N=new Date().toISOString(),_={id:crypto.randomUUID(),reviewer:p,comment:"",color:a,position:d,minimized:!1,rotation:parseFloat((Math.random()*4-2).toFixed(1)),createdAt:N,updatedAt:N};n.push(_),I(_),C(n),z()}function W(e,t){let o;return(...i)=>{clearTimeout(o),o=setTimeout(()=>e(...i),t)}}function $(e){return Math.round(e*1e4)/1e4}function ee(e){let t=[],o=e;for(;o&&o.tagName!=="HTML";){if(o.id){t.unshift("#"+CSS.escape(o.id));break}let i=o.tagName.toLowerCase(),h=o.parentElement;if(h){let D=o.tagName,d=Array.from(h.children).filter(T=>T.tagName===D);t.unshift(d.length>1?`${i}:nth-of-type(${d.indexOf(o)+1})`:i)}else t.unshift(i);if(o=h,t.length>=6)break}return t.join(" > ")}function te(e){let t=e.position;if(t.anchorSelector)try{let o=document.querySelector(t.anchorSelector);if(o){let i=o.getBoundingClientRect();if(i.width>0||i.height>0)return{left:i.left+window.scrollX+(t.anchorOffsetXPct??0)*i.width,top:i.top+window.scrollY+(t.anchorOffsetYPct??0)*i.height}}}catch{}return t.xPct!=null?{left:t.xPct*document.documentElement.scrollWidth,top:t.yPct*document.documentElement.scrollHeight}:{top:t.top??0,left:t.left??0}}function ve(){r&&(r=!1,document.documentElement.classList.remove("fs-mode"),document.removeEventListener("click",j,!0),F.textContent="Start Reviewing",F.className="mbtn off",E.classList.remove("on")),document.querySelectorAll("[data-sticker-id]").forEach(e=>e.remove()),n=V(),n.forEach(I),z()}window.__feedbackStickers={refresh:ve},n.forEach(I),z(),window.addEventListener("resize",W(()=>{document.querySelectorAll("[data-sticker-id]").forEach(e=>{let t=n.find(h=>h.id===e.dataset.stickerId);if(!t)return;let{top:o,left:i}=te(t);e.style.top=o+"px",e.style.left=i+"px"})},150))}function Ee(s,r,a){let p=new Blob([s],{type:a}),n=URL.createObjectURL(p);Object.assign(document.createElement("a"),{href:n,download:r}).click(),URL.revokeObjectURL(n)}})();
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gridd/feedback-stickers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Drop-in sticky-note annotation panel for any HTML page. Self-contained IIFE — no framework required.",
|
|
5
|
+
"keywords": ["feedback", "review", "sticky", "annotation", "stickers", "docusaurus"],
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"main": "dist/feedback-stickers.min.js",
|
|
8
|
+
"jsdelivr": "dist/feedback-stickers.min.js",
|
|
9
|
+
"unpkg": "dist/feedback-stickers.min.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/feedback-stickers.min.js"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "esbuild src/index.ts --bundle --format=iife --global-name=FeedbackStickers --minify --target=es2020 --outfile=dist/feedback-stickers.min.js",
|
|
15
|
+
"dev": "esbuild src/index.ts --bundle --format=iife --global-name=FeedbackStickers --target=es2020 --sourcemap --outfile=dist/feedback-stickers.js --watch"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"esbuild": "^0.21.0",
|
|
19
|
+
"typescript": "^5.4.0"
|
|
20
|
+
}
|
|
21
|
+
}
|