@mdbase-dev/ui 0.1.0-beta.125 → 0.1.0-beta.147

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.
Files changed (77) hide show
  1. package/README.md +159 -0
  2. package/css/brand.css +10 -10
  3. package/css/collections.css +978 -0
  4. package/css/controls.css +220 -43
  5. package/css/feedback.css +20 -20
  6. package/css/fonts.css +2 -3
  7. package/css/palette.css +10 -10
  8. package/css/screens.css +58 -16
  9. package/css/tokens.css +43 -3
  10. package/dist/AppSwitcher.js +187 -33
  11. package/dist/Checkbox.d.ts +14 -0
  12. package/dist/Checkbox.js +32 -0
  13. package/dist/CommandPalette.d.ts +4 -2
  14. package/dist/CommandPalette.js +275 -103
  15. package/dist/Dialog.js +72 -25
  16. package/dist/Feedback.js +763 -229
  17. package/dist/FeedbackMarkup.d.ts +1 -1
  18. package/dist/FeedbackMarkup.js +326 -125
  19. package/dist/FeedbackVerification.js +115 -68
  20. package/dist/SaveNotice.js +71 -28
  21. package/dist/Select.js +183 -135
  22. package/dist/SelectList.js +59 -14
  23. package/dist/ThemeSelect.js +55 -4
  24. package/dist/apps.d.ts +1 -0
  25. package/dist/apps.js +50 -29
  26. package/dist/brand.js +251 -68
  27. package/dist/codemirror.js +116 -87
  28. package/dist/collections/CollectionGate.d.ts +52 -0
  29. package/dist/collections/CollectionGate.js +456 -0
  30. package/dist/collections/CollectionSaveWarning.d.ts +10 -0
  31. package/dist/collections/CollectionSaveWarning.js +28 -0
  32. package/dist/collections/CollectionSwitcher.d.ts +26 -0
  33. package/dist/collections/CollectionSwitcher.js +579 -0
  34. package/dist/collections/SetupReview.d.ts +9 -0
  35. package/dist/collections/SetupReview.js +175 -0
  36. package/dist/collections/copy.d.ts +100 -0
  37. package/dist/collections/copy.js +937 -0
  38. package/dist/collections/dialogs.d.ts +27 -0
  39. package/dist/collections/dialogs.js +169 -0
  40. package/dist/collections/model.d.ts +146 -0
  41. package/dist/collections/model.js +337 -0
  42. package/dist/collections/mount.d.ts +18 -0
  43. package/dist/collections/mount.js +56 -0
  44. package/dist/collections/parts.d.ts +86 -0
  45. package/dist/collections/parts.js +439 -0
  46. package/dist/collections/testing.d.ts +19 -0
  47. package/dist/collections/testing.js +39 -0
  48. package/dist/collections/use-collection-ui.d.ts +140 -0
  49. package/dist/collections/use-collection-ui.js +236 -0
  50. package/dist/collections.d.ts +19 -0
  51. package/dist/collections.js +13 -0
  52. package/dist/feedback-data.d.ts +13 -12
  53. package/dist/feedback-data.js +365 -168
  54. package/dist/flock.d.ts +63 -0
  55. package/dist/flock.js +140 -0
  56. package/dist/i18n.d.ts +6 -0
  57. package/dist/i18n.js +71 -0
  58. package/dist/locales/de/messages.json +1 -0
  59. package/dist/locales/en/messages.json +1 -0
  60. package/dist/locales/es/messages.json +1 -0
  61. package/dist/locales/fr/messages.json +1 -0
  62. package/dist/locales/it/messages.json +1 -0
  63. package/dist/locales/ja/messages.json +1 -0
  64. package/dist/locales/ko/messages.json +1 -0
  65. package/dist/locales/pt/messages.json +1 -0
  66. package/dist/locales/ru/messages.json +1 -0
  67. package/dist/locales/zh/messages.json +1 -0
  68. package/dist/mark-activity.js +82 -75
  69. package/dist/permission-selection.js +27 -17
  70. package/dist/popover.js +138 -130
  71. package/dist/screens.d.ts +20 -3
  72. package/dist/screens.js +275 -6
  73. package/dist/select-model.js +34 -28
  74. package/dist/text-diff.d.ts +1 -0
  75. package/dist/text-diff.js +106 -74
  76. package/dist/theme.js +34 -40
  77. package/package.json +53 -8
@@ -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
- problem: { label: "Report a problem", prompt: "What happened?", placeholder: "What were you trying to do?", thanks: "Thanks for the report." },
6
- idea: { label: "Suggest an improvement", prompt: "What would you like to improve?", placeholder: "How would it help you?", thanks: "Thanks for the idea." },
7
- appreciation: { label: "Share something you like", prompt: "What’s working well for you?", placeholder: "What made a difference?", thanks: "Thanks for the kind words." }
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
- return events.filter((event) => { const age = now - Date.parse(event.at); return age >= 0 && age <= 5 * 60_000; }).slice(-30).map(({ at, code, status }) => ({ at, code, ...(status === undefined ? {} : { status }) }));
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
- const ua = navigator.userAgent;
15
- const family = /Edg\//u.test(ua) ? "Edge" : /Firefox\//u.test(ua) ? "Firefox" : /Chrome\//u.test(ua) ? "Chrome" : /Safari\//u.test(ua) ? "Safari" : "Other";
16
- const browserMatch = { Edge: /Edg\/(\d+)/u, Firefox: /Firefox\/(\d+)/u, Chrome: /Chrome\/(\d+)/u, Safari: /Version\/(\d+)/u, Other: /$^/u }[family].exec(ua);
17
- return {
18
- schema_version: 2,
19
- browser: family === "Other" ? "Other" : `${family} ${browserMatch?.[1] ?? "unknown"}`,
20
- 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",
21
- viewport: window.innerWidth < 760 ? "compact" : window.innerWidth < 1200 ? "medium" : "wide",
22
- events: feedbackEvents(events)
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
- const value = configured?.trim() || (development ? "http://127.0.0.1:8790/v1/feedback" : "");
27
- if (!value)
28
- return null;
29
- try {
30
- const url = new URL(value);
31
- if (url.username || url.password || (url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost"].includes(url.hostname))))
32
- return null;
33
- return url.href;
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
- if (!/^[a-z][a-z0-9_-]{0,63}$/u.test(sourceView))
41
- throw new TypeError("Feedback views must be fixed identifiers, not URLs or record paths.");
42
- return { product, source_view: sourceView, build_revision: revision && /^[a-zA-Z0-9._-]{1,64}$/u.test(revision) ? revision : null, environment: environment === "staging" || environment === "development" || environment === "lab" ? environment : "production" };
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
- let response;
46
- try {
47
- response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(submission), credentials: "omit", referrerPolicy: "no-referrer", signal });
48
- }
49
- catch (error) {
50
- if (signal.aborted)
51
- throw error;
52
- throw new Error("Feedback could not be sent. Check your connection and try again.");
53
- }
54
- if (response.ok)
55
- return;
56
- let message = "Feedback could not be sent. Please try again.";
57
- try {
58
- const result = await response.json();
59
- if (typeof result.error?.message === "string" && result.error.message.length <= 200)
60
- message = result.error.message;
61
- }
62
- catch { /* Never reflect infrastructure/provider response bodies. */ }
63
- throw new Error(message);
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
- let binary = "";
68
- for (let offset = 0; offset < bytes.length; offset += 32_768)
69
- binary += String.fromCharCode(...bytes.subarray(offset, offset + 32_768));
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
- const blob = await new Promise((resolve, reject) => canvas.toBlob((result) => result ? resolve(result) : reject(new Error("The screenshot could not be created.")), type, type === "image/jpeg" ? 0.9 : undefined));
74
- if (blob.size > FEEDBACK_MAX_SCREENSHOT_BYTES)
75
- throw new Error("The screenshot is larger than 3 MB. Choose a smaller image.");
76
- return { media_type: type, filename: type === "image/png" ? "screenshot.png" : "screenshot.jpg", content_base64: bytesToBase64(new Uint8Array(await blob.arrayBuffer())) };
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
- if (file.size > FEEDBACK_MAX_SCREENSHOT_BYTES)
80
- throw new Error("Choose a screenshot smaller than 3 MB.");
81
- if (file.type !== "image/png" && file.type !== "image/jpeg")
82
- throw new Error("Choose a PNG or JPEG screenshot.");
83
- const bytes = new Uint8Array(await file.arrayBuffer());
84
- 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;
85
- if (!valid)
86
- throw new Error("The screenshot does not contain a valid PNG or JPEG image.");
87
- const dimensions = rasterDimensions(bytes, file.type);
88
- assertImageSize(dimensions.width, dimensions.height); // Reject decompression bombs before browser decoding.
89
- const url = URL.createObjectURL(file);
90
- try {
91
- const image = new Image();
92
- image.src = url;
93
- await image.decode();
94
- assertImageSize(image.naturalWidth, image.naturalHeight);
95
- const canvas = document.createElement("canvas");
96
- canvas.width = image.naturalWidth;
97
- canvas.height = image.naturalHeight;
98
- const context = canvas.getContext("2d");
99
- if (!context)
100
- throw new Error("The screenshot could not be cleaned in this browser.");
101
- context.drawImage(image, 0, 0);
102
- return await screenshotFromCanvas(canvas, file.type);
103
- }
104
- finally {
105
- URL.revokeObjectURL(url);
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
- if (!width || !height || width > 8192 || height > 8192 || width * height > 16_000_000)
110
- throw new Error("Choose a screenshot smaller than 16 megapixels.");
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
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
114
- if (type === "image/png" && bytes.length >= 24 && String.fromCharCode(...bytes.subarray(12, 16)) === "IHDR")
115
- return { width: view.getUint32(16), height: view.getUint32(20) };
116
- if (type === "image/jpeg") {
117
- let offset = 2;
118
- while (offset + 4 <= bytes.length) {
119
- if (bytes[offset++] !== 0xff)
120
- break;
121
- while (bytes[offset] === 0xff)
122
- offset++;
123
- const marker = bytes[offset++];
124
- if (marker === 0xd9 || marker === 0xda || marker === undefined || offset + 2 > bytes.length)
125
- break;
126
- if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7))
127
- continue;
128
- const length = view.getUint16(offset);
129
- if (length < 2 || offset + length > bytes.length)
130
- break;
131
- if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker) && length >= 8)
132
- return { width: view.getUint16(offset + 5), height: view.getUint16(offset + 3) };
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
- throw new Error("The screenshot does not contain a valid PNG or JPEG image.");
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
- let timer;
140
- let abort;
141
- try {
142
- return await Promise.race([promise, new Promise((_, reject) => {
143
- abort = () => reject(new DOMException("Capture cancelled", "AbortError"));
144
- if (signal.aborted) {
145
- abort();
146
- return;
147
- }
148
- signal.addEventListener("abort", abort, { once: true });
149
- timer = setTimeout(() => reject(new Error("The screenshot could not be captured. Try again.")), milliseconds);
150
- })]);
151
- }
152
- finally {
153
- clearTimeout(timer);
154
- if (abort)
155
- signal.removeEventListener("abort", abort);
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
- if (!navigator.mediaDevices?.getDisplayMedia)
161
- throw new Error("Screen capture isn’t available in this browser. You can attach an image instead.");
162
- await bounded(new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))), 1000, signal);
163
- if (signal.aborted)
164
- throw new DOMException("Capture cancelled", "AbortError");
165
- // The picker is intentionally not timed out: the user decides whether to grant capture.
166
- const stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: "browser" }, audio: false, preferCurrentTab: true, selfBrowserSurface: "include", surfaceSwitching: "exclude" });
167
- const stop = () => stream.getTracks().forEach((track) => track.stop());
168
- const video = document.createElement("video");
169
- let frameCallback;
170
- signal.addEventListener("abort", stop, { once: true });
171
- try {
172
- if (signal.aborted)
173
- throw new DOMException("Capture cancelled", "AbortError");
174
- video.muted = true;
175
- video.playsInline = true;
176
- video.srcObject = stream;
177
- const frame = typeof video.requestVideoFrameCallback === "function" ? new Promise((resolve) => { frameCallback = video.requestVideoFrameCallback(() => resolve()); }) : Promise.resolve();
178
- await bounded(Promise.all([video.play(), frame]), 5000, signal);
179
- if (!video.videoWidth || !video.videoHeight || stream.getVideoTracks()[0]?.readyState === "ended")
180
- throw new Error("Screen sharing ended before a screenshot was taken.");
181
- const scale = Math.min(1, 1600 / video.videoWidth, 1200 / video.videoHeight);
182
- const canvas = document.createElement("canvas");
183
- canvas.width = Math.round(video.videoWidth * scale);
184
- canvas.height = Math.round(video.videoHeight * scale);
185
- const context = canvas.getContext("2d");
186
- if (!context)
187
- throw new Error("The screenshot could not be created in this browser.");
188
- context.drawImage(video, 0, 0, canvas.width, canvas.height);
189
- stop(); // Do not leave sharing running during asynchronous PNG encoding.
190
- return await screenshotFromCanvas(canvas);
191
- }
192
- finally {
193
- stop();
194
- signal.removeEventListener("abort", stop);
195
- if (frameCallback !== undefined)
196
- video.cancelVideoFrameCallback(frameCallback);
197
- video.pause();
198
- video.srcObject = null;
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
  }
@@ -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 {};