@goflowstate/widget-sdk 0.2.2 → 0.3.1
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 +44 -5
- package/dist/canvas-widget-sdk.js +455 -7
- package/dist/canvas-widget-sdk.js.map +4 -4
- package/dist/feedback.d.ts +79 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.test.d.ts +2 -0
- package/dist/feedback.test.d.ts.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +456 -10
- package/dist/index.mjs.map +3 -3
- package/dist/session.d.ts +71 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.request.test.d.ts +2 -0
- package/dist/session.request.test.d.ts.map +1 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -104,10 +104,13 @@ for await (const ev of widget.ai.stream({
|
|
|
104
104
|
}
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
Available aliases: `widget.
|
|
108
|
-
(
|
|
109
|
-
(
|
|
110
|
-
|
|
107
|
+
Available aliases: `widget.haiku` (Anthropic, fast), `widget.sonnet`
|
|
108
|
+
(Anthropic, strongest), `widget.gpt` (OpenAI, strongest), `widget.gpt-mini`
|
|
109
|
+
(OpenAI, fast), `widget.gemini` (Google, strongest), `widget.gemini-mini`
|
|
110
|
+
(Google, fast), `widget.sonar` (Perplexity, answers grounded in live web
|
|
111
|
+
search), `widget.sonar-pro` (Perplexity, web-grounded, deeper research).
|
|
112
|
+
Aliases are platform-managed; the model behind one can improve without a
|
|
113
|
+
code change on your side.
|
|
111
114
|
|
|
112
115
|
Notes:
|
|
113
116
|
|
|
@@ -138,9 +141,45 @@ if (result.granted) {
|
|
|
138
141
|
widget.notify('Saved!', { level: 'info', duration: 3000 });
|
|
139
142
|
```
|
|
140
143
|
|
|
144
|
+
## Viewer claims (shell guests)
|
|
145
|
+
|
|
146
|
+
A session opened from a unishell shell page belongs to a guest whose name,
|
|
147
|
+
email or phone the shell has already verified. Those facts arrive on every
|
|
148
|
+
state read as `state.viewer.claims`, and the latest ones are on
|
|
149
|
+
`session.claims`:
|
|
150
|
+
|
|
151
|
+
const session = CanvasWidget.initSession();
|
|
152
|
+
const state = await session.join();
|
|
153
|
+
const email = session.claims?.email; // { type, value, source, verifiedAt? } | undefined
|
|
154
|
+
|
|
155
|
+
Claims are only ever the reader's own, and they are context, not
|
|
156
|
+
authorization. `session.claims` is `null` for members, for PWA guests and
|
|
157
|
+
before the first state arrives.
|
|
158
|
+
|
|
159
|
+
## Flow feedback (the bug icon)
|
|
160
|
+
|
|
161
|
+
A flow that calls `CanvasWidget.initSession()` carries the feedback icon with no code of its own.
|
|
162
|
+
While the ring has feedback switched on, the session state says so and the SDK puts a round
|
|
163
|
+
button in the corner of the page. A tap takes a screenshot of the page, then opens a sheet with
|
|
164
|
+
one field. The note is sent through the flow's session to `POST /api/w/:slug/feedback`.
|
|
165
|
+
|
|
166
|
+
The icon and the sheet take the look of the page they sit on: its fonts, ground, ink, the fill
|
|
167
|
+
and shape of its main button, and its corner radius. To pin a role, set a custom property on
|
|
168
|
+
`:root`:
|
|
169
|
+
|
|
170
|
+
```css
|
|
171
|
+
:root {
|
|
172
|
+
--fs-feedback-cta: #5136c4; /* also -cta-ink, -ground, -ink, -font, -font-display, -radius */
|
|
173
|
+
--fs-feedback-bottom: 96px; /* where the icon sits; also -right */
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Name each screen so a report says where it came from: `document.body.dataset.pageKey = 'question-2'`.
|
|
178
|
+
A page that must not show the icon (a projected wall) sets `<html data-fs-feedback="off">`.
|
|
179
|
+
|
|
141
180
|
## Full API Reference
|
|
142
181
|
|
|
143
|
-
See [
|
|
182
|
+
See the [Flowstate developer docs](https://developer.goflowstate.com/docs/) for the complete API reference, the manifest format, the AI integration guide, and the submission pipeline.
|
|
144
183
|
|
|
145
184
|
## License
|
|
146
185
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @goflowstate/widget-sdk v0.
|
|
1
|
+
// @goflowstate/widget-sdk v0.3.1
|
|
2
2
|
// https://github.com/goflowstate/goflowstate-sdk
|
|
3
3
|
|
|
4
4
|
"use strict";
|
|
@@ -7,6 +7,9 @@ var CanvasWidgetSDK = (() => {
|
|
|
7
7
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
8
8
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
9
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
10
13
|
var __export = (target, all) => {
|
|
11
14
|
for (var name in all)
|
|
12
15
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -21,6 +24,18 @@ var CanvasWidgetSDK = (() => {
|
|
|
21
24
|
};
|
|
22
25
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
23
26
|
|
|
27
|
+
// no-rasterizer:modern-screenshot
|
|
28
|
+
var modern_screenshot_exports = {};
|
|
29
|
+
__export(modern_screenshot_exports, {
|
|
30
|
+
domToCanvas: () => domToCanvas
|
|
31
|
+
});
|
|
32
|
+
var domToCanvas;
|
|
33
|
+
var init_modern_screenshot = __esm({
|
|
34
|
+
"no-rasterizer:modern-screenshot"() {
|
|
35
|
+
domToCanvas = () => Promise.reject(new Error("no rasterizer in the script-tag bundle"));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
24
39
|
// src/sdk.ts
|
|
25
40
|
var sdk_exports = {};
|
|
26
41
|
__export(sdk_exports, {
|
|
@@ -62,6 +77,389 @@ var CanvasWidgetSDK = (() => {
|
|
|
62
77
|
};
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
// src/feedback.ts
|
|
81
|
+
var FEEDBACK_HOST_ATTR = "data-fs-feedback-host";
|
|
82
|
+
var SCREENSHOT_MAX_BYTES = 15e4;
|
|
83
|
+
var SCREENSHOT_MAX_SIDE = 1280;
|
|
84
|
+
var CAPTURE_TIMEOUT_MS = 4e3;
|
|
85
|
+
function parseColor(raw) {
|
|
86
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
87
|
+
if (!value || value === "transparent") return null;
|
|
88
|
+
const hex = /^#([0-9a-f]{3,8})$/.exec(value);
|
|
89
|
+
if (hex) {
|
|
90
|
+
const h = hex[1];
|
|
91
|
+
const full = h.length <= 4 ? h.split("").map((c) => c + c).join("") : h;
|
|
92
|
+
if (full.length !== 6 && full.length !== 8) return null;
|
|
93
|
+
const n = (i) => parseInt(full.slice(i, i + 2), 16);
|
|
94
|
+
return [n(0), n(2), n(4), full.length === 8 ? n(6) / 255 : 1];
|
|
95
|
+
}
|
|
96
|
+
const fn = /^rgba?\(([^)]+)\)$/.exec(value);
|
|
97
|
+
if (!fn) return null;
|
|
98
|
+
const parts = fn[1].split(/[\s,/]+/).filter(Boolean);
|
|
99
|
+
if (parts.length < 3) return null;
|
|
100
|
+
const channel = (p) => p.endsWith("%") ? parseFloat(p) * 255 / 100 : parseFloat(p);
|
|
101
|
+
const alpha = parts[3] === void 0 ? 1 : parts[3].endsWith("%") ? parseFloat(parts[3]) / 100 : parseFloat(parts[3]);
|
|
102
|
+
const out = [channel(parts[0]), channel(parts[1]), channel(parts[2]), alpha];
|
|
103
|
+
return out.some((v) => Number.isNaN(v)) ? null : out;
|
|
104
|
+
}
|
|
105
|
+
var opaque = (c) => c !== null && c[3] >= 0.9;
|
|
106
|
+
var css = (c, alpha = c[3]) => alpha >= 1 ? `rgb(${Math.round(c[0])}, ${Math.round(c[1])}, ${Math.round(c[2])})` : `rgba(${Math.round(c[0])}, ${Math.round(c[1])}, ${Math.round(c[2])}, ${alpha})`;
|
|
107
|
+
var mix = (a, b, t) => [
|
|
108
|
+
a[0] + (b[0] - a[0]) * t,
|
|
109
|
+
a[1] + (b[1] - a[1]) * t,
|
|
110
|
+
a[2] + (b[2] - a[2]) * t,
|
|
111
|
+
1
|
|
112
|
+
];
|
|
113
|
+
var luminance = (c) => {
|
|
114
|
+
const lin = (v) => {
|
|
115
|
+
const s = v / 255;
|
|
116
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
117
|
+
};
|
|
118
|
+
return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
|
|
119
|
+
};
|
|
120
|
+
var contrast = (a, b) => {
|
|
121
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
122
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
123
|
+
};
|
|
124
|
+
var WHITE = [255, 255, 255, 1];
|
|
125
|
+
var BLACK = [17, 17, 20, 1];
|
|
126
|
+
var styleOf = (el) => getComputedStyle(el);
|
|
127
|
+
var rootVar = (doc, names) => {
|
|
128
|
+
for (const el of [doc.documentElement, doc.body]) {
|
|
129
|
+
if (!el) continue;
|
|
130
|
+
const style = styleOf(el);
|
|
131
|
+
for (const name of names) {
|
|
132
|
+
const value = style.getPropertyValue(name).trim();
|
|
133
|
+
if (value) return value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return "";
|
|
137
|
+
};
|
|
138
|
+
var visible = (el) => {
|
|
139
|
+
const rect = el.getBoundingClientRect();
|
|
140
|
+
const style = styleOf(el);
|
|
141
|
+
return style.display !== "none" && style.visibility !== "hidden" && rect.width * rect.height >= 0;
|
|
142
|
+
};
|
|
143
|
+
var inOurs = (el) => el.closest(`[${FEEDBACK_HOST_ATTR}]`) !== null;
|
|
144
|
+
var primaryButton = (doc, ground) => {
|
|
145
|
+
let best = null;
|
|
146
|
+
let bestArea = -1;
|
|
147
|
+
doc.querySelectorAll(
|
|
148
|
+
'button, [role="button"], input[type="submit"], a[class*="btn"], a[class*="cta"]'
|
|
149
|
+
).forEach((el) => {
|
|
150
|
+
if (inOurs(el) || !visible(el)) return;
|
|
151
|
+
const fill = parseColor(styleOf(el).backgroundColor);
|
|
152
|
+
if (!opaque(fill) || contrast(fill, ground) < 1.25) return;
|
|
153
|
+
const rect = el.getBoundingClientRect();
|
|
154
|
+
const area = rect.width * rect.height;
|
|
155
|
+
if (area > bestArea) {
|
|
156
|
+
best = el;
|
|
157
|
+
bestArea = area;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return best;
|
|
161
|
+
};
|
|
162
|
+
function readFlowTheme(doc = document) {
|
|
163
|
+
const body = doc.body;
|
|
164
|
+
const stage = body.querySelector(":scope > :not(script):not(style):not([data-fs-feedback-host])");
|
|
165
|
+
const bodyStyle = styleOf(body);
|
|
166
|
+
const sampledGround = [stage, body, doc.documentElement].map((el) => el ? parseColor(styleOf(el).backgroundColor) : null).find(opaque);
|
|
167
|
+
const sampledInk = parseColor((stage ? styleOf(stage) : bodyStyle).color) ?? parseColor(bodyStyle.color);
|
|
168
|
+
const tokenGround = parseColor(rootVar(doc, ["--sb-ground", "--sb-bg", "--color-ink-050"]));
|
|
169
|
+
const ink0 = parseColor(rootVar(doc, ["--fs-feedback-ink"])) ?? sampledInk;
|
|
170
|
+
let ground = parseColor(rootVar(doc, ["--fs-feedback-ground"])) ?? sampledGround ?? (tokenGround && opaque(tokenGround) ? tokenGround : null) ?? (ink0 && luminance(ink0) > 0.5 ? BLACK : WHITE);
|
|
171
|
+
ground = [ground[0], ground[1], ground[2], 1];
|
|
172
|
+
const dark = luminance(ground) < 0.35;
|
|
173
|
+
let ink = ink0 && contrast([ink0[0], ink0[1], ink0[2], 1], ground) >= 4.5 ? ink0 : dark ? WHITE : BLACK;
|
|
174
|
+
ink = [ink[0], ink[1], ink[2], 1];
|
|
175
|
+
const button = primaryButton(doc, ground);
|
|
176
|
+
const buttonStyle = button ? styleOf(button) : null;
|
|
177
|
+
const pinnedCta = parseColor(rootVar(doc, ["--fs-feedback-cta"]));
|
|
178
|
+
const tokenCta = parseColor(rootVar(doc, ["--sb-cta", "--sb-accent", "--color-cream"]));
|
|
179
|
+
const sampledCta = buttonStyle ? parseColor(buttonStyle.backgroundColor) : null;
|
|
180
|
+
const cta = pinnedCta ?? sampledCta ?? (tokenCta && contrast(tokenCta, ground) >= 1.25 ? tokenCta : null) ?? ink;
|
|
181
|
+
const pinnedCtaInk = parseColor(rootVar(doc, ["--fs-feedback-cta-ink"]));
|
|
182
|
+
const sampledCtaInk = !pinnedCta && sampledCta && buttonStyle ? parseColor(buttonStyle.color) : null;
|
|
183
|
+
const ctaInk = pinnedCtaInk ?? (sampledCtaInk && contrast(sampledCtaInk, cta) >= 3 ? sampledCtaInk : null) ?? (luminance(cta) > 0.4 ? BLACK : WHITE);
|
|
184
|
+
const heading = Array.from(doc.querySelectorAll('h1, h2, [class*="title"]')).find(
|
|
185
|
+
(el) => !inOurs(el) && visible(el)
|
|
186
|
+
);
|
|
187
|
+
const headingStyle = heading ? styleOf(heading) : null;
|
|
188
|
+
const fontBody = rootVar(doc, ["--fs-feedback-font"]) || (stage ? styleOf(stage).fontFamily : "") || bodyStyle.fontFamily || rootVar(doc, ["--sb-font-body", "--font-body"]) || "system-ui, sans-serif";
|
|
189
|
+
const fontDisplay = rootVar(doc, ["--fs-feedback-font-display"]) || headingStyle?.fontFamily || rootVar(doc, ["--sb-font-display", "--sb-font-title", "--font-display"]) || fontBody;
|
|
190
|
+
const field = Array.from(
|
|
191
|
+
doc.querySelectorAll('textarea, input[type="text"], input:not([type])')
|
|
192
|
+
).find((el) => !inOurs(el) && visible(el));
|
|
193
|
+
const px = (value) => {
|
|
194
|
+
const n = parseFloat(value ?? "");
|
|
195
|
+
return Number.isFinite(n) ? n : null;
|
|
196
|
+
};
|
|
197
|
+
const cornerOf = (style) => style ? px(style.borderTopLeftRadius || style.borderRadius) : null;
|
|
198
|
+
const buttonRadius = cornerOf(buttonStyle);
|
|
199
|
+
const fieldRadius = cornerOf(field ? styleOf(field) : null);
|
|
200
|
+
const radius = rootVar(doc, ["--fs-feedback-radius"]) || `${Math.min(fieldRadius ?? Math.min(buttonRadius ?? 16, 22), 28)}px`;
|
|
201
|
+
return {
|
|
202
|
+
ground: css(ground),
|
|
203
|
+
surface: css(dark ? mix(ground, WHITE, 0.07) : mix(ground, WHITE, 0.6)),
|
|
204
|
+
ink: css(ink),
|
|
205
|
+
inkDim: css(mix(ink, ground, 0.32)),
|
|
206
|
+
line: css(ink, 0.16),
|
|
207
|
+
fill: css(ink, 0.05),
|
|
208
|
+
cta: css([cta[0], cta[1], cta[2], 1]),
|
|
209
|
+
ctaInk: css([ctaInk[0], ctaInk[1], ctaInk[2], 1]),
|
|
210
|
+
fontBody,
|
|
211
|
+
fontDisplay,
|
|
212
|
+
displayWeight: headingStyle?.fontWeight || "500",
|
|
213
|
+
radius,
|
|
214
|
+
buttonRadius: buttonRadius === null ? "999px" : `${Math.min(buttonRadius, 999)}px`,
|
|
215
|
+
dark
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
var dataUrlBytes = (url) => Math.floor((url.length - url.indexOf(",") - 1) * 3 / 4);
|
|
219
|
+
function encodeScreenshot(canvas) {
|
|
220
|
+
let source = canvas;
|
|
221
|
+
for (let pass = 0; pass < 3; pass += 1) {
|
|
222
|
+
for (const quality of [0.82, 0.7, 0.58, 0.46, 0.34]) {
|
|
223
|
+
const url = source.toDataURL("image/jpeg", quality);
|
|
224
|
+
if (url.startsWith("data:image/jpeg") && dataUrlBytes(url) <= SCREENSHOT_MAX_BYTES)
|
|
225
|
+
return url;
|
|
226
|
+
}
|
|
227
|
+
const smaller = document.createElement("canvas");
|
|
228
|
+
smaller.width = Math.max(1, Math.round(source.width * 0.75));
|
|
229
|
+
smaller.height = Math.max(1, Math.round(source.height * 0.75));
|
|
230
|
+
const ctx = smaller.getContext("2d");
|
|
231
|
+
if (!ctx) return null;
|
|
232
|
+
ctx.drawImage(source, 0, 0, smaller.width, smaller.height);
|
|
233
|
+
source = smaller;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
async function captureViewport(ground) {
|
|
238
|
+
const { domToCanvas: domToCanvas2 } = await Promise.resolve().then(() => (init_modern_screenshot(), modern_screenshot_exports));
|
|
239
|
+
const width = window.innerWidth;
|
|
240
|
+
const height = window.innerHeight;
|
|
241
|
+
const scale = Math.min(
|
|
242
|
+
window.devicePixelRatio || 1,
|
|
243
|
+
SCREENSHOT_MAX_SIDE / Math.max(width, height)
|
|
244
|
+
);
|
|
245
|
+
const canvas = await domToCanvas2(document.documentElement, {
|
|
246
|
+
width,
|
|
247
|
+
height,
|
|
248
|
+
scale,
|
|
249
|
+
backgroundColor: ground,
|
|
250
|
+
style: { transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)` },
|
|
251
|
+
filter: (node) => !(node instanceof Element && node.hasAttribute(FEEDBACK_HOST_ATTR))
|
|
252
|
+
});
|
|
253
|
+
return encodeScreenshot(canvas);
|
|
254
|
+
}
|
|
255
|
+
var withTimeout = (work, ms) => new Promise((resolve) => {
|
|
256
|
+
const timer = setTimeout(() => resolve(null), ms);
|
|
257
|
+
work.then(
|
|
258
|
+
(value) => {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
resolve(value);
|
|
261
|
+
},
|
|
262
|
+
() => {
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
resolve(null);
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
var BUG_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="7" width="8" height="12" rx="4"/><path d="M9.5 7a2.5 2.5 0 0 1 5 0M12 11v8M4 13h4M16 13h4M5 7l3.2 2.4M19 7l-3.2 2.4M5 20l3.2-2.6M19 20l-3.2-2.6"/></svg>';
|
|
269
|
+
var STYLE = `
|
|
270
|
+
:host { all: initial; }
|
|
271
|
+
* { box-sizing: border-box; }
|
|
272
|
+
.bug {
|
|
273
|
+
position: fixed; z-index: 2147483000;
|
|
274
|
+
right: calc(var(--fs-feedback-right, 14px) + env(safe-area-inset-right, 0px));
|
|
275
|
+
bottom: calc(var(--fs-feedback-bottom, 88px) + env(safe-area-inset-bottom, 0px));
|
|
276
|
+
width: 44px; height: 44px; border-radius: 50%; padding: 0;
|
|
277
|
+
display: grid; place-items: center; cursor: pointer;
|
|
278
|
+
background: var(--surface); color: var(--ink); border: 1px solid var(--line);
|
|
279
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.14);
|
|
280
|
+
-webkit-tap-highlight-color: transparent;
|
|
281
|
+
}
|
|
282
|
+
.bug svg { width: 20px; height: 20px; }
|
|
283
|
+
.bug:focus-visible, .send:focus-visible, .link:focus-visible, textarea:focus-visible {
|
|
284
|
+
outline: 2px solid var(--ink); outline-offset: 2px;
|
|
285
|
+
}
|
|
286
|
+
.scrim { position: fixed; inset: 0; z-index: 2147483001; background: rgba(0, 0, 0, 0.42); }
|
|
287
|
+
.sheet {
|
|
288
|
+
position: fixed; left: 0; right: 0; bottom: 0; z-index: 2147483002;
|
|
289
|
+
margin: 0 auto; max-width: 480px;
|
|
290
|
+
background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-bottom: 0;
|
|
291
|
+
border-radius: calc(var(--radius) + 6px) calc(var(--radius) + 6px) 0 0;
|
|
292
|
+
padding: 12px 18px calc(20px + env(safe-area-inset-bottom, 0px));
|
|
293
|
+
display: flex; flex-direction: column; gap: 12px;
|
|
294
|
+
font-family: var(--font-body); font-size: 15px; line-height: 1.4;
|
|
295
|
+
}
|
|
296
|
+
.grab { width: 36px; height: 4px; border-radius: 2px; background: var(--line); align-self: center; }
|
|
297
|
+
h2 { margin: 0; font-family: var(--font-display); font-weight: var(--display-weight); font-size: 19px; line-height: 1.2; }
|
|
298
|
+
.shot { display: flex; gap: 12px; align-items: center; font-size: 13px; color: var(--ink-dim); }
|
|
299
|
+
.shot img { width: 50px; max-height: 108px; object-fit: cover; object-position: top; border-radius: 8px; border: 1px solid var(--line); }
|
|
300
|
+
.link { background: none; border: 0; padding: 0; font: inherit; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
301
|
+
textarea {
|
|
302
|
+
width: 100%; min-height: 84px; resize: none; font: inherit; font-size: 16px; color: var(--ink);
|
|
303
|
+
background: var(--fill); border: 1px solid var(--line); border-radius: var(--radius); padding: 13px 15px;
|
|
304
|
+
}
|
|
305
|
+
textarea::placeholder { color: var(--ink-dim); }
|
|
306
|
+
.send {
|
|
307
|
+
font: inherit; font-weight: 500; cursor: pointer; border: 0; padding: 14px;
|
|
308
|
+
background: var(--cta); color: var(--cta-ink); border-radius: var(--button-radius);
|
|
309
|
+
}
|
|
310
|
+
.send[disabled] { opacity: 0.5; cursor: default; }
|
|
311
|
+
.status { margin: 0; font-size: 13px; color: var(--ink-dim); min-height: 1.4em; }
|
|
312
|
+
[hidden] { display: none !important; }
|
|
313
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
314
|
+
.sheet { animation: rise 0.22s ease-out; }
|
|
315
|
+
@keyframes rise { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
var mounted = null;
|
|
319
|
+
var feedbackOptedOut = (doc = document) => doc.documentElement.getAttribute("data-fs-feedback") === "off";
|
|
320
|
+
function mountFlowFeedback(options) {
|
|
321
|
+
if (typeof document === "undefined" || !document.body) return null;
|
|
322
|
+
if (mounted) return mounted;
|
|
323
|
+
if (feedbackOptedOut()) return null;
|
|
324
|
+
const host = document.createElement("div");
|
|
325
|
+
host.setAttribute(FEEDBACK_HOST_ATTR, "");
|
|
326
|
+
const root = host.attachShadow({ mode: "open" });
|
|
327
|
+
root.innerHTML = `<style>${STYLE}</style>
|
|
328
|
+
<button type="button" class="bug" aria-label="Send feedback on this page">${BUG_ICON}</button>
|
|
329
|
+
<div class="scrim" hidden></div>
|
|
330
|
+
<div class="sheet" role="dialog" aria-modal="true" aria-labelledby="fsf-title" hidden>
|
|
331
|
+
<div class="grab" aria-hidden="true"></div>
|
|
332
|
+
<h2 id="fsf-title">Feedback on this page</h2>
|
|
333
|
+
<div class="shot" hidden><img alt="Screenshot of this page" /><span><span>Screenshot attached</span><br /><button type="button" class="link remove">Remove screenshot</button></span></div>
|
|
334
|
+
<textarea aria-labelledby="fsf-title" placeholder="What should change?" maxlength="2000"></textarea>
|
|
335
|
+
<p class="status" role="status" aria-live="polite"></p>
|
|
336
|
+
<button type="button" class="send" disabled>Send feedback</button>
|
|
337
|
+
</div>`;
|
|
338
|
+
const q = (selector) => root.querySelector(selector);
|
|
339
|
+
const bug = q(".bug");
|
|
340
|
+
const scrim = q(".scrim");
|
|
341
|
+
const sheet = q(".sheet");
|
|
342
|
+
const shot = q(".shot");
|
|
343
|
+
const img = q(".shot img");
|
|
344
|
+
const remove = q(".remove");
|
|
345
|
+
const text = q("textarea");
|
|
346
|
+
const status = q(".status");
|
|
347
|
+
const send = q(".send");
|
|
348
|
+
let theme = readFlowTheme();
|
|
349
|
+
let screenshot = null;
|
|
350
|
+
let busy = false;
|
|
351
|
+
let closeTimer = null;
|
|
352
|
+
const paint = () => {
|
|
353
|
+
theme = readFlowTheme();
|
|
354
|
+
const vars = {
|
|
355
|
+
"--surface": theme.surface,
|
|
356
|
+
"--ink": theme.ink,
|
|
357
|
+
"--ink-dim": theme.inkDim,
|
|
358
|
+
"--line": theme.line,
|
|
359
|
+
"--fill": theme.fill,
|
|
360
|
+
"--cta": theme.cta,
|
|
361
|
+
"--cta-ink": theme.ctaInk,
|
|
362
|
+
"--font-body": theme.fontBody,
|
|
363
|
+
"--font-display": theme.fontDisplay,
|
|
364
|
+
"--display-weight": theme.displayWeight,
|
|
365
|
+
"--radius": theme.radius,
|
|
366
|
+
"--button-radius": theme.buttonRadius
|
|
367
|
+
};
|
|
368
|
+
for (const [name, value] of Object.entries(vars)) host.style.setProperty(name, value);
|
|
369
|
+
host.style.colorScheme = theme.dark ? "dark" : "light";
|
|
370
|
+
};
|
|
371
|
+
const sync = () => {
|
|
372
|
+
send.disabled = busy || text.value.trim().length === 0;
|
|
373
|
+
};
|
|
374
|
+
const setShot = (url) => {
|
|
375
|
+
screenshot = url;
|
|
376
|
+
shot.hidden = url === null;
|
|
377
|
+
if (url) img.src = url;
|
|
378
|
+
else img.removeAttribute("src");
|
|
379
|
+
};
|
|
380
|
+
const close = () => {
|
|
381
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
382
|
+
closeTimer = null;
|
|
383
|
+
sheet.hidden = true;
|
|
384
|
+
scrim.hidden = true;
|
|
385
|
+
bug.hidden = false;
|
|
386
|
+
setShot(null);
|
|
387
|
+
text.value = "";
|
|
388
|
+
status.textContent = "";
|
|
389
|
+
busy = false;
|
|
390
|
+
sync();
|
|
391
|
+
};
|
|
392
|
+
const open = async () => {
|
|
393
|
+
if (!sheet.hidden || busy) return;
|
|
394
|
+
busy = true;
|
|
395
|
+
paint();
|
|
396
|
+
bug.hidden = true;
|
|
397
|
+
const capture = options.capture ?? (() => captureViewport(theme.ground));
|
|
398
|
+
const url = await withTimeout(Promise.resolve().then(capture), CAPTURE_TIMEOUT_MS);
|
|
399
|
+
busy = false;
|
|
400
|
+
setShot(url);
|
|
401
|
+
scrim.hidden = false;
|
|
402
|
+
sheet.hidden = false;
|
|
403
|
+
sync();
|
|
404
|
+
text.focus();
|
|
405
|
+
};
|
|
406
|
+
const submit = async () => {
|
|
407
|
+
const message = text.value.trim();
|
|
408
|
+
if (!message || busy) return;
|
|
409
|
+
busy = true;
|
|
410
|
+
sync();
|
|
411
|
+
status.textContent = "";
|
|
412
|
+
try {
|
|
413
|
+
await options.submit({
|
|
414
|
+
message,
|
|
415
|
+
...screenshot ? { screenshot } : {},
|
|
416
|
+
...document.body.dataset.pageKey ? { pageKey: document.body.dataset.pageKey } : {},
|
|
417
|
+
pagePath: window.location.pathname.slice(-200),
|
|
418
|
+
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
419
|
+
});
|
|
420
|
+
status.textContent = "Sent.";
|
|
421
|
+
closeTimer = setTimeout(() => {
|
|
422
|
+
close();
|
|
423
|
+
bug.focus();
|
|
424
|
+
}, 1200);
|
|
425
|
+
} catch {
|
|
426
|
+
busy = false;
|
|
427
|
+
status.textContent = "That did not send. Try again.";
|
|
428
|
+
sync();
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
bug.addEventListener("click", () => void open());
|
|
432
|
+
scrim.addEventListener("click", close);
|
|
433
|
+
remove.addEventListener("click", () => setShot(null));
|
|
434
|
+
text.addEventListener("input", sync);
|
|
435
|
+
send.addEventListener("click", () => void submit());
|
|
436
|
+
sheet.addEventListener("keydown", (event) => {
|
|
437
|
+
if (event.key === "Escape") {
|
|
438
|
+
close();
|
|
439
|
+
bug.focus();
|
|
440
|
+
}
|
|
441
|
+
event.stopPropagation();
|
|
442
|
+
});
|
|
443
|
+
document.body.appendChild(host);
|
|
444
|
+
paint();
|
|
445
|
+
options.onReady?.();
|
|
446
|
+
mounted = {
|
|
447
|
+
host,
|
|
448
|
+
refresh: () => {
|
|
449
|
+
if (sheet.hidden) paint();
|
|
450
|
+
},
|
|
451
|
+
open,
|
|
452
|
+
close,
|
|
453
|
+
unmount: () => {
|
|
454
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
455
|
+
host.remove();
|
|
456
|
+
mounted = null;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
return mounted;
|
|
460
|
+
}
|
|
461
|
+
var unmountFlowFeedback = () => mounted?.unmount();
|
|
462
|
+
|
|
65
463
|
// src/session.ts
|
|
66
464
|
function readSessionLaunchParams() {
|
|
67
465
|
if (typeof window === "undefined") return null;
|
|
@@ -128,7 +526,8 @@ var CanvasWidgetSDK = (() => {
|
|
|
128
526
|
myContribution: s.myWishlist ?? null,
|
|
129
527
|
doc: s.plan ?? null,
|
|
130
528
|
artifactId: s.artifactId,
|
|
131
|
-
extension: s.extension ?? null
|
|
529
|
+
extension: s.extension ?? null,
|
|
530
|
+
viewer: s.viewer ? { userId: s.viewer.userId, guest: s.viewer.guest, claims: s.viewer.claims ?? null } : null
|
|
132
531
|
};
|
|
133
532
|
}
|
|
134
533
|
var parentOpener = (hostOrigin) => {
|
|
@@ -144,14 +543,17 @@ var CanvasWidgetSDK = (() => {
|
|
|
144
543
|
slug;
|
|
145
544
|
webBase;
|
|
146
545
|
mobileBase;
|
|
546
|
+
latestClaims = null;
|
|
147
547
|
apiBase;
|
|
148
548
|
tokenPromise;
|
|
149
549
|
opener;
|
|
550
|
+
hostOrigin;
|
|
150
551
|
constructor(params, opener) {
|
|
151
552
|
this.slug = params.slug;
|
|
152
553
|
this.webBase = params.webBase;
|
|
153
554
|
this.mobileBase = params.mobileBase ?? null;
|
|
154
555
|
this.apiBase = params.apiBase;
|
|
556
|
+
this.hostOrigin = hostOriginOf(params);
|
|
155
557
|
this.opener = opener ?? parentOpener(hostOriginOf(params));
|
|
156
558
|
this.tokenPromise = this.acquireTicket(params).then((t) => this.redeem(t));
|
|
157
559
|
}
|
|
@@ -185,7 +587,31 @@ var CanvasWidgetSDK = (() => {
|
|
|
185
587
|
}
|
|
186
588
|
const json = await res.json();
|
|
187
589
|
const data = json.data;
|
|
188
|
-
|
|
590
|
+
const wire = data.state ?? data;
|
|
591
|
+
const view = toStateView(wire);
|
|
592
|
+
this.latestClaims = view.viewer?.claims ?? null;
|
|
593
|
+
this.syncFeedback(wire.feedbackEnabled === true);
|
|
594
|
+
return view;
|
|
595
|
+
}
|
|
596
|
+
get claims() {
|
|
597
|
+
return this.latestClaims;
|
|
598
|
+
}
|
|
599
|
+
/** Flow feedback: every state read says whether the bug icon is on for this ring. On, the
|
|
600
|
+
* icon mounts once and re-reads the page's look on each read; off, it leaves. */
|
|
601
|
+
syncFeedback(enabled) {
|
|
602
|
+
if (typeof document === "undefined") return;
|
|
603
|
+
if (!enabled) {
|
|
604
|
+
unmountFlowFeedback();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const handle = mountFlowFeedback({
|
|
608
|
+
submit: (report) => this.request("feedback", report),
|
|
609
|
+
onReady: () => {
|
|
610
|
+
if (window.parent !== window)
|
|
611
|
+
window.parent.postMessage({ type: "flowstate:feedback-ready" }, this.hostOrigin);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
handle?.refresh();
|
|
189
615
|
}
|
|
190
616
|
join() {
|
|
191
617
|
return this.call("/enter", { method: "POST" });
|
|
@@ -227,15 +653,37 @@ var CanvasWidgetSDK = (() => {
|
|
|
227
653
|
body: JSON.stringify(body ?? {})
|
|
228
654
|
});
|
|
229
655
|
}
|
|
230
|
-
async request(verb, body) {
|
|
231
|
-
const
|
|
656
|
+
async request(verb, body, options) {
|
|
657
|
+
const signal = options?.signal;
|
|
658
|
+
if (signal?.aborted) {
|
|
659
|
+
void this.tokenPromise.catch(() => {
|
|
660
|
+
});
|
|
661
|
+
signal.throwIfAborted();
|
|
662
|
+
}
|
|
663
|
+
let onAbort;
|
|
664
|
+
let token;
|
|
665
|
+
try {
|
|
666
|
+
token = await (signal ? Promise.race([
|
|
667
|
+
this.tokenPromise,
|
|
668
|
+
new Promise((_resolve, reject) => {
|
|
669
|
+
onAbort = () => reject(signal.reason);
|
|
670
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
671
|
+
})
|
|
672
|
+
]) : this.tokenPromise);
|
|
673
|
+
} finally {
|
|
674
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
675
|
+
}
|
|
232
676
|
const res = await fetch(`${this.apiBase}/api/w/${this.slug}/${verb.replace(/^\//, "")}`, {
|
|
677
|
+
signal,
|
|
233
678
|
method: "POST",
|
|
234
679
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
235
680
|
body: JSON.stringify(body ?? {})
|
|
236
681
|
});
|
|
237
682
|
if (!res.ok) {
|
|
238
|
-
const errBody = await res.json().catch(() =>
|
|
683
|
+
const errBody = await res.json().catch(() => {
|
|
684
|
+
signal?.throwIfAborted();
|
|
685
|
+
return {};
|
|
686
|
+
});
|
|
239
687
|
throw new Error(errBody.message ?? `session service ${res.status}`);
|
|
240
688
|
}
|
|
241
689
|
const json = await res.json();
|
|
@@ -245,7 +693,7 @@ var CanvasWidgetSDK = (() => {
|
|
|
245
693
|
this.opener(url);
|
|
246
694
|
}
|
|
247
695
|
revealUrl(artifactId) {
|
|
248
|
-
return `${this.webBase}/a/${artifactId}`;
|
|
696
|
+
return this.mobileBase ? `${this.mobileBase}/?a=${artifactId}` : `${this.webBase}/a/${artifactId}`;
|
|
249
697
|
}
|
|
250
698
|
};
|
|
251
699
|
function initSession() {
|