visual-feedback 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.
- package/AGENTS.md +12 -7
- package/CHANGELOG.md +110 -0
- package/README.md +5 -3
- package/dist/dialog-W6m1knfu.mjs +1382 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/keep-BG5WxZPU.mjs +74 -0
- package/dist/pdf-Gj-ZO9W5.mjs +85 -0
- package/dist/react.d.mts +1 -1
- package/dist/react.mjs +1 -1
- package/dist/{report-BxonlIdW.mjs → report-BwLr1sYt.mjs} +45 -21
- package/dist/{report-CIgy7uzP.d.mts → report-CaJegYyg.d.mts} +30 -16
- package/dist/{report-BRZX6sWL.mjs → report-CnspEsHf.mjs} +27 -15
- package/dist/server.d.mts +2 -3
- package/dist/server.mjs +22 -13
- package/dist/widget-DvGTo0B4.mjs +565 -0
- package/dist/{widget-Ca1heF8n.d.mts → widget-Z1KOUKjx.d.mts} +14 -4
- package/docs/API.md +59 -31
- package/docs/SPEC.md +122 -30
- package/docs/THEMING.md +50 -19
- package/docs/recipes/github-issues.md +44 -20
- package/docs/recipes/local.md +17 -13
- package/docs/recipes/postgres.md +42 -20
- package/docs/recipes/s3.md +15 -10
- package/docs/recipes/supabase.md +18 -18
- package/docs/recipes/vercel-blob.md +18 -12
- package/package.json +6 -3
- package/dist/dialog-BTY6FKLo.mjs +0 -485
- package/dist/widget-CVnusNlY.mjs +0 -278
package/dist/widget-CVnusNlY.mjs
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
//#region src/widget.ts
|
|
3
|
-
const SVG = "http://www.w3.org/2000/svg";
|
|
4
|
-
function supported() {
|
|
5
|
-
return typeof document !== "undefined" && "adoptedStyleSheets" in Document.prototype && "adoptedStyleSheets" in ShadowRoot.prototype && CSS.supports("color", "color-mix(in srgb, red, red)");
|
|
6
|
-
}
|
|
7
|
-
function sheet(text) {
|
|
8
|
-
const result = new CSSStyleSheet();
|
|
9
|
-
result.replaceSync(text);
|
|
10
|
-
return result;
|
|
11
|
-
}
|
|
12
|
-
function variables() {
|
|
13
|
-
return `.vf{--_r:var(--vf-radius,12px);--_s:var(--vf-surface,Canvas);--_t:var(--vf-text,CanvasText);--_m:var(--vf-muted,color-mix(in srgb,var(--_t) 60%,transparent));--_b:var(--vf-border,color-mix(in srgb,var(--_t) 15%,transparent));--_a:var(--vf-accent,${CSS.supports("color", "AccentColor") ? "AccentColor" : "#0b57d0"});--_at:var(--vf-accent-text,#fff);--_f:var(--vf-focus,var(--_a));--_mark:var(--vf-mark,#e5243b);--_bd:var(--vf-backdrop,rgb(0 0 0/45%));--_sh:var(--vf-shadow,0 4px 16px rgb(0 0 0/12%));--_size:var(--vf-trigger-size,36px);--_off:var(--vf-offset,16px);--_z:var(--vf-z,2147483000)}`;
|
|
14
|
-
}
|
|
15
|
-
const LAYOUT = `.vf{display:contents;font-family:var(--vf-font,inherit);color-scheme:light dark}.trigger{position:fixed;z-index:var(--_z);box-sizing:border-box;width:var(--_size);height:var(--_size);margin:0;padding:0;display:grid;place-items:center}.trigger[data-y=bottom]{bottom:calc(var(--_off) + env(safe-area-inset-bottom,0px))}.trigger[data-y=top]{top:calc(var(--_off) + env(safe-area-inset-top,0px))}.trigger[data-x=right]{right:calc(var(--_off) + env(safe-area-inset-right,0px))}.trigger[data-x=left]{left:calc(var(--_off) + env(safe-area-inset-left,0px))}.trigger svg{width:55%;height:55%}.capturing,.capturing .backdrop{visibility:hidden}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}`;
|
|
16
|
-
const THEME = `.trigger{border:1px solid var(--_b);border-radius:50%;background:var(--_s);color:var(--_t);box-shadow:var(--_sh);cursor:pointer;transition:background-color .15s}.trigger:hover{background:color-mix(in srgb,var(--_s) 90%,var(--_t))}:focus-visible{outline:2px solid var(--_f);outline-offset:2px}`;
|
|
17
|
-
function icon() {
|
|
18
|
-
const svg = document.createElementNS(SVG, "svg");
|
|
19
|
-
const path = document.createElementNS(SVG, "path");
|
|
20
|
-
for (const [name, value] of Object.entries({
|
|
21
|
-
viewBox: "0 0 24 24",
|
|
22
|
-
"aria-hidden": "true",
|
|
23
|
-
fill: "none",
|
|
24
|
-
stroke: "currentColor",
|
|
25
|
-
"stroke-width": "2",
|
|
26
|
-
"stroke-linejoin": "round",
|
|
27
|
-
part: "trigger-icon"
|
|
28
|
-
})) svg.setAttribute(name, value);
|
|
29
|
-
path.setAttribute("d", "M21 12a8 8 0 0 1-11.6 7.1L4 20l1.1-4.4A8 8 0 1 1 21 12z");
|
|
30
|
-
svg.append(path);
|
|
31
|
-
return svg;
|
|
32
|
-
}
|
|
33
|
-
function field(element) {
|
|
34
|
-
return element.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element.tagName);
|
|
35
|
-
}
|
|
36
|
-
function deepActive() {
|
|
37
|
-
let active = document.activeElement;
|
|
38
|
-
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
39
|
-
return active;
|
|
40
|
-
}
|
|
41
|
-
const DISCONNECTED = Symbol.for("visual-feedback.disconnected");
|
|
42
|
-
const noop = {
|
|
43
|
-
open: async () => {},
|
|
44
|
-
close() {},
|
|
45
|
-
destroy() {},
|
|
46
|
-
update() {},
|
|
47
|
-
on: () => () => {}
|
|
48
|
-
};
|
|
49
|
-
function createFeedback(options = {}) {
|
|
50
|
-
if (!supported()) return noop;
|
|
51
|
-
const tag = options.tagName ?? "visual-feedback";
|
|
52
|
-
if (!customElements.get(tag)) customElements.define(tag, class extends HTMLElement {
|
|
53
|
-
disconnectedCallback() {
|
|
54
|
-
this[DISCONNECTED]?.();
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
const host = document.createElement(tag);
|
|
58
|
-
const root = host.attachShadow({ mode: "open" });
|
|
59
|
-
const wrapper = document.createElement("div");
|
|
60
|
-
wrapper.className = "vf";
|
|
61
|
-
root.append(wrapper);
|
|
62
|
-
let view;
|
|
63
|
-
const handlers = /* @__PURE__ */ new Map();
|
|
64
|
-
const call = (fns, args) => {
|
|
65
|
-
for (const fn of [...fns]) try {
|
|
66
|
-
fn(...args);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
reportError(error);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const emit = (event, ...args) => call(handlers.get(event) ?? [], args);
|
|
72
|
-
const start = () => void widget.open().catch(() => {});
|
|
73
|
-
let state = "idle";
|
|
74
|
-
let dialog;
|
|
75
|
-
let session = 0;
|
|
76
|
-
let returnFocus = null;
|
|
77
|
-
let pending;
|
|
78
|
-
let trigger;
|
|
79
|
-
let unlisten;
|
|
80
|
-
function listen(shortcut) {
|
|
81
|
-
const parts = shortcut.toLowerCase().split("+");
|
|
82
|
-
const key = parts.pop();
|
|
83
|
-
const names = [
|
|
84
|
-
"alt",
|
|
85
|
-
"shift",
|
|
86
|
-
"control",
|
|
87
|
-
"meta"
|
|
88
|
-
];
|
|
89
|
-
const mods = names.map((name) => parts.includes(name)).join();
|
|
90
|
-
const typing = !/control|meta/.test(shortcut.toLowerCase());
|
|
91
|
-
const on = (event) => {
|
|
92
|
-
if (event instanceof KeyboardEvent && state === "idle" && !event.repeat && !event.isComposing && !event.getModifierState("AltGraph") && parts.every((part) => names.includes(part)) && [
|
|
93
|
-
event.altKey,
|
|
94
|
-
event.shiftKey,
|
|
95
|
-
event.ctrlKey,
|
|
96
|
-
event.metaKey
|
|
97
|
-
].join() === mods && [event.code.replace(/^(Key|Digit)/, ""), event.key].some((name) => name.toLowerCase() === key) && !(typing && field(event.composedPath()[0]))) {
|
|
98
|
-
event.preventDefault();
|
|
99
|
-
start();
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
addEventListener("keydown", on);
|
|
103
|
-
return () => removeEventListener("keydown", on);
|
|
104
|
-
}
|
|
105
|
-
let base = [];
|
|
106
|
-
let extra = [];
|
|
107
|
-
function sheets() {
|
|
108
|
-
const dialogCss = view?.css(Boolean(options.unstyled)).map(sheet) ?? [];
|
|
109
|
-
root.adoptedStyleSheets = [
|
|
110
|
-
...base,
|
|
111
|
-
...dialogCss,
|
|
112
|
-
...extra
|
|
113
|
-
];
|
|
114
|
-
}
|
|
115
|
-
function apply() {
|
|
116
|
-
wrapper.style.colorScheme = options.theme?.replace("auto", "") ?? "";
|
|
117
|
-
base = [sheet(variables() + LAYOUT), ...options.unstyled ? [] : [sheet(THEME)]];
|
|
118
|
-
extra = options.css ? [sheet(options.css)] : [];
|
|
119
|
-
sheets();
|
|
120
|
-
if (options.trigger === "none") {
|
|
121
|
-
trigger?.remove();
|
|
122
|
-
trigger = void 0;
|
|
123
|
-
} else {
|
|
124
|
-
if (!trigger) {
|
|
125
|
-
trigger = document.createElement("button");
|
|
126
|
-
trigger.type = "button";
|
|
127
|
-
trigger.className = "trigger";
|
|
128
|
-
trigger.setAttribute("part", "trigger");
|
|
129
|
-
trigger.append(icon());
|
|
130
|
-
trigger.addEventListener("click", start);
|
|
131
|
-
wrapper.prepend(trigger);
|
|
132
|
-
}
|
|
133
|
-
const [y, x] = (options.position ?? "bottom-right").split("-");
|
|
134
|
-
trigger.dataset.y = y;
|
|
135
|
-
trigger.dataset.x = x;
|
|
136
|
-
trigger.setAttribute("aria-label", options.strings?.trigger ?? "Send feedback");
|
|
137
|
-
}
|
|
138
|
-
unlisten?.();
|
|
139
|
-
unlisten = options.shortcut ? listen(options.shortcut) : void 0;
|
|
140
|
-
}
|
|
141
|
-
apply();
|
|
142
|
-
(options.container ?? document.body ?? document.documentElement).append(host);
|
|
143
|
-
function hide(on) {
|
|
144
|
-
wrapper.classList.toggle("capturing", on);
|
|
145
|
-
}
|
|
146
|
-
function requestDisplay() {
|
|
147
|
-
const media = navigator.mediaDevices;
|
|
148
|
-
if (options.capture?.mode !== "display" || typeof media?.getDisplayMedia !== "function") return;
|
|
149
|
-
hide(true);
|
|
150
|
-
const stream = media.getDisplayMedia({ video: true });
|
|
151
|
-
stream.catch(() => {});
|
|
152
|
-
return stream;
|
|
153
|
-
}
|
|
154
|
-
function stop(stream) {
|
|
155
|
-
stream?.then((media) => {
|
|
156
|
-
for (const track of media.getTracks()) track.stop();
|
|
157
|
-
}, () => {});
|
|
158
|
-
}
|
|
159
|
-
async function grab(module, mine, stream) {
|
|
160
|
-
hide(true);
|
|
161
|
-
const quiet = view?.announce(root, options.strings);
|
|
162
|
-
try {
|
|
163
|
-
return await module.capture(options.capture, stream);
|
|
164
|
-
} catch {
|
|
165
|
-
return null;
|
|
166
|
-
} finally {
|
|
167
|
-
quiet?.();
|
|
168
|
-
if (mine === session) hide(false);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function cancel() {
|
|
172
|
-
session++;
|
|
173
|
-
hide(false);
|
|
174
|
-
}
|
|
175
|
-
function teardown() {
|
|
176
|
-
dialog?.unmount();
|
|
177
|
-
dialog = void 0;
|
|
178
|
-
const target = returnFocus?.isConnected && returnFocus !== document.body ? returnFocus : trigger;
|
|
179
|
-
returnFocus = null;
|
|
180
|
-
target?.focus?.();
|
|
181
|
-
}
|
|
182
|
-
async function show(mine) {
|
|
183
|
-
returnFocus = deepActive();
|
|
184
|
-
const stream = requestDisplay();
|
|
185
|
-
try {
|
|
186
|
-
const [loaded, capture] = await Promise.all([import("./dialog-BTY6FKLo.mjs"), import("./capture-CufRosj3.mjs")]);
|
|
187
|
-
if (mine !== session) return stop(stream);
|
|
188
|
-
view = loaded;
|
|
189
|
-
sheets();
|
|
190
|
-
const image = await grab(capture, mine, stream);
|
|
191
|
-
if (mine !== session) return;
|
|
192
|
-
if (!host.isConnected) {
|
|
193
|
-
state = "idle";
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
dialog = view.mount(wrapper, {
|
|
197
|
-
close: () => widget.close(),
|
|
198
|
-
retry: () => void retry(capture).catch((error) => {
|
|
199
|
-
widget.close();
|
|
200
|
-
emit("error", error);
|
|
201
|
-
}),
|
|
202
|
-
emit,
|
|
203
|
-
options: () => options
|
|
204
|
-
}, options);
|
|
205
|
-
dialog.capture(image);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
stop(stream);
|
|
208
|
-
if (mine === session) {
|
|
209
|
-
state = "idle";
|
|
210
|
-
hide(false);
|
|
211
|
-
teardown();
|
|
212
|
-
}
|
|
213
|
-
emit("error", error);
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
state = "open";
|
|
217
|
-
emit("open");
|
|
218
|
-
}
|
|
219
|
-
async function retry(capture) {
|
|
220
|
-
if (wrapper.classList.contains("capturing")) return;
|
|
221
|
-
const mine = session;
|
|
222
|
-
dialog?.lock(false);
|
|
223
|
-
const image = await grab(capture, mine, requestDisplay());
|
|
224
|
-
if (mine !== session) return;
|
|
225
|
-
dialog?.capture(image);
|
|
226
|
-
}
|
|
227
|
-
const widget = {
|
|
228
|
-
open() {
|
|
229
|
-
if (state === "opening" && pending) return pending;
|
|
230
|
-
if (state !== "idle") return Promise.resolve();
|
|
231
|
-
state = "opening";
|
|
232
|
-
pending = show(++session);
|
|
233
|
-
return pending;
|
|
234
|
-
},
|
|
235
|
-
close() {
|
|
236
|
-
if (state !== "open" && state !== "opening") return;
|
|
237
|
-
const wasOpen = state === "open";
|
|
238
|
-
state = "idle";
|
|
239
|
-
cancel();
|
|
240
|
-
teardown();
|
|
241
|
-
if (wasOpen) emit("close");
|
|
242
|
-
},
|
|
243
|
-
destroy() {
|
|
244
|
-
if (state === "destroyed") return;
|
|
245
|
-
const wasOpen = state === "open";
|
|
246
|
-
state = "destroyed";
|
|
247
|
-
cancel();
|
|
248
|
-
unlisten?.();
|
|
249
|
-
if (wasOpen) teardown();
|
|
250
|
-
const closers = wasOpen ? [...handlers.get("close") ?? []] : [];
|
|
251
|
-
handlers.clear();
|
|
252
|
-
host.remove();
|
|
253
|
-
call(closers, []);
|
|
254
|
-
},
|
|
255
|
-
update(next) {
|
|
256
|
-
if (state === "destroyed") return;
|
|
257
|
-
options = {
|
|
258
|
-
...options,
|
|
259
|
-
...next
|
|
260
|
-
};
|
|
261
|
-
apply();
|
|
262
|
-
},
|
|
263
|
-
on(event, fn) {
|
|
264
|
-
if (state === "destroyed") return () => {};
|
|
265
|
-
const set = handlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
266
|
-
handlers.set(event, set);
|
|
267
|
-
const handler = fn;
|
|
268
|
-
set.add(handler);
|
|
269
|
-
return () => set.delete(handler);
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
host[DISCONNECTED] = () => queueMicrotask(() => {
|
|
273
|
-
if (!host.isConnected) widget.close();
|
|
274
|
-
});
|
|
275
|
-
return widget;
|
|
276
|
-
}
|
|
277
|
-
//#endregion
|
|
278
|
-
export { createFeedback as t };
|