visual-feedback 0.2.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 CHANGED
@@ -74,8 +74,9 @@ your own words.
74
74
  limit. The local folder works only in development: keep its dev server on localhost instead, since
75
75
  it checks no user.
76
76
  - **If the app has sign-in, call `discardDraft()` from `visual-feedback` at logout, with the same
77
- `tagName` if one was set.** Why: a folded or open draft, screenshots included, is kept in the tab
78
- across page loads and route changes, and would come back for the next person who signs in there.
77
+ `tagName` if one was set.** Why: a draft, screenshots included, is kept in the tab across route
78
+ changes, and across page loads as it was last folded, and would come back for the next person
79
+ who signs in there.
79
80
  - **For GitHub Issues, default to a separate private repo. Offer the code repo only when the
80
81
  developer confirms it stays private forever.** Why: images cannot be fully erased from GitHub,
81
82
  and if the code repo is made public later, every report in it is exposed.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-10-11)
4
+
5
+ - Breaking: with no `--vf-*` variables set, the widget now matches the site by itself. It takes
6
+ the page's accent (`theme-color`, else its coloured buttons, else its links), its button corners
7
+ (up to 15px) and, with `theme: "auto"`, its background and text colour, light or dark following
8
+ that background. A guess that does not contrast keeps the old default: the accent needs 3:1
9
+ against the widget's background and the text 4.5:1; the hints, the text at 60%, take more of it
10
+ where they would fall under 4.5:1. A site that wants the system colours back sets them, such as
11
+ `--vf-accent: AccentColor`; any `--vf-*` it sets still wins, and setting only one of
12
+ `--vf-surface` and `--vf-text` keeps the other at its default.
13
+ - The widget reads the page at load, at each open (a draft opened again from its bar too), when
14
+ `theme` or `css` changes, and when the page switches its theme: a change to the attributes of
15
+ `<html>` or `<body>` that changes the page's background, text colour or `color-scheme`, or a
16
+ change of the system's light or dark. The trigger, the dialog and the draft bar follow a site's
17
+ dark mode switch at once, or when the site's own colour fade ends. To see a switch it keeps one
18
+ mutation observer and one media query listener, both removed by `destroy()` (SPEC §9).
19
+ - The note shows 4 rows when the dialog opens, and the screenshot leaves room for them, so Send
20
+ stays in the window.
21
+
3
22
  ## 0.2.0 (2026-10-10)
4
23
 
5
24
  - Breaking: a report holds `screenshots`, a list of up to 10, in place of `image` and `marks`.
package/README.md CHANGED
@@ -4,7 +4,7 @@ A small button lets your users screenshot the page, mark it up and write a note.
4
4
  report and store it where you want. It has no service, no tracking and no global CSS, and it fits
5
5
  any design.
6
6
 
7
- ![The feedback dialog over a video site, with a box and an arrow drawn around a card that shows NaN views](docs/demo.png)
7
+ ![The feedback dialog over a video site, with a box and an arrow drawn around the topic buttons, seven of which read Undefined](docs/demo.png)
8
8
 
9
9
  ```sh
10
10
  npm install visual-feedback
@@ -504,7 +504,7 @@ function icon(d, part) {
504
504
  }
505
505
  //#endregion
506
506
  //#region src/dialog.ts
507
- const LAYOUT = `.backdrop{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:0;border:0;box-sizing:border-box;background:none;color:inherit;display:grid;place-items:center;overflow:auto}.backdrop:not([open]){display:none}.backdrop::backdrop{background:none}.dialog{box-sizing:border-box;width:min(560px,100% - 32px);display:grid;gap:12px}.dialog.wide{width:min(1100px,100% - 32px)}.header{display:flex;align-items:center;justify-content:space-between;gap:8px}.title{margin:0}.minimise{margin-left:auto}.body{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:start}.body>.stage:only-child{grid-column:1/-1}.stage{position:relative;display:grid;gap:8px;min-width:0}.image{display:block;max-width:100%;max-height:min(60vh,100vh - 23em);margin:0 auto;color:var(--_mark)}.toolbar+.stage>.image{touch-action:none}.selection{position:absolute;pointer-events:none;box-sizing:border-box}.text-input{position:absolute;transform:translateY(-50%);min-width:6em;margin:0;box-sizing:content-box}.toolbar{display:flex;flex-direction:column;gap:4px}.dialog svg,.draft svg{width:1.15em;height:1.15em;flex:none;stroke-linecap:round}.divider{display:block}.strip{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:8px}.thumb{position:relative;display:flex}.thumb-pick{position:relative;display:block;margin:0;padding:0;line-height:0}.thumb-image{display:block;height:3em;width:auto}.thumb-number{position:absolute;left:4px;top:4px;line-height:1.2}.thumb-remove{position:absolute;top:-.5em;right:-.5em}.add,.capture-view{display:inline-flex;align-items:center;gap:6px}.composer{display:grid;gap:8px}.note{box-sizing:border-box;width:100%;min-height:64px;resize:vertical}.footer{display:flex;align-items:center;justify-content:space-between;gap:8px}.composer-bar,.actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px}.send{display:inline-flex;align-items:center;gap:6px}.split{position:relative;display:flex;margin-left:auto}.send-more{display:inline-flex;align-items:center}.menu{position:absolute;right:0;bottom:calc(100% + 6px);z-index:1;display:grid;min-width:max-content}.menu[hidden]{display:none}.save-pdf{display:inline-flex;align-items:center;gap:8px}.draft{position:fixed;z-index:var(--_z);box-sizing:border-box;max-width:calc(100% - 2 * var(--_off));display:flex;flex-wrap:wrap;align-items:center;gap:8px}.draft-thumbs{display:flex;gap:4px}.draft-thumbs .thumb-image{height:1.75em}.drafting>.trigger{display:none}.live{position:fixed;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}`;
507
+ const LAYOUT = `.backdrop{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:0;border:0;box-sizing:border-box;background:none;color:inherit;display:grid;place-items:center;overflow:auto}.backdrop:not([open]){display:none}.backdrop::backdrop{background:none}.dialog{box-sizing:border-box;width:min(560px,100% - 32px);display:grid;gap:12px}.dialog.wide{width:min(1100px,100% - 32px)}.header{display:flex;align-items:center;justify-content:space-between;gap:8px}.title{margin:0}.minimise{margin-left:auto}.body{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:start}.body>.stage:only-child{grid-column:1/-1}.stage{position:relative;display:grid;gap:8px;min-width:0}.image{display:block;max-width:100%;max-height:min(60vh,100vh - 24.5em);margin:0 auto;color:var(--_mark)}.toolbar+.stage>.image{touch-action:none}.selection{position:absolute;pointer-events:none;box-sizing:border-box}.text-input{position:absolute;transform:translateY(-50%);min-width:6em;margin:0;box-sizing:content-box}.toolbar{display:flex;flex-direction:column;gap:4px}.dialog svg,.draft svg{width:1.15em;height:1.15em;flex:none;stroke-linecap:round}.divider{display:block}.strip{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:8px}.thumb{position:relative;display:flex}.thumb-pick{position:relative;display:block;margin:0;padding:0;line-height:0}.thumb-image{display:block;height:3em;width:auto}.thumb-number{position:absolute;left:4px;top:4px;line-height:1.2}.thumb-remove{position:absolute;top:-.5em;right:-.5em}.add,.capture-view{display:inline-flex;align-items:center;gap:6px}.composer{display:grid;gap:8px}.note{box-sizing:border-box;width:100%;min-height:64px;resize:vertical}.footer{display:flex;align-items:center;justify-content:space-between;gap:8px}.composer-bar,.actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px}.send{display:inline-flex;align-items:center;gap:6px}.split{position:relative;display:flex;margin-left:auto}.send-more{display:inline-flex;align-items:center}.menu{position:absolute;right:0;bottom:calc(100% + 6px);z-index:1;display:grid;min-width:max-content}.menu[hidden]{display:none}.save-pdf{display:inline-flex;align-items:center;gap:8px}.draft{position:fixed;z-index:var(--_z);box-sizing:border-box;max-width:calc(100% - 2 * var(--_off));display:flex;flex-wrap:wrap;align-items:center;gap:8px}.draft-thumbs{display:flex;gap:4px}.draft-thumbs .thumb-image{height:1.75em}.drafting>.trigger{display:none}.live{position:fixed;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}`;
508
508
  const THEME = `.backdrop{background:var(--_bd)}.dialog,.draft{background:var(--_s);color:var(--_t);border:1px solid var(--_b);border-radius:var(--_r);box-shadow:var(--_sh);padding:14px}.draft{padding:8px}.title{margin-left:4px;font-size:1.1em;font-weight:600}.header button,.toolbar button,.thumb-remove{font:inherit;width:2.15em;height:2.15em;padding:0;display:grid;place-items:center;border:0;border-radius:calc(var(--_r) / 2);background:none;color:var(--_m);cursor:pointer}.header button:hover,.toolbar button:hover:not(:disabled){background:color-mix(in srgb,var(--_t) 8%,transparent);color:var(--_t)}.body{padding:12px;border-radius:calc(var(--_r) * .75);background:color-mix(in srgb,var(--_t) 5%,var(--_s))}.toolbar{padding:4px;border:1px solid var(--_b);border-radius:calc(var(--_r) * .75);background:var(--_s)}.toolbar [aria-pressed=true]{background:color-mix(in srgb,var(--_a) 16%,transparent);color:var(--_a)}.toolbar button:disabled{opacity:.35;cursor:default}.divider{width:20px;margin:4px auto;border-top:1px solid var(--_b)}.image{border-radius:calc(var(--_r) / 2);box-shadow:0 0 0 1px var(--_b),0 2px 8px rgb(0 0 0/10%)}.toolbar+.stage>.image{cursor:crosshair}.selection{border:2px dashed var(--_f);border-radius:4px}.text-input{font-family:inherit;font-weight:600;line-height:1.2;color:var(--_mark);background:color-mix(in srgb,var(--_s) 85%,transparent);border:1px dashed var(--_mark);border-radius:4px;padding:0 4px;outline:none}.thumb-pick{border:2px solid transparent;border-radius:calc(var(--_r) / 2.5);background:none;overflow:hidden;cursor:pointer}.thumb-pick[aria-current=true]{border-color:var(--_a)}.thumb-image{border-radius:calc(var(--_r) / 4);box-shadow:0 0 0 1px var(--_b)}.thumb-number{font-size:.75em;font-weight:600;padding:0 4px;border-radius:4px;background:var(--_s);color:var(--_t)}.thumb-remove{width:1.5em;height:1.5em;border:1px solid var(--_b);border-radius:50%;background:var(--_s)}.label{font-size:.9em;font-weight:600;color:var(--_m)}.composer{padding:8px;border:1px solid var(--_b);border-radius:var(--_r);background:var(--_s);transition:border-color .15s,box-shadow .15s}.composer:focus-within{border-color:var(--_f);box-shadow:0 0 0 3px color-mix(in srgb,var(--_f) 22%,transparent)}.note{font:inherit;color:inherit;background:none;border:0;border-radius:calc(var(--_r) / 2);padding:4px 6px}.note:focus-visible{outline:none}.note::placeholder{color:var(--_m)}.category{font:inherit;color:inherit;background:color-mix(in srgb,var(--_t) 5%,var(--_s));border:1px solid var(--_b);border-radius:calc(var(--_r) / 2);padding:6px 8px}.send,.actions button,.add,.draft button{font:inherit;font-weight:600;height:2.25em;padding:0 .9em;border:1px solid var(--_b);border-radius:calc(var(--_r) / 1.6);background:var(--_s);color:inherit;cursor:pointer}.add{border-style:dashed;color:var(--_m)}.send,.draft .capture-view{background:var(--_a);color:var(--_at);border-color:var(--_a)}.split .send{border-radius:calc(var(--_r) / 1.6) 0 0 calc(var(--_r) / 1.6)}.send-more{font:inherit;height:2.25em;padding:0 .55em;border:1px solid var(--_a);border-left-color:color-mix(in srgb,var(--_at) 40%,var(--_a));border-radius:0 calc(var(--_r) / 1.6) calc(var(--_r) / 1.6) 0;background:var(--_a);color:var(--_at);cursor:pointer}.send-more:disabled{opacity:.6;cursor:progress}.send:where([data-ink=dark]),.send-more:where([data-ink=dark]),.draft .capture-view:where([data-ink=dark]){color:#000}.menu{padding:4px;border:1px solid var(--_b);border-radius:calc(var(--_r) / 1.6);background:var(--_s);color:var(--_t);box-shadow:var(--_sh)}.save-pdf{font:inherit;padding:.5em .8em;border:0;border-radius:calc(var(--_r) / 2.5);background:none;color:inherit;cursor:pointer;white-space:nowrap}.save-pdf:hover,.save-pdf:focus-visible{background:color-mix(in srgb,var(--_t) 8%,transparent)}.draft button:disabled{opacity:.5;cursor:default}.draft-label{margin:0 4px;font-size:.9em;font-weight:600}[aria-disabled=true]{opacity:.6;cursor:progress}.note:disabled,.category:disabled{opacity:.6}.hint{margin:0 4px;font-size:.85em;color:var(--_m)}.error{margin:0 4px;font-weight:500}.status{margin:8px 4px}`;
509
509
  const ICONS = {
510
510
  pen: "M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16v4zm9.5-13.5 4 4",
@@ -619,6 +619,7 @@ function thumbnail(image) {
619
619
  }
620
620
  function ink(button) {
621
621
  const style = getComputedStyle(button);
622
+ delete button.dataset.ink;
622
623
  if (style.getPropertyValue("--vf-accent-text").trim()) return;
623
624
  let [r = 0, g = 0, b = 0] = /^rgba?\(([\d.]+), ([\d.]+), ([\d.]+)/.exec(style.backgroundColor)?.slice(1).map(Number) ?? [];
624
625
  if (!/^rgb/.test(style.backgroundColor)) {
@@ -974,14 +975,19 @@ function mount(parent, handlers, settings = {}) {
974
975
  function reopen() {
975
976
  if (!minimised) return;
976
977
  minimised = false;
978
+ handlers.restyle();
977
979
  bar?.remove();
978
980
  bar = void 0;
979
981
  parent.append(backdrop);
980
982
  backdrop.showModal();
981
983
  lockPage(true);
982
984
  renderStrip();
985
+ reink();
983
986
  note?.focus();
984
987
  }
988
+ function reink() {
989
+ for (const accent of parent.querySelectorAll(".send, .send-more, .capture-view")) ink(accent);
990
+ }
985
991
  let seen;
986
992
  let report;
987
993
  let mounted = true;
@@ -1091,7 +1097,8 @@ function mount(parent, handlers, settings = {}) {
1091
1097
  const field = element("textarea", "note", {
1092
1098
  id: "vf-note",
1093
1099
  placeholder: text.notePlaceholder,
1094
- maxlength: String(NOTE_LIMIT)
1100
+ maxlength: String(NOTE_LIMIT),
1101
+ rows: "4"
1095
1102
  });
1096
1103
  note = field;
1097
1104
  const required = element("p", "error", {
@@ -1312,6 +1319,7 @@ function mount(parent, handlers, settings = {}) {
1312
1319
  lockPage(true);
1313
1320
  },
1314
1321
  reopen,
1322
+ reink,
1315
1323
  async revive() {
1316
1324
  reviving = true;
1317
1325
  try {
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
1
 
2
- export { t as createFeedback, n as discardDraft } from "./widget-B1JDOVI1.mjs";
2
+ export { t as createFeedback, n as discardDraft } from "./widget-DvGTo0B4.mjs";
package/dist/react.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { t as createFeedback } from "./widget-B1JDOVI1.mjs";
2
+ import { t as createFeedback } from "./widget-DvGTo0B4.mjs";
3
3
  import { useEffect, useRef } from "react";
4
4
  //#region src/react/index.ts
5
5
  function Feedback(props) {
@@ -1,4 +1,156 @@
1
1
 
2
+ //#region src/match.ts
3
+ const BUTTONS = "button,[role=\"button\"],input[type=\"submit\"],input[type=\"button\"]";
4
+ const SAMPLE = 200;
5
+ let pixel;
6
+ /** A colour as sRGB bytes and alpha; syntax other than rgb() (oklch, hex, names) goes through a canvas. */
7
+ function rgb(color) {
8
+ const match = /^rgba?\(([\d.]+)[ ,]+([\d.]+)[ ,]+([\d.]+)(?:[ ,/]+([\d.]+)(%?))?\)/.exec(color);
9
+ if (match) {
10
+ const alpha = match[4] === void 0 ? 1 : Number(match[4]) / (match[5] ? 100 : 1);
11
+ return [
12
+ Number(match[1]),
13
+ Number(match[2]),
14
+ Number(match[3]),
15
+ alpha
16
+ ];
17
+ }
18
+ if (pixel === void 0) pixel = Object.assign(document.createElement("canvas"), {
19
+ width: 1,
20
+ height: 1
21
+ }).getContext("2d", { willReadFrequently: true });
22
+ if (!pixel || !color) return void 0;
23
+ pixel.clearRect(0, 0, 1, 1);
24
+ pixel.fillStyle = "#0000";
25
+ pixel.fillStyle = color;
26
+ pixel.fillRect(0, 0, 1, 1);
27
+ const [r = 0, g = 0, b = 0, a = 0] = pixel.getImageData(0, 0, 1, 1).data;
28
+ return [
29
+ r,
30
+ g,
31
+ b,
32
+ a / 255
33
+ ];
34
+ }
35
+ function luminance([r, g, b]) {
36
+ const linear = (c) => c / 255 <= .04045 ? c / 255 / 12.92 : ((c / 255 + .055) / 1.055) ** 2.4;
37
+ return .2126 * linear(r) + .7152 * linear(g) + .0722 * linear(b);
38
+ }
39
+ function contrast(a, b) {
40
+ const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x);
41
+ return ((light ?? 0) + .05) / ((dark ?? 0) + .05);
42
+ }
43
+ const css = ([r, g, b]) => `rgb(${r} ${g} ${b})`;
44
+ const opaque = (color) => (color?.[3] ?? 0) > .99;
45
+ function vivid(color) {
46
+ if (!opaque(color)) return false;
47
+ const max = Math.max(color[0], color[1], color[2]);
48
+ const min = Math.min(color[0], color[1], color[2]);
49
+ const light = (max + min) / 510;
50
+ return (max - min) / 255 >= .2 && light > .1 && light < .9;
51
+ }
52
+ function visible(element) {
53
+ return element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden";
54
+ }
55
+ function commonest(colors) {
56
+ const counts = /* @__PURE__ */ new Map();
57
+ for (const color of colors) {
58
+ const entry = counts.get(css(color)) ?? [color, 0];
59
+ counts.set(css(color), [entry[0], entry[1] + 1]);
60
+ }
61
+ return [...counts.values()].sort((a, b) => b[1] - a[1])[0]?.[0];
62
+ }
63
+ function themeColor() {
64
+ for (const meta of document.querySelectorAll("meta[name=\"theme-color\"]")) {
65
+ const media = meta.getAttribute("media");
66
+ if (!media || matchMedia(media).matches) return rgb(meta.content.trim());
67
+ }
68
+ }
69
+ /**
70
+ * Takes the page's own accent, corners and, with `theme: "auto"`, background and text, where the
71
+ * host set no --vf-* for them. Each guess that fails contrast falls back to the defaults.
72
+ */
73
+ function match(wrapper, theme) {
74
+ const style = wrapper.style;
75
+ for (const name of [
76
+ "--_pa",
77
+ "--_pm",
78
+ "--_pr",
79
+ "--_ps",
80
+ "--_pt"
81
+ ]) style.setProperty(name, "initial");
82
+ style.colorScheme = theme?.replace("auto", "") ?? "";
83
+ if (!document.body || !wrapper.isConnected) return;
84
+ try {
85
+ guess(wrapper, theme, document.body);
86
+ } catch {}
87
+ }
88
+ function guess(wrapper, theme, body) {
89
+ const style = wrapper.style;
90
+ const own = getComputedStyle(wrapper);
91
+ const free = (variable) => !own.getPropertyValue(variable).trim();
92
+ if ((theme ?? "auto") === "auto" && free("--vf-surface") && free("--vf-text")) {
93
+ const background = [body, document.documentElement].map((element) => rgb(getComputedStyle(element).backgroundColor)).find(opaque);
94
+ const text = rgb(getComputedStyle(body).color);
95
+ if (background && opaque(text) && contrast(background, text) >= 4.5) {
96
+ style.setProperty("--_ps", css(background));
97
+ style.setProperty("--_pt", css(text));
98
+ const mixed = (share) => [
99
+ share * text[0] + (1 - share) * background[0],
100
+ share * text[1] + (1 - share) * background[1],
101
+ share * text[2] + (1 - share) * background[2],
102
+ 1
103
+ ];
104
+ let share = .6;
105
+ while (share < 1 && contrast(background, mixed(share)) < 4.5) share += .05;
106
+ if (share > .6) style.setProperty("--_pm", css(mixed(Math.min(1, share))));
107
+ style.colorScheme = luminance(background) > .179 ? "light" : "dark";
108
+ }
109
+ }
110
+ const browser = /* @__PURE__ */ new Map();
111
+ const plain = (scheme) => {
112
+ if (!browser.has(scheme)) {
113
+ const native = wrapper.appendChild(document.createElement("button"));
114
+ native.style.all = "revert";
115
+ native.style.colorScheme = scheme;
116
+ const look = getComputedStyle(native);
117
+ browser.set(scheme, `${look.backgroundColor} ${look.borderTopLeftRadius}`);
118
+ native.remove();
119
+ }
120
+ return browser.get(scheme);
121
+ };
122
+ const filled = [...document.querySelectorAll(BUTTONS)].slice(0, SAMPLE).filter(visible).map((button) => [button, getComputedStyle(button)]).filter(([, look]) => {
123
+ const face = `${look.backgroundColor} ${look.borderTopLeftRadius}`;
124
+ return face !== plain("light") && face !== plain("dark");
125
+ });
126
+ if (free("--vf-radius")) {
127
+ const shapes = filled.filter(([, look]) => opaque(rgb(look.backgroundColor))).map(([button, look]) => {
128
+ const radius = Number.parseFloat(look.borderTopLeftRadius) || 0;
129
+ return {
130
+ radius,
131
+ pill: radius * 2 >= button.getBoundingClientRect().height - 1
132
+ };
133
+ });
134
+ const corners = shapes.filter((shape) => !shape.pill).map((shape) => shape.radius);
135
+ corners.sort((a, b) => a - b);
136
+ const middle = corners[Math.floor((corners.length - 1) / 2)];
137
+ if (middle !== void 0 && corners.length * 2 > shapes.length) style.setProperty("--_pr", `${Math.min(24, middle * 1.6)}px`);
138
+ }
139
+ if (free("--vf-accent")) {
140
+ const links = () => [...document.querySelectorAll("a[href]")].slice(0, SAMPLE).filter(visible).map((link) => rgb(getComputedStyle(link).color)).filter(vivid);
141
+ const accent = [
142
+ themeColor(),
143
+ commonest(filled.map(([, look]) => rgb(look.backgroundColor)).filter(vivid)),
144
+ commonest(links())
145
+ ].find(vivid);
146
+ const probe = wrapper.appendChild(document.createElement("i"));
147
+ probe.style.background = "var(--_s)";
148
+ const surface = rgb(getComputedStyle(probe).backgroundColor);
149
+ probe.remove();
150
+ if (accent && opaque(surface) && contrast(accent, surface) >= 3) style.setProperty("--_pa", css(accent));
151
+ }
152
+ }
153
+ //#endregion
2
154
  //#region src/widget.ts
3
155
  const SVG = "http://www.w3.org/2000/svg";
4
156
  function supported() {
@@ -10,9 +162,9 @@ function sheet(text) {
10
162
  return result;
11
163
  }
12
164
  function variables() {
13
- return `.vf{--_r:var(--vf-radius,16px);--_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 12px 32px -12px rgb(0 0 0/35%));--_size:var(--vf-trigger-size,2.5em);--_off:var(--vf-offset,16px);--_z:var(--vf-z,2147483000)}`;
165
+ return `.vf{--_r:var(--vf-radius,var(--_pr,16px));--_s:var(--vf-surface,var(--_ps,Canvas));--_t:var(--vf-text,var(--_pt,CanvasText));--_m:var(--vf-muted,var(--_pm,color-mix(in srgb,var(--_t) 60%,transparent)));--_b:var(--vf-border,color-mix(in srgb,var(--_t) 15%,transparent));--_a:var(--vf-accent,var(--_pa,${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 12px 32px -12px rgb(0 0 0/35%));--_size:var(--vf-trigger-size,2.5em);--_off:var(--vf-offset,16px);--_z:var(--vf-z,2147483000)}`;
14
166
  }
15
- const LAYOUT = `.vf{display:contents;font-family:var(--vf-font,inherit);color-scheme:light dark}.trigger{font:inherit;position:fixed;z-index:var(--_z);box-sizing:border-box;height:var(--_size);margin:0;padding:0 14px 0 12px;display:flex;align-items:center;gap:8px;white-space:nowrap}.trigger[data-icon]{width:var(--_size);padding:0;justify-content:center}[data-y=bottom]{bottom:calc(var(--_off) + env(safe-area-inset-bottom,0px))}[data-y=top]{top:calc(var(--_off) + env(safe-area-inset-top,0px))}[data-x=right]{right:calc(var(--_off) + env(safe-area-inset-right,0px))}[data-x=left]{left:calc(var(--_off) + env(safe-area-inset-left,0px))}.trigger svg{width:1.15em;height:1.15em}.capturing,.capturing .backdrop{visibility:hidden}:focus-visible{outline:2px solid var(--_f);outline-offset:2px}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}`;
167
+ const LAYOUT = `.vf{display:contents;font-family:var(--vf-font,inherit);color-scheme:light dark}.trigger{font:inherit;position:fixed;z-index:var(--_z);box-sizing:border-box;height:var(--_size);margin:0;padding:0 14px 0 12px;display:flex;align-items:center;gap:8px;white-space:nowrap}.trigger[data-icon]{width:var(--_size);padding:0;justify-content:center}[data-y=bottom]{bottom:calc(var(--_off) + env(safe-area-inset-bottom,0px))}[data-y=top]{top:calc(var(--_off) + env(safe-area-inset-top,0px))}[data-x=right]{right:calc(var(--_off) + env(safe-area-inset-right,0px))}[data-x=left]{left:calc(var(--_off) + env(safe-area-inset-left,0px))}.trigger svg{width:1.15em;height:1.15em}.capturing,.capturing .backdrop{visibility:hidden}.still .trigger{transition:none!important}:focus-visible{outline:2px solid var(--_f);outline-offset:2px}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}`;
16
168
  const THEME = `.trigger{font-weight:600;border:1px solid var(--_b);border-radius:999px;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))}`;
17
169
  function icon() {
18
170
  const svg = document.createElementNS(SVG, "svg");
@@ -39,6 +191,7 @@ function deepActive() {
39
191
  return active;
40
192
  }
41
193
  const DISCONNECTED = Symbol.for("visual-feedback.disconnected");
194
+ const CONNECTED = Symbol.for("visual-feedback.connected");
42
195
  const owners = /* @__PURE__ */ new Map();
43
196
  const enders = /* @__PURE__ */ new WeakMap();
44
197
  let dialogs;
@@ -74,6 +227,9 @@ function createFeedback(options = {}) {
74
227
  if (!supported()) return noop;
75
228
  const tag = options.tagName ?? "visual-feedback";
76
229
  if (!customElements.get(tag)) customElements.define(tag, class extends HTMLElement {
230
+ connectedCallback() {
231
+ this[CONNECTED]?.();
232
+ }
77
233
  disconnectedCallback() {
78
234
  this[DISCONNECTED]?.();
79
235
  }
@@ -137,11 +293,16 @@ function createFeedback(options = {}) {
137
293
  ...extra
138
294
  ];
139
295
  }
296
+ let matched;
140
297
  function apply() {
141
- wrapper.style.colorScheme = options.theme?.replace("auto", "") ?? "";
142
298
  base = [sheet(variables() + LAYOUT), ...options.unstyled ? [] : [sheet(THEME)]];
143
299
  extra = options.css ? [sheet(options.css)] : [];
144
300
  sheets();
301
+ if (`${options.theme} ${options.css}` !== matched) {
302
+ matched = `${options.theme} ${options.css}`;
303
+ match(wrapper, options.theme);
304
+ dialog?.reink();
305
+ }
145
306
  if (options.trigger === "none") {
146
307
  trigger?.remove();
147
308
  trigger = void 0;
@@ -173,6 +334,50 @@ function createFeedback(options = {}) {
173
334
  }
174
335
  apply();
175
336
  (options.container ?? document.body ?? document.documentElement).append(host);
337
+ let seen = "";
338
+ const look = () => [document.documentElement, document.body].map((element) => {
339
+ const style = element && getComputedStyle(element);
340
+ return style ? `${style.backgroundColor} ${style.color} ${style.colorScheme}` : "";
341
+ }).join();
342
+ function restyle() {
343
+ wrapper.classList.add("still");
344
+ match(wrapper, options.theme);
345
+ if (trigger) getComputedStyle(trigger).backgroundColor;
346
+ wrapper.classList.remove("still");
347
+ dialog?.reink();
348
+ seen = look();
349
+ }
350
+ restyle();
351
+ let missed = false;
352
+ function follow(always = false) {
353
+ missed ||= always;
354
+ if (!host.isConnected) return;
355
+ const read = () => {
356
+ if (!host.isConnected || !(missed || look() !== seen)) return;
357
+ missed = false;
358
+ restyle();
359
+ };
360
+ const fading = document.getAnimations().filter((animation) => animation instanceof CSSTransition).filter((animation) => /color|background/.test(animation.transitionProperty));
361
+ if (fading.length) Promise.allSettled(fading.map((fade) => fade.finished)).then(read);
362
+ else read();
363
+ }
364
+ const system = matchMedia("(prefers-color-scheme: dark)");
365
+ const switched = () => follow(true);
366
+ system.addEventListener("change", switched);
367
+ let watched = null;
368
+ const observer = new MutationObserver(() => {
369
+ if (document.body !== watched) {
370
+ watched = document.body;
371
+ if (watched) observer.observe(watched, { attributes: true });
372
+ }
373
+ follow();
374
+ });
375
+ observer.observe(document.documentElement, {
376
+ attributes: true,
377
+ childList: true
378
+ });
379
+ watched = document.body;
380
+ if (watched) observer.observe(watched, { attributes: true });
176
381
  const keeps = () => {
177
382
  if (options.draft === "memory") return false;
178
383
  if (!owners.get(tag)?.isConnected && host.isConnected) owners.set(tag, host);
@@ -235,7 +440,7 @@ function createFeedback(options = {}) {
235
440
  returnFocus = deepActive();
236
441
  const stream = kept ? void 0 : requestDisplay();
237
442
  try {
238
- dialogs ??= import("./dialog-B57CJ4I_.mjs").catch((error) => {
443
+ dialogs ??= import("./dialog-W6m1knfu.mjs").catch((error) => {
239
444
  dialogs = void 0;
240
445
  throw error;
241
446
  });
@@ -249,6 +454,7 @@ function createFeedback(options = {}) {
249
454
  state = "idle";
250
455
  return;
251
456
  }
457
+ match(wrapper, options.theme);
252
458
  dialog = view.mount(wrapper, {
253
459
  close: () => widget.close(),
254
460
  retry: () => void retry(capture).catch((error) => {
@@ -257,7 +463,8 @@ function createFeedback(options = {}) {
257
463
  }),
258
464
  emit,
259
465
  options: () => options,
260
- keeps
466
+ keeps,
467
+ restyle: () => match(wrapper, options.theme)
261
468
  }, options);
262
469
  const mounted = dialog;
263
470
  if (!kept) mounted.capture(image);
@@ -315,6 +522,8 @@ function createFeedback(options = {}) {
315
522
  state = "destroyed";
316
523
  cancel();
317
524
  unlisten?.();
525
+ observer.disconnect();
526
+ system.removeEventListener("change", switched);
318
527
  const closers = wasOpen && !teardown(true) ? [...handlers.get("close") ?? []] : [];
319
528
  handlers.clear();
320
529
  host.remove();
@@ -346,6 +555,7 @@ function createFeedback(options = {}) {
346
555
  if (wasOpen && !stays) emit("close");
347
556
  }
348
557
  enders.set(host, () => end());
558
+ host[CONNECTED] = () => follow();
349
559
  host[DISCONNECTED] = () => queueMicrotask(() => {
350
560
  if (!host.isConnected) end(true);
351
561
  });
package/docs/API.md CHANGED
@@ -39,7 +39,7 @@ const feedback = createFeedback({
39
39
  | `headers` | `Record<string, string>` or `() => Record<string, string>` | – | Extra headers for `endpoint`. A function is called at Send; a throw shows the send error state, using the error's message. |
40
40
  | `trigger` | `"button" \| "none"` | `"button"` | `"none"` draws no trigger; the host calls `open()`. A kept draft's bar still shows. |
41
41
  | `position` | `"bottom-right" \| "bottom-left" \| "top-right" \| "top-left"` | `"bottom-right"` | Corner of the trigger. |
42
- | `shortcut` | `string \| false` | `false` | For example `"Alt+Shift+F"`: any of `Alt`, `Shift`, `Control` and `Meta`, then one key, a letter, a digit or a key name such as `F2`, joined by `+`. The modifiers must match exactly, AltGr never matches, and any other modifier name makes it match nothing. Without `Control` or `Meta` it does not fire while focus is in a text field. It does nothing while the dialog is open or a draft is kept in the draft bar. Opt-in; it is the widget's only global listener, one `keydown` on `window` (SPEC §9 names what the capture library keeps). |
42
+ | `shortcut` | `string \| false` | `false` | For example `"Alt+Shift+F"`: any of `Alt`, `Shift`, `Control` and `Meta`, then one key, a letter, a digit or a key name such as `F2`, joined by `+`. The modifiers must match exactly, AltGr never matches, and any other modifier name makes it match nothing. Without `Control` or `Meta` it does not fire while focus is in a form field (`input`, `textarea`, `select`) or editable content. It does nothing while the dialog is open or a draft is kept in the draft bar. Opt-in; it is the widget's only listener on `window` or `document`, one `keydown` on `window` (SPEC §9 names what else the widget watches, and what the capture library keeps). |
43
43
  | `tools` | `("pen" \| "box" \| "arrow" \| "text" \| "select")[]` | all five | Order is the toolbar order, and the first drawing tool is the one selected at open. Repeats and unknown names are ignored; an empty list, or Select alone, shows no toolbar. The Delete button is there only with Select. |
44
44
  | `categories` | `{ id: string; label: string }[]` | none | Shows a category picker. |
45
45
  | `requireNote` | `boolean` | `true` | |
@@ -62,9 +62,9 @@ const feedback = createFeedback({
62
62
  |---|---|
63
63
  | `open(): Promise<void>` | Starts a capture. When a draft is kept in the draft bar, or this tab still keeps one, it opens the dialog on that draft instead, once it is back; when the dialog is open already, it does nothing. When the dialog cannot load or be shown, it rejects and `error` fires. |
64
64
  | `close(): void` | Cancels and discards the draft, also when it is kept in the draft bar, and never asks first as the Cancel button can (SPEC §2); one a page load is still bringing back is only hidden, kept, with no event (SPEC §7). Before `submit` fires, nothing is sent; after it, the delivery is not stopped, and `sent` or `error` still fires when it ends. |
65
- | `destroy(): void` | Removes the element, its listeners and its stylesheet, but not what the capture library keeps (SPEC §9). A delivery still under way fires neither `sent` nor `error` after it. The draft, open or folded, stays kept, so `close` does not fire: the next widget of the same tag made in this tab brings it back folded, as after a page load, so a route change keeps it. A host that removes the element gets the same. A draft it cannot keep ends, and `close` fires as the last event: with `draft: "memory"`, a Send already handed to the host (`submit` fired), a widget that does not own its tag's draft, no screenshot (the capture error, or after "Send without screenshot"), a screenshot taken while it did not keep drafts, or sessionStorage that refuses it. To end the draft too, as at a logout, call `discardDraft()`. A draft whose IndexedDB write then fails (storage full) is lost, with no `close`: the widget is gone by then. |
65
+ | `destroy(): void` | Removes the element, its listeners, its watch of the page's theme and its stylesheet, but not what the capture library keeps (SPEC §9). A delivery still under way fires neither `sent` nor `error` after it. The draft, open or folded, stays kept, so `close` does not fire: the next widget of the same tag made in this tab brings it back folded, as after a page load, so a route change keeps it. A host that removes the element gets the same for the draft, but its listeners and its watch of the page's theme stay until `destroy()`. A draft it cannot keep ends, and `close` fires as the last event: with `draft: "memory"`, a Send already handed to the host (`submit` fired), a widget that does not own its tag's draft, no screenshot (the capture error, or after "Send without screenshot"), a screenshot taken while it did not keep drafts, or sessionStorage that refuses it. To end the draft too, as at a logout, call `discardDraft()`. A draft whose IndexedDB write then fails (storage full) is lost, with no `close`: the widget is gone by then. |
66
66
  | `on(event, fn): () => void` | Events: `open` (when a draft starts, and when a kept draft comes back), `close` (only when the draft ends: sent and closed, Cancel, Discard, `close()`, or a `destroy()` or removed element that does not keep it; folding into the draft bar, opening it again, and a `destroy()` or removal that keeps it fire none), `submit` (with the report, before each attempt), `sent` (with the report), `error` (with the error), `saved` (after Save as PDF downloads the PDF; nothing is sent). The type `FeedbackEvents` maps each event to what it passes. Returns an unsubscribe function. |
67
- | `update(options): void` | Merges these options into the current ones, key by key: a key given as `undefined` goes back to its default, and `strings` or `capture` replaces the whole object. `tagName` and `container` are ignored. The trigger, `position` (a draft bar already shown moves at its next fold), `theme`, `css`, `unstyled`, `strings.trigger`, `strings.triggerText` and `shortcut` change at once. The dialog's texts, `tools`, `categories` and `requireNote` apply from the next open, and the rest at the next capture or send. |
67
+ | `update(options): void` | Merges these options into the current ones, key by key: a key given as `undefined` goes back to its default, and `strings` or `capture` replaces the whole object. `tagName` and `container` are ignored. The trigger, `position` (a draft bar already shown moves at its next fold), `theme`, `css`, `unstyled`, `strings.trigger`, `strings.triggerText` and `shortcut` change at once, and a new `theme` or `css` reads the page's look again (THEMING.md). The dialog's texts, `tools`, `categories` and `requireNote` apply from the next open, and the rest at the next capture or send. |
68
68
 
69
69
  ## discardDraft(tagName?: string): Promise<void>
70
70
  Deletes the draft this tab keeps for a tag (default `"visual-feedback"`), as at a logout, so no
package/docs/SPEC.md CHANGED
@@ -55,8 +55,8 @@ annotating, draft --Cancel (it may ask first, below) / Discard--> idle
55
55
  speech control can say what it sees. It has no badge, and it does not move or pulse.
56
56
  - It sits 16 px from the edges by default, and it respects `env(safe-area-inset-*)`.
57
57
  - It is hidden while there is a draft, whether the dialog is open or kept in the draft bar.
58
- - It does not watch the page. The host moves it off its own content with `position` or
59
- `--vf-offset`.
58
+ - It does not move to keep clear of the page's content. The host moves it off its own content
59
+ with `position` or `--vf-offset`.
60
60
 
61
61
  ## 4. Annotating
62
62
  - **Tools:** Pen (freehand), Box (rectangle outline), Arrow and Text. Text places a label where the
@@ -148,13 +148,19 @@ Defined in `docs/API.md` (`FeedbackReport`). It holds:
148
148
  - With the keyboard alone, the user can still write the note, choose a category and send without
149
149
  marks.
150
150
  - `prefers-reduced-motion` turns off every transition.
151
- - `prefers-color-scheme` sets the default theme (`theme: "auto"`).
151
+ - With `theme: "auto"`, the page's own background sets light or dark, and `prefers-color-scheme`
152
+ where the page shows none (THEMING.md).
152
153
 
153
154
  ## 9. Budgets
154
155
  - **Loader** (the trigger and the `open()` wiring): 15 KB or less, minified and gzipped.
155
156
  - **Annotator and dialog:** 135 KB or less, gzipped. This does not include the capture library.
156
157
  - **Idle cost:** no work after the first paint of the trigger, past reading one sessionStorage
157
- key at load; only when this tab kept a draft does the dialog load, to bring it back. After the first screenshot, the
158
+ key and the page's colours and button corners at load, and reading them again when the page
159
+ switches its theme (THEMING.md section 1). To see a switch, it keeps one mutation observer on
160
+ the attributes of `<html>` and `<body>` and on the children of `<html>` (to find a new
161
+ `<body>`), and one `change` listener on `prefers-color-scheme`, both gone at `destroy()`. A
162
+ change the observer sees that leaves the page's colours alone reads the colours of `<html>` and
163
+ `<body>` and scans nothing else; a system switch always reads the page. Only when this tab kept a draft does the dialog load, to bring it back. After the first screenshot, the
158
164
  capture library (snapdom) keeps 12 listeners on `window` and `document` and up to 5 mutation observers
159
165
  until the page unloads, and offers no way to remove them; `destroy()` removes everything else
160
166
  but a draft it keeps (§7). The opt-in `shortcut` adds one `keydown` listener on `window`, gone at
@@ -168,7 +174,7 @@ Chrome 111+, Firefox 113+ and Safari 16.4+. Older browsers get no trigger and no
168
174
  ## 11. Content Security Policy
169
175
  - The widget's own CSS goes in through constructable stylesheets. It uses no `<style>` tags, and
170
176
  sets the few inline styles it needs (the selection frame, a label being typed, the theme's
171
- `color-scheme`) through the CSSOM, so a strict `style-src` does not block it. While the dialog is
177
+ `color-scheme` and the colours and corners it found on the page) through the CSSOM, so a strict `style-src` does not block it. While the dialog is
172
178
  open, the scroll lock sets `overflow` on `<html>` through the CSSOM, which a CSP allows, and
173
179
  puts back what was there on close.
174
180
  - Capture (`@zumer/snapdom`) renders through `data:` image URLs, so a site with a CSP needs
package/docs/THEMING.md CHANGED
@@ -3,11 +3,34 @@
3
3
  The widget lives in one shadow root, so the site's CSS cannot break it, and its CSS cannot leak
4
4
  out. There are four ways to adapt it, from lightest to heaviest:
5
5
 
6
- ## 1. It inherits
7
- Inside the shadow root, the widget sets `font: inherit` and `color-scheme: light dark`, so a site
8
- rule such as `* { all: unset }` cannot reset them. The defaults use the system colours `Canvas`,
9
- `CanvasText` and `AccentColor`. With no theming at all, it takes the site's font and follows light
10
- and dark mode.
6
+ ## 1. It matches the site
7
+ Inside the shadow root, the widget sets `font: inherit`, so a site rule such as
8
+ `* { all: unset }` cannot reset it. With no theming at all, it takes the site's font, and it
9
+ reads the page when it loads, at each open (a draft opened again from its bar too), when
10
+ `theme` or `css` changes, and when the page switches its theme: a change to the attributes of `<html>` or
11
+ `<body>`, such as a `dark` class, that changes the page's background, text colour or
12
+ `color-scheme`, or a change of the system's light or dark. The trigger, the dialog and the draft
13
+ bar then take the new look at once, or, where the site fades its colours, when the fade ends. A
14
+ widget whose element was off the page reads the page again when the element is put back. The rest
15
+ of the page is not watched, so a look changed only by a new stylesheet shows at the next open.
16
+ What it reads:
17
+
18
+ - **accent:** the page's `<meta name="theme-color">` (one with a `media` attribute counts only
19
+ where its media matches), else the most common coloured background
20
+ of its buttons, else the colour of its links. Greys, muted colours, near black, near white and
21
+ the browser's own unstyled buttons do not count.
22
+ - **corners:** the middle radius of the page's filled buttons, so the widget's buttons get the
23
+ same, up to 15px. A site where at least half of the filled buttons are pills keeps the
24
+ default.
25
+ - **background and text**, with `theme: "auto"`: the page's background and text colour, and light
26
+ or dark from that background.
27
+
28
+ A guess is used only when it contrasts: the accent 3:1 against the widget's background, the text
29
+ 4.5:1 against it. Otherwise the default stays: the system colours `Canvas`, `CanvasText` and
30
+ `AccentColor`, light and dark from the system, and `16px` corners. With the page's text, the
31
+ widget's hints take more of it than 60% where 60% would fall under 4.5:1. Each `--vf-*` variable
32
+ the site sets wins over the guess for that variable; a site that sets only one of `--vf-surface`
33
+ and `--vf-text` gets the other from the defaults.
11
34
 
12
35
  ## 2. CSS custom properties
13
36
  Set these on `visual-feedback` (or on any ancestor). Custom properties cross the shadow boundary.
@@ -15,12 +38,12 @@ Set these on `visual-feedback` (or on any ancestor). Custom properties cross the
15
38
  | variable | default | used for |
16
39
  |---|---|---|
17
40
  | `--vf-font` | `inherit` | font family of all text, such as `Inter, sans-serif` |
18
- | `--vf-radius` | `16px` | panel and composer, and a share of it for buttons and the image |
19
- | `--vf-surface` | `Canvas` | panel, tool rail and composer background |
20
- | `--vf-text` | `CanvasText` | text |
21
- | `--vf-muted` | text at 60% | hints, counters |
41
+ | `--vf-radius` | from the page's button corners, so the widget's buttons match them up to 15px (this is 1.6 times theirs, up to `24px`), else `16px` | panel and composer, and a share of it for buttons and the image |
42
+ | `--vf-surface` | with `theme: "auto"` the page's background, else `Canvas` | panel, tool rail and composer background |
43
+ | `--vf-text` | with `theme: "auto"` the page's text colour, else `CanvasText` | text |
44
+ | `--vf-muted` | text at 60%, more on a page whose text would make that too faint | hints, counters |
22
45
  | `--vf-border` | text at 15% | hairlines |
23
- | `--vf-accent` | `AccentColor`, or `#0b57d0` where the browser has no `AccentColor` | Send button, active tool |
46
+ | `--vf-accent` | the page's accent, else `AccentColor`, or `#0b57d0` where the browser has no `AccentColor` | Send button and its menu button, "Capture this view", active tool, chosen screenshot |
24
47
  | `--vf-accent-text` | black or white, whichever contrasts more with the accent | text on the accent |
25
48
  | `--vf-focus` | `--vf-accent` | focus ring |
26
49
  | `--vf-mark` | `#e5243b` | marks on the screenshot |
@@ -68,5 +91,6 @@ Class names used inside the shadow root are part of the public API from 1.0. Bef
68
91
  change.
69
92
 
70
93
  ## Theme
71
- `theme: "light" | "dark"` sets `color-scheme` inside the shadow root, which forces that scheme.
72
- `"auto"` follows the system.
94
+ `theme: "light" | "dark"` sets `color-scheme` inside the shadow root, which forces that scheme
95
+ with the system colours. `"auto"` follows the page's background (section 1), and the system
96
+ where the page shows none.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visual-feedback",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A small button lets your users screenshot the page, mark it up and write a note. You receive one report and store it where you want.",
5
5
  "keywords": [
6
6
  "feedback",
@@ -60,7 +60,7 @@
60
60
  "test:pack": "playwright test -c tests/pack-smoke/playwright.config.ts",
61
61
  "size": "size-limit",
62
62
  "publint": "publint --strict",
63
- "check": "npm run check:static && npm run check:browser",
63
+ "check": "node tests/check.mjs",
64
64
  "check:static": "npm run lint && npm run typecheck && npm run build && npm run test && npm run test:edge && npm run size && npm run publint",
65
65
  "check:browser": "npm run test:e2e && npm run test:react && npm run test:pack",
66
66
  "prepack": "npm run --silent build -- --logLevel warn",