@mdbase-dev/ui 0.1.0-beta.126 → 0.1.0-beta.148
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 +168 -0
- package/css/collections.css +978 -0
- package/css/controls.css +33 -1
- package/css/screens.css +44 -2
- package/dist/AppSwitcher.js +187 -33
- package/dist/Checkbox.d.ts +14 -0
- package/dist/Checkbox.js +32 -0
- package/dist/CollectionModeControl.d.ts +11 -0
- package/dist/CollectionModeControl.js +290 -0
- package/dist/CommandPalette.d.ts +4 -2
- package/dist/CommandPalette.js +275 -103
- package/dist/Dialog.js +72 -25
- package/dist/Feedback.js +763 -229
- package/dist/FeedbackMarkup.d.ts +1 -1
- package/dist/FeedbackMarkup.js +326 -125
- package/dist/FeedbackVerification.js +115 -68
- package/dist/SaveNotice.js +71 -28
- package/dist/Select.js +183 -135
- package/dist/SelectList.js +59 -14
- package/dist/ThemeSelect.js +55 -4
- package/dist/apps.d.ts +1 -0
- package/dist/apps.js +50 -29
- package/dist/brand.js +251 -68
- package/dist/codemirror.js +116 -87
- package/dist/collections/CollectionGate.d.ts +52 -0
- package/dist/collections/CollectionGate.js +456 -0
- package/dist/collections/CollectionSaveWarning.d.ts +10 -0
- package/dist/collections/CollectionSaveWarning.js +28 -0
- package/dist/collections/CollectionSwitcher.d.ts +51 -0
- package/dist/collections/CollectionSwitcher.js +598 -0
- package/dist/collections/SetupReview.d.ts +9 -0
- package/dist/collections/SetupReview.js +216 -0
- package/dist/collections/copy.d.ts +100 -0
- package/dist/collections/copy.js +937 -0
- package/dist/collections/dialogs.d.ts +27 -0
- package/dist/collections/dialogs.js +169 -0
- package/dist/collections/model.d.ts +146 -0
- package/dist/collections/model.js +337 -0
- package/dist/collections/mount.d.ts +19 -0
- package/dist/collections/mount.js +56 -0
- package/dist/collections/parts.d.ts +86 -0
- package/dist/collections/parts.js +439 -0
- package/dist/collections/testing.d.ts +19 -0
- package/dist/collections/testing.js +39 -0
- package/dist/collections/use-collection-ui.d.ts +140 -0
- package/dist/collections/use-collection-ui.js +236 -0
- package/dist/collections.d.ts +19 -0
- package/dist/collections.js +13 -0
- package/dist/feedback-data.d.ts +13 -12
- package/dist/feedback-data.js +365 -168
- package/dist/flock.d.ts +63 -0
- package/dist/flock.js +140 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +71 -0
- package/dist/locales/de/messages.json +1 -0
- package/dist/locales/en/messages.json +1 -0
- package/dist/locales/es/messages.json +1 -0
- package/dist/locales/fr/messages.json +1 -0
- package/dist/locales/it/messages.json +1 -0
- package/dist/locales/ja/messages.json +1 -0
- package/dist/locales/ko/messages.json +1 -0
- package/dist/locales/pt/messages.json +1 -0
- package/dist/locales/ru/messages.json +1 -0
- package/dist/locales/zh/messages.json +1 -0
- package/dist/mark-activity.js +82 -75
- package/dist/permission-selection.js +27 -17
- package/dist/popover.js +138 -130
- package/dist/screens.d.ts +20 -3
- package/dist/screens.js +275 -6
- package/dist/select-model.js +34 -28
- package/dist/text-diff.d.ts +1 -0
- package/dist/text-diff.js +106 -74
- package/dist/theme.js +34 -40
- package/package.json +56 -7
package/dist/feedback-data.js
CHANGED
|
@@ -1,200 +1,397 @@
|
|
|
1
|
+
import { i18n as _i18n } from "@lingui/core";
|
|
2
|
+
import "./i18n.js";
|
|
1
3
|
export const FEEDBACK_MAX_MESSAGE_LENGTH = 5_000;
|
|
2
4
|
export const FEEDBACK_MAX_SCREENSHOT_BYTES = 3 * 1024 * 1024;
|
|
3
5
|
export const FEEDBACK_ERROR_CODES = ["cancelled", "http_error", "invalid_response", "outcome_unknown", "partial_failure", "timeout", "network_error", "unknown_error", "save_failed", "source_open_failed", "preview_failed"];
|
|
4
6
|
export const feedbackTopics = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
problem: {
|
|
8
|
+
get label() {
|
|
9
|
+
return _i18n._(
|
|
10
|
+
/** i18n */
|
|
11
|
+
{
|
|
12
|
+
id: "hV3yAb",
|
|
13
|
+
message: "Report a problem"
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
get prompt() {
|
|
17
|
+
return _i18n._(
|
|
18
|
+
/** i18n */
|
|
19
|
+
{
|
|
20
|
+
id: "y_K0Dz",
|
|
21
|
+
message: "What happened?"
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
get placeholder() {
|
|
25
|
+
return _i18n._(
|
|
26
|
+
/** i18n */
|
|
27
|
+
{
|
|
28
|
+
id: "zDJycW",
|
|
29
|
+
message: "What were you trying to do?"
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
get thanks() {
|
|
33
|
+
return _i18n._(
|
|
34
|
+
/** i18n */
|
|
35
|
+
{
|
|
36
|
+
id: "GtzbT7",
|
|
37
|
+
message: "Thanks for the report."
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
idea: {
|
|
42
|
+
get label() {
|
|
43
|
+
return _i18n._(
|
|
44
|
+
/** i18n */
|
|
45
|
+
{
|
|
46
|
+
id: "aPgjJq",
|
|
47
|
+
message: "Suggest an improvement"
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
get prompt() {
|
|
51
|
+
return _i18n._(
|
|
52
|
+
/** i18n */
|
|
53
|
+
{
|
|
54
|
+
id: "TKM0Sf",
|
|
55
|
+
message: "What would you like to improve?"
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
get placeholder() {
|
|
59
|
+
return _i18n._(
|
|
60
|
+
/** i18n */
|
|
61
|
+
{
|
|
62
|
+
id: "ktO6Lq",
|
|
63
|
+
message: "How would it help you?"
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
get thanks() {
|
|
67
|
+
return _i18n._(
|
|
68
|
+
/** i18n */
|
|
69
|
+
{
|
|
70
|
+
id: "ngIpWJ",
|
|
71
|
+
message: "Thanks for the idea."
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
appreciation: {
|
|
76
|
+
get label() {
|
|
77
|
+
return _i18n._(
|
|
78
|
+
/** i18n */
|
|
79
|
+
{
|
|
80
|
+
id: "GaeNkI",
|
|
81
|
+
message: "Share something you like"
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
get prompt() {
|
|
85
|
+
return _i18n._(
|
|
86
|
+
/** i18n */
|
|
87
|
+
{
|
|
88
|
+
id: "nne584",
|
|
89
|
+
message: "What\u2019s working well for you?"
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
get placeholder() {
|
|
93
|
+
return _i18n._(
|
|
94
|
+
/** i18n */
|
|
95
|
+
{
|
|
96
|
+
id: "Dg50Vr",
|
|
97
|
+
message: "What made a difference?"
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
get thanks() {
|
|
101
|
+
return _i18n._(
|
|
102
|
+
/** i18n */
|
|
103
|
+
{
|
|
104
|
+
id: "UoMXZp",
|
|
105
|
+
message: "Thanks for the kind words."
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
8
109
|
};
|
|
9
110
|
/** Each provider owns its bounded, short-lived error buffer; never accepts messages or request data. */
|
|
10
111
|
export function feedbackEvents(events, now = Date.now()) {
|
|
11
|
-
|
|
112
|
+
return events.filter(event => {
|
|
113
|
+
const age = now - Date.parse(event.at);
|
|
114
|
+
return age >= 0 && age <= 5 * 60_000;
|
|
115
|
+
}).slice(-30).map(({
|
|
116
|
+
at,
|
|
117
|
+
code,
|
|
118
|
+
status
|
|
119
|
+
}) => ({
|
|
120
|
+
at,
|
|
121
|
+
code,
|
|
122
|
+
...(status === undefined ? {} : {
|
|
123
|
+
status
|
|
124
|
+
})
|
|
125
|
+
}));
|
|
12
126
|
}
|
|
13
127
|
export function feedbackDiagnostics(events) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
128
|
+
const ua = navigator.userAgent;
|
|
129
|
+
const family = /Edg\//u.test(ua) ? "Edge" : /Firefox\//u.test(ua) ? "Firefox" : /Chrome\//u.test(ua) ? "Chrome" : /Safari\//u.test(ua) ? "Safari" : "Other";
|
|
130
|
+
const browserMatch = {
|
|
131
|
+
Edge: /Edg\/(\d+)/u,
|
|
132
|
+
Firefox: /Firefox\/(\d+)/u,
|
|
133
|
+
Chrome: /Chrome\/(\d+)/u,
|
|
134
|
+
Safari: /Version\/(\d+)/u,
|
|
135
|
+
Other: /$^/u
|
|
136
|
+
}[family].exec(ua);
|
|
137
|
+
return {
|
|
138
|
+
schema_version: 2,
|
|
139
|
+
browser: family === "Other" ? "Other" : `${family} ${browserMatch?.[1] ?? "unknown"}`,
|
|
140
|
+
operating_system: /iPhone|iPad|iPod/u.test(ua) || /Macintosh/u.test(ua) && navigator.maxTouchPoints > 1 ? "iOS" : /Android/u.test(ua) ? "Android" : /Windows/u.test(ua) ? "Windows" : /Macintosh|Mac OS/u.test(ua) ? "macOS" : /Linux/u.test(ua) ? "Linux" : "Other",
|
|
141
|
+
viewport: window.innerWidth < 760 ? "compact" : window.innerWidth < 1200 ? "medium" : "wide",
|
|
142
|
+
events: feedbackEvents(events)
|
|
143
|
+
};
|
|
24
144
|
}
|
|
25
145
|
export function resolveFeedbackEndpoint(configured, development = false) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
146
|
+
const value = configured?.trim() || (development ? "http://127.0.0.1:8790/v1/feedback" : "");
|
|
147
|
+
if (!value) return null;
|
|
148
|
+
try {
|
|
149
|
+
const url = new URL(value);
|
|
150
|
+
if (url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost"].includes(url.hostname))) return null;
|
|
151
|
+
return url.href;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
38
155
|
}
|
|
39
156
|
export function feedbackApplication(product, sourceView, revision, environment) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
157
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/u.test(sourceView)) throw new TypeError("Feedback views must be fixed identifiers, not URLs or record paths.");
|
|
158
|
+
return {
|
|
159
|
+
product,
|
|
160
|
+
source_view: sourceView,
|
|
161
|
+
build_revision: revision && /^[a-zA-Z0-9._-]{1,64}$/u.test(revision) ? revision : null,
|
|
162
|
+
environment: environment === "staging" || environment === "development" || environment === "lab" ? environment : "production"
|
|
163
|
+
};
|
|
43
164
|
}
|
|
44
165
|
export async function sendFeedback(endpoint, submission, signal) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
166
|
+
let response;
|
|
167
|
+
try {
|
|
168
|
+
response = await fetch(endpoint, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: {
|
|
171
|
+
"content-type": "application/json"
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify(submission),
|
|
174
|
+
credentials: "omit",
|
|
175
|
+
referrerPolicy: "no-referrer",
|
|
176
|
+
signal
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (signal.aborted) throw error;
|
|
180
|
+
throw new Error(_i18n._(
|
|
181
|
+
/** i18n */
|
|
182
|
+
{
|
|
183
|
+
id: "1CWcjd",
|
|
184
|
+
message: "Feedback could not be sent. Check your connection and try again."
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
if (response.ok) return;
|
|
188
|
+
let message = _i18n._(
|
|
189
|
+
/** i18n */
|
|
190
|
+
{
|
|
191
|
+
id: "mRd7qL",
|
|
192
|
+
message: "Feedback could not be sent. Please try again."
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
const result = await response.json();
|
|
196
|
+
if (typeof result.error?.message === "string" && result.error.message.length <= 200) message = result.error.message;
|
|
197
|
+
} catch {/* Never reflect infrastructure/provider response bodies. */}
|
|
198
|
+
throw new Error(message);
|
|
199
|
+
}
|
|
200
|
+
export function screenshotUrl(screenshot) {
|
|
201
|
+
return `data:${screenshot.media_type};base64,${screenshot.content_base64}`;
|
|
64
202
|
}
|
|
65
|
-
export function screenshotUrl(screenshot) { return `data:${screenshot.media_type};base64,${screenshot.content_base64}`; }
|
|
66
203
|
function bytesToBase64(bytes) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return btoa(binary);
|
|
204
|
+
let binary = "";
|
|
205
|
+
for (let offset = 0; offset < bytes.length; offset += 32_768) binary += String.fromCharCode(...bytes.subarray(offset, offset + 32_768));
|
|
206
|
+
return btoa(binary);
|
|
71
207
|
}
|
|
72
208
|
export async function screenshotFromCanvas(canvas, type = "image/png") {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
209
|
+
const blob = await new Promise((resolve, reject) => canvas.toBlob(result => result ? resolve(result) : reject(new Error(_i18n._(
|
|
210
|
+
/** i18n */
|
|
211
|
+
{
|
|
212
|
+
id: "DXe-Ku",
|
|
213
|
+
message: "The screenshot could not be created."
|
|
214
|
+
}))), type, type === "image/jpeg" ? 0.9 : undefined));
|
|
215
|
+
if (blob.size > FEEDBACK_MAX_SCREENSHOT_BYTES) throw new Error(_i18n._(
|
|
216
|
+
/** i18n */
|
|
217
|
+
{
|
|
218
|
+
id: "06ZW-I",
|
|
219
|
+
message: "The screenshot is larger than 3 MB. Choose a smaller image."
|
|
220
|
+
}));
|
|
221
|
+
return {
|
|
222
|
+
media_type: type,
|
|
223
|
+
filename: type === "image/png" ? "screenshot.png" : "screenshot.jpg",
|
|
224
|
+
content_base64: bytesToBase64(new Uint8Array(await blob.arrayBuffer()))
|
|
225
|
+
};
|
|
77
226
|
}
|
|
78
227
|
export async function readFeedbackScreenshot(file) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
228
|
+
if (file.size > FEEDBACK_MAX_SCREENSHOT_BYTES) throw new Error(_i18n._(
|
|
229
|
+
/** i18n */
|
|
230
|
+
{
|
|
231
|
+
id: "88OB58",
|
|
232
|
+
message: "Choose a screenshot smaller than 3 MB."
|
|
233
|
+
}));
|
|
234
|
+
if (file.type !== "image/png" && file.type !== "image/jpeg") throw new Error(_i18n._(
|
|
235
|
+
/** i18n */
|
|
236
|
+
{
|
|
237
|
+
id: "bRZJdX",
|
|
238
|
+
message: "Choose a PNG or JPEG screenshot."
|
|
239
|
+
}));
|
|
240
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
241
|
+
const valid = file.type === "image/png" ? [137, 80, 78, 71, 13, 10, 26, 10].every((v, i) => bytes[i] === v) : bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
|
|
242
|
+
if (!valid) throw new Error(_i18n._(
|
|
243
|
+
/** i18n */
|
|
244
|
+
{
|
|
245
|
+
id: "WtBLy2",
|
|
246
|
+
message: "The screenshot does not contain a valid PNG or JPEG image."
|
|
247
|
+
}));
|
|
248
|
+
const dimensions = rasterDimensions(bytes, file.type);
|
|
249
|
+
assertImageSize(dimensions.width, dimensions.height); // Reject decompression bombs before browser decoding.
|
|
250
|
+
const url = URL.createObjectURL(file);
|
|
251
|
+
try {
|
|
252
|
+
const image = new Image();
|
|
253
|
+
image.src = url;
|
|
254
|
+
await image.decode();
|
|
255
|
+
assertImageSize(image.naturalWidth, image.naturalHeight);
|
|
256
|
+
const canvas = document.createElement("canvas");
|
|
257
|
+
canvas.width = image.naturalWidth;
|
|
258
|
+
canvas.height = image.naturalHeight;
|
|
259
|
+
const context = canvas.getContext("2d");
|
|
260
|
+
if (!context) throw new Error(_i18n._(
|
|
261
|
+
/** i18n */
|
|
262
|
+
{
|
|
263
|
+
id: "0LOjOR",
|
|
264
|
+
message: "The screenshot could not be cleaned in this browser."
|
|
265
|
+
}));
|
|
266
|
+
context.drawImage(image, 0, 0);
|
|
267
|
+
return await screenshotFromCanvas(canvas, file.type);
|
|
268
|
+
} finally {
|
|
269
|
+
URL.revokeObjectURL(url);
|
|
270
|
+
}
|
|
107
271
|
}
|
|
108
272
|
function assertImageSize(width, height) {
|
|
109
|
-
|
|
110
|
-
|
|
273
|
+
if (!width || !height || width > 8192 || height > 8192 || width * height > 16_000_000) throw new Error(_i18n._(
|
|
274
|
+
/** i18n */
|
|
275
|
+
{
|
|
276
|
+
id: "BCJqT2",
|
|
277
|
+
message: "Choose a screenshot smaller than 16 megapixels."
|
|
278
|
+
}));
|
|
111
279
|
}
|
|
112
280
|
function rasterDimensions(bytes, type) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
offset += length;
|
|
134
|
-
}
|
|
281
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
282
|
+
if (type === "image/png" && bytes.length >= 24 && String.fromCharCode(...bytes.subarray(12, 16)) === "IHDR") return {
|
|
283
|
+
width: view.getUint32(16),
|
|
284
|
+
height: view.getUint32(20)
|
|
285
|
+
};
|
|
286
|
+
if (type === "image/jpeg") {
|
|
287
|
+
let offset = 2;
|
|
288
|
+
while (offset + 4 <= bytes.length) {
|
|
289
|
+
if (bytes[offset++] !== 0xff) break;
|
|
290
|
+
while (bytes[offset] === 0xff) offset++;
|
|
291
|
+
const marker = bytes[offset++];
|
|
292
|
+
if (marker === 0xd9 || marker === 0xda || marker === undefined || offset + 2 > bytes.length) break;
|
|
293
|
+
if (marker === 0x01 || marker >= 0xd0 && marker <= 0xd7) continue;
|
|
294
|
+
const length = view.getUint16(offset);
|
|
295
|
+
if (length < 2 || offset + length > bytes.length) break;
|
|
296
|
+
if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker) && length >= 8) return {
|
|
297
|
+
width: view.getUint16(offset + 5),
|
|
298
|
+
height: view.getUint16(offset + 3)
|
|
299
|
+
};
|
|
300
|
+
offset += length;
|
|
135
301
|
}
|
|
136
|
-
|
|
302
|
+
}
|
|
303
|
+
throw new Error(_i18n._(
|
|
304
|
+
/** i18n */
|
|
305
|
+
{
|
|
306
|
+
id: "WtBLy2",
|
|
307
|
+
message: "The screenshot does not contain a valid PNG or JPEG image."
|
|
308
|
+
}));
|
|
137
309
|
}
|
|
138
310
|
async function bounded(promise, milliseconds, signal) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
311
|
+
let timer;
|
|
312
|
+
let abort;
|
|
313
|
+
try {
|
|
314
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
315
|
+
abort = () => reject(new DOMException("Capture cancelled", "AbortError"));
|
|
316
|
+
if (signal.aborted) {
|
|
317
|
+
abort();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
signal.addEventListener("abort", abort, {
|
|
321
|
+
once: true
|
|
322
|
+
});
|
|
323
|
+
timer = setTimeout(() => reject(new Error(_i18n._(
|
|
324
|
+
/** i18n */
|
|
325
|
+
{
|
|
326
|
+
id: "5u4cOF",
|
|
327
|
+
message: "The screenshot could not be captured. Try again."
|
|
328
|
+
}))), milliseconds);
|
|
329
|
+
})]);
|
|
330
|
+
} finally {
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
if (abort) signal.removeEventListener("abort", abort);
|
|
333
|
+
}
|
|
157
334
|
}
|
|
158
335
|
/** Caller closes the native dialog first. Media is always stopped, including cancellation/unmount. */
|
|
159
336
|
export async function captureFeedbackScreenshot(signal) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
337
|
+
if (!navigator.mediaDevices?.getDisplayMedia) throw new Error(_i18n._(
|
|
338
|
+
/** i18n */
|
|
339
|
+
{
|
|
340
|
+
id: "DWBQ45",
|
|
341
|
+
message: "Screen capture isn\u2019t available in this browser. You can attach an image instead."
|
|
342
|
+
}));
|
|
343
|
+
await bounded(new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))), 1000, signal);
|
|
344
|
+
if (signal.aborted) throw new DOMException("Capture cancelled", "AbortError");
|
|
345
|
+
// The picker is intentionally not timed out: the user decides whether to grant capture.
|
|
346
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
347
|
+
video: {
|
|
348
|
+
displaySurface: "browser"
|
|
349
|
+
},
|
|
350
|
+
audio: false,
|
|
351
|
+
preferCurrentTab: true,
|
|
352
|
+
selfBrowserSurface: "include",
|
|
353
|
+
surfaceSwitching: "exclude"
|
|
354
|
+
});
|
|
355
|
+
const stop = () => stream.getTracks().forEach(track => track.stop());
|
|
356
|
+
const video = document.createElement("video");
|
|
357
|
+
let frameCallback;
|
|
358
|
+
signal.addEventListener("abort", stop, {
|
|
359
|
+
once: true
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
if (signal.aborted) throw new DOMException("Capture cancelled", "AbortError");
|
|
363
|
+
video.muted = true;
|
|
364
|
+
video.playsInline = true;
|
|
365
|
+
video.srcObject = stream;
|
|
366
|
+
const frame = typeof video.requestVideoFrameCallback === "function" ? new Promise(resolve => {
|
|
367
|
+
frameCallback = video.requestVideoFrameCallback(() => resolve());
|
|
368
|
+
}) : Promise.resolve();
|
|
369
|
+
await bounded(Promise.all([video.play(), frame]), 5000, signal);
|
|
370
|
+
if (!video.videoWidth || !video.videoHeight || stream.getVideoTracks()[0]?.readyState === "ended") throw new Error(_i18n._(
|
|
371
|
+
/** i18n */
|
|
372
|
+
{
|
|
373
|
+
id: "zJfG-M",
|
|
374
|
+
message: "Screen sharing ended before a screenshot was taken."
|
|
375
|
+
}));
|
|
376
|
+
const scale = Math.min(1, 1600 / video.videoWidth, 1200 / video.videoHeight);
|
|
377
|
+
const canvas = document.createElement("canvas");
|
|
378
|
+
canvas.width = Math.round(video.videoWidth * scale);
|
|
379
|
+
canvas.height = Math.round(video.videoHeight * scale);
|
|
380
|
+
const context = canvas.getContext("2d");
|
|
381
|
+
if (!context) throw new Error(_i18n._(
|
|
382
|
+
/** i18n */
|
|
383
|
+
{
|
|
384
|
+
id: "Qymvdk",
|
|
385
|
+
message: "The screenshot could not be created in this browser."
|
|
386
|
+
}));
|
|
387
|
+
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
388
|
+
stop(); // Do not leave sharing running during asynchronous PNG encoding.
|
|
389
|
+
return await screenshotFromCanvas(canvas);
|
|
390
|
+
} finally {
|
|
391
|
+
stop();
|
|
392
|
+
signal.removeEventListener("abort", stop);
|
|
393
|
+
if (frameCallback !== undefined) video.cancelVideoFrameCallback(frameCallback);
|
|
394
|
+
video.pause();
|
|
395
|
+
video.srcObject = null;
|
|
396
|
+
}
|
|
200
397
|
}
|
package/dist/flock.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type MdbaseMarkRect } from "./brand.js";
|
|
2
|
+
/**
|
|
3
|
+
* The launch flock: small birds fly in from the edges of the screen, land on the
|
|
4
|
+
* Frontmatter mark as two rows of dots per bar, and close up into the solid bars.
|
|
5
|
+
* Everything here is a pure function of time, so a frame can be drawn at any moment
|
|
6
|
+
* and tests can check where the birds end up.
|
|
7
|
+
*/
|
|
8
|
+
/** Seconds. The mark is solid at `solid`; the entrance hands over to the real mark at `end`. */
|
|
9
|
+
export declare const FLOCK_TIMING: {
|
|
10
|
+
/** Spread of start times; each bird leaves within this window. */
|
|
11
|
+
readonly spread: 0.45;
|
|
12
|
+
/** How long each bird is in the air. */
|
|
13
|
+
readonly flight: 0.85;
|
|
14
|
+
/** When the first bars start closing up. */
|
|
15
|
+
readonly close: 1.2;
|
|
16
|
+
/** Spread of the bars' close-up times, top to bottom with the highlight last. */
|
|
17
|
+
readonly closeStagger: 0.2;
|
|
18
|
+
/** How long one bar takes to close up. */
|
|
19
|
+
readonly closeDuration: 0.3;
|
|
20
|
+
readonly solid: 1.8;
|
|
21
|
+
readonly end: 2;
|
|
22
|
+
};
|
|
23
|
+
type FlockBird = Readonly<{
|
|
24
|
+
/** Index of the bar this bird belongs to in `flockBars`. */
|
|
25
|
+
bar: number;
|
|
26
|
+
accent: boolean;
|
|
27
|
+
/** Landed dot, in mark units (the 120-unit board the mark's rects use). */
|
|
28
|
+
dotX: number;
|
|
29
|
+
dotY: number;
|
|
30
|
+
/** The share of its bar the bird grows into when the bar closes up. */
|
|
31
|
+
cell: MdbaseMarkRect;
|
|
32
|
+
seed: number;
|
|
33
|
+
}>;
|
|
34
|
+
type FlockBar = Readonly<{
|
|
35
|
+
rect: MdbaseMarkRect;
|
|
36
|
+
accent: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
export declare const flockBars: readonly FlockBar[];
|
|
39
|
+
/** Two rows of dots per bar; each dot's cell tiles its bar exactly. */
|
|
40
|
+
export declare const flockBirds: readonly FlockBird[];
|
|
41
|
+
/** Where the mark sits on screen: its 84-unit artboard (viewBox 18 18 84 84) in pixels. */
|
|
42
|
+
type FlockStage = Readonly<{
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
markLeft: number;
|
|
46
|
+
markTop: number;
|
|
47
|
+
markSize: number;
|
|
48
|
+
}>;
|
|
49
|
+
type FlockFrame = Readonly<{
|
|
50
|
+
/** One rectangle per bird still drawn as a bird, in screen pixels. */
|
|
51
|
+
birds: readonly (MdbaseMarkRect & {
|
|
52
|
+
accent: boolean;
|
|
53
|
+
})[];
|
|
54
|
+
/** Bars that have closed up completely, drawn as the real rectangles. */
|
|
55
|
+
bars: readonly (MdbaseMarkRect & {
|
|
56
|
+
accent: boolean;
|
|
57
|
+
})[];
|
|
58
|
+
}>;
|
|
59
|
+
/** How far bar `index` has closed up at time `t`, 0 to 1. */
|
|
60
|
+
export declare function flockBarClosed(index: number, t: number): number;
|
|
61
|
+
/** Everything to draw at time `t` seconds into the entrance. */
|
|
62
|
+
export declare function flockFrame(stage: FlockStage, t: number): FlockFrame;
|
|
63
|
+
export {};
|