@mdbase-dev/ui 0.1.0-beta.121 → 0.1.0-beta.124
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 +66 -1
- package/css/brand.css +27 -24
- package/css/feedback.css +62 -0
- package/css/motion.css +238 -130
- package/css/screens.css +1 -1
- package/dist/AppSwitcher.d.ts +3 -0
- package/dist/AppSwitcher.js +14 -1
- package/dist/Dialog.js +1 -1
- package/dist/Feedback.d.ts +27 -0
- package/dist/Feedback.js +247 -0
- package/dist/FeedbackMarkup.d.ts +18 -0
- package/dist/FeedbackMarkup.js +136 -0
- package/dist/FeedbackVerification.d.ts +21 -0
- package/dist/FeedbackVerification.js +76 -0
- package/dist/SaveNotice.d.ts +7 -1
- package/dist/SaveNotice.js +32 -0
- package/dist/brand.d.ts +32 -23
- package/dist/brand.js +80 -34
- package/dist/feedback-data.d.ts +75 -0
- package/dist/feedback-data.js +200 -0
- package/dist/mark-activity.d.ts +39 -0
- package/dist/mark-activity.js +121 -0
- package/dist/screens.d.ts +3 -2
- package/dist/screens.js +6 -4
- package/package.json +10 -1
package/dist/SaveNotice.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { holdMdbaseMarkBusy, signalMdbaseMark } from "./mark-activity.js";
|
|
4
|
+
/** A save shorter than this never sets the mark moving. */
|
|
5
|
+
const SLOW_SAVE_MS = 600;
|
|
6
|
+
/** The mark's reaction when a notice changes from `before` to `tone`; none when it first appears. */
|
|
7
|
+
export function saveToneSignal(before, tone) {
|
|
8
|
+
if (before === null || before === tone)
|
|
9
|
+
return null;
|
|
10
|
+
if (tone === "attention")
|
|
11
|
+
return "error";
|
|
12
|
+
return tone === "saved" && (before === "saving" || before === "pending") ? "saved" : null;
|
|
13
|
+
}
|
|
2
14
|
const defaultLabels = {
|
|
3
15
|
saved: "Saved",
|
|
4
16
|
saving: "Saving",
|
|
@@ -9,7 +21,27 @@ const defaultLabels = {
|
|
|
9
21
|
* The minimal save notice every mdbase app shows beside what it edits. Put it inside an
|
|
10
22
|
* element with `mdbase-settle-host` so hovering or focusing that element brings a settled
|
|
11
23
|
* notice back.
|
|
24
|
+
*
|
|
25
|
+
* The notice also drives the app's mark: a save that completes plays `saved`, a newly
|
|
26
|
+
* raised problem plays `error`, and a slow save bounces the mark until it ends.
|
|
12
27
|
*/
|
|
13
28
|
export function SaveNotice({ tone, label = defaultLabels[tone], className }) {
|
|
29
|
+
const previous = useRef(null);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const kind = saveToneSignal(previous.current, tone);
|
|
32
|
+
previous.current = tone;
|
|
33
|
+
if (kind)
|
|
34
|
+
signalMdbaseMark(kind);
|
|
35
|
+
}, [tone]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (tone !== "saving")
|
|
38
|
+
return;
|
|
39
|
+
let release;
|
|
40
|
+
const timer = setTimeout(() => { release = holdMdbaseMarkBusy("bounce"); }, SLOW_SAVE_MS);
|
|
41
|
+
return () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
release?.();
|
|
44
|
+
};
|
|
45
|
+
}, [tone]);
|
|
14
46
|
return _jsxs("span", { className: ["mdbase-save-notice", `is-${tone}`, tone === "saved" && "mdbase-settle", className].filter(Boolean).join(" "), "aria-live": "polite", children: [tone === "saved" && _jsx("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: _jsx("path", { d: "m3.5 8.5 3 3 6-7" }) }), label] });
|
|
15
47
|
}
|
package/dist/brand.d.ts
CHANGED
|
@@ -5,10 +5,21 @@ export type MdbaseMarkRect = Readonly<{
|
|
|
5
5
|
y: number;
|
|
6
6
|
width: number;
|
|
7
7
|
height: number;
|
|
8
|
-
rx: number;
|
|
9
8
|
}>;
|
|
10
|
-
|
|
9
|
+
/** Indeterminate waits. Each repeats until the motion is removed. */
|
|
10
|
+
export declare const mdbaseMarkLoops: readonly ["orbit", "scan", "bounce", "stream", "sort", "hop"];
|
|
11
|
+
/** Played once when the mark appears; the mark then rests. */
|
|
12
|
+
export declare const mdbaseMarkEntrances: readonly ["keys-first", "drop", "assemble"];
|
|
13
|
+
export declare const mdbaseMarkMotions: readonly ["orbit", "scan", "bounce", "stream", "sort", "hop", "keys-first", "drop", "assemble"];
|
|
14
|
+
export type MdbaseMarkLoop = typeof mdbaseMarkLoops[number];
|
|
11
15
|
export type MdbaseMarkMotion = typeof mdbaseMarkMotions[number];
|
|
16
|
+
/** One-shot reactions to something the person did. */
|
|
17
|
+
export type MdbaseMarkSignalKind = "saved" | "error";
|
|
18
|
+
/** `id` changes for every occurrence, so the same kind can play twice in a row. */
|
|
19
|
+
export type MdbaseMarkSignal = Readonly<{
|
|
20
|
+
kind: MdbaseMarkSignalKind;
|
|
21
|
+
id: number;
|
|
22
|
+
}>;
|
|
12
23
|
export declare function mdbaseMarkMotionClass(motion?: MdbaseMarkMotion): string;
|
|
13
24
|
export declare const MDBASE_MARK_VIEW_BOX = "18 18 84 84";
|
|
14
25
|
export declare const mdbaseMarkInkRects: readonly [{
|
|
@@ -16,75 +27,71 @@ export declare const mdbaseMarkInkRects: readonly [{
|
|
|
16
27
|
readonly y: 22;
|
|
17
28
|
readonly width: 20;
|
|
18
29
|
readonly height: 10;
|
|
19
|
-
readonly rx: 2;
|
|
20
30
|
}, {
|
|
21
31
|
readonly x: 50;
|
|
22
32
|
readonly y: 22;
|
|
23
33
|
readonly width: 20;
|
|
24
34
|
readonly height: 10;
|
|
25
|
-
readonly rx: 2;
|
|
26
35
|
}, {
|
|
27
36
|
readonly x: 78;
|
|
28
37
|
readonly y: 22;
|
|
29
38
|
readonly width: 20;
|
|
30
39
|
readonly height: 10;
|
|
31
|
-
readonly rx: 2;
|
|
32
40
|
}, {
|
|
33
41
|
readonly x: 22;
|
|
34
42
|
readonly y: 44;
|
|
35
43
|
readonly width: 12;
|
|
36
44
|
readonly height: 10;
|
|
37
|
-
readonly rx: 2;
|
|
38
45
|
}, {
|
|
39
46
|
readonly x: 22;
|
|
40
47
|
readonly y: 66;
|
|
41
48
|
readonly width: 28;
|
|
42
49
|
readonly height: 10;
|
|
43
|
-
readonly rx: 2;
|
|
44
50
|
}, {
|
|
45
51
|
readonly x: 58;
|
|
46
52
|
readonly y: 66;
|
|
47
53
|
readonly width: 40;
|
|
48
54
|
readonly height: 10;
|
|
49
|
-
readonly rx: 2;
|
|
50
55
|
}, {
|
|
51
56
|
readonly x: 22;
|
|
52
57
|
readonly y: 88;
|
|
53
58
|
readonly width: 20;
|
|
54
59
|
readonly height: 10;
|
|
55
|
-
readonly rx: 2;
|
|
56
60
|
}, {
|
|
57
61
|
readonly x: 50;
|
|
58
62
|
readonly y: 88;
|
|
59
63
|
readonly width: 20;
|
|
60
64
|
readonly height: 10;
|
|
61
|
-
readonly rx: 2;
|
|
62
65
|
}, {
|
|
63
66
|
readonly x: 78;
|
|
64
67
|
readonly y: 88;
|
|
65
68
|
readonly width: 20;
|
|
66
69
|
readonly height: 10;
|
|
67
|
-
readonly rx: 2;
|
|
68
70
|
}];
|
|
69
71
|
export declare const mdbaseMarkAccentRect: {
|
|
70
72
|
readonly x: 42;
|
|
71
73
|
readonly y: 44;
|
|
72
74
|
readonly width: 56;
|
|
73
75
|
readonly height: 10;
|
|
74
|
-
readonly rx: 2;
|
|
75
76
|
};
|
|
77
|
+
/** How much of each line is filled when `progress` of the whole is done, in reading order. */
|
|
78
|
+
export declare function mdbaseMarkLineFill(progress: number): number[];
|
|
76
79
|
/**
|
|
77
|
-
* The
|
|
78
|
-
*
|
|
80
|
+
* The Frontmatter mark. Without `app` it is the platform's own; with `app` it is
|
|
81
|
+
* inverted, so the bars take the app's colour and the highlighted line stays ink.
|
|
82
|
+
*
|
|
83
|
+
* `motion` loops or plays an entrance, `signal` plays a one-shot reaction over
|
|
84
|
+
* whatever else is running, and `progress` (0 to 1) fills the lines in reading
|
|
85
|
+
* order over a faint copy of the mark. Animations live in `motion.css`.
|
|
79
86
|
*/
|
|
80
|
-
export declare function MdbaseMark({ motion, className }: {
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
export declare function MdbaseMark({ app, motion, signal, progress, className }: {
|
|
88
|
+
app?: MdbaseAppId | undefined;
|
|
89
|
+
motion?: MdbaseMarkMotion | undefined;
|
|
90
|
+
signal?: MdbaseMarkSignal | null | undefined;
|
|
91
|
+
progress?: number | null | undefined;
|
|
92
|
+
className?: string | undefined;
|
|
83
93
|
}): JSX.Element;
|
|
84
|
-
/**
|
|
85
|
-
* An app's mark: the platform mark inverted, so the bars take the app's colour and
|
|
86
|
-
* the highlighted line stays ink. The plain mark stays the platform's own.
|
|
87
|
-
*/
|
|
94
|
+
/** An app's mark. Kept for callers that only ever draw an app's still mark. */
|
|
88
95
|
export declare function MdbaseAppMark({ app, className }: {
|
|
89
96
|
app: MdbaseAppId;
|
|
90
97
|
className?: string;
|
|
@@ -93,7 +100,9 @@ export declare function MdbaseAppMark({ app, className }: {
|
|
|
93
100
|
* `mdbase` followed by the product's quieter name. Editor carries the platform mark
|
|
94
101
|
* as the canonical app; Reader and Writer carry their app marks.
|
|
95
102
|
*/
|
|
96
|
-
export declare function Wordmark({ app, motion }: {
|
|
103
|
+
export declare function Wordmark({ app, motion, signal, progress }: {
|
|
97
104
|
app: MdbaseAppId;
|
|
98
|
-
motion?: MdbaseMarkMotion;
|
|
105
|
+
motion?: MdbaseMarkMotion | undefined;
|
|
106
|
+
signal?: MdbaseMarkSignal | null | undefined;
|
|
107
|
+
progress?: number | null | undefined;
|
|
99
108
|
}): JSX.Element;
|
package/dist/brand.js
CHANGED
|
@@ -1,61 +1,107 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useId } from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
/** Indeterminate waits. Each repeats until the motion is removed. */
|
|
4
|
+
export const mdbaseMarkLoops = [
|
|
5
|
+
"orbit",
|
|
6
|
+
"scan",
|
|
7
|
+
"bounce",
|
|
8
|
+
"stream",
|
|
9
|
+
"sort",
|
|
10
|
+
"hop"
|
|
8
11
|
];
|
|
12
|
+
/** Played once when the mark appears; the mark then rests. */
|
|
13
|
+
export const mdbaseMarkEntrances = [
|
|
14
|
+
"keys-first",
|
|
15
|
+
"drop",
|
|
16
|
+
"assemble"
|
|
17
|
+
];
|
|
18
|
+
export const mdbaseMarkMotions = [...mdbaseMarkLoops, ...mdbaseMarkEntrances];
|
|
19
|
+
/** Long enough for each entrance in `motion.css` to finish. */
|
|
20
|
+
const ENTRANCE_MS = 1300;
|
|
9
21
|
export function mdbaseMarkMotionClass(motion) {
|
|
10
22
|
return motion ? ` mdbase-motion-${motion}` : "";
|
|
11
23
|
}
|
|
24
|
+
function isEntrance(motion) {
|
|
25
|
+
return mdbaseMarkEntrances.includes(motion ?? "");
|
|
26
|
+
}
|
|
12
27
|
export const MDBASE_MARK_VIEW_BOX = "18 18 84 84";
|
|
13
28
|
export const mdbaseMarkInkRects = [
|
|
14
|
-
{ x: 22, y: 22, width: 20, height: 10
|
|
15
|
-
{ x: 50, y: 22, width: 20, height: 10
|
|
16
|
-
{ x: 78, y: 22, width: 20, height: 10
|
|
17
|
-
{ x: 22, y: 44, width: 12, height: 10
|
|
18
|
-
{ x: 22, y: 66, width: 28, height: 10
|
|
19
|
-
{ x: 58, y: 66, width: 40, height: 10
|
|
20
|
-
{ x: 22, y: 88, width: 20, height: 10
|
|
21
|
-
{ x: 50, y: 88, width: 20, height: 10
|
|
22
|
-
{ x: 78, y: 88, width: 20, height: 10
|
|
29
|
+
{ x: 22, y: 22, width: 20, height: 10 },
|
|
30
|
+
{ x: 50, y: 22, width: 20, height: 10 },
|
|
31
|
+
{ x: 78, y: 22, width: 20, height: 10 },
|
|
32
|
+
{ x: 22, y: 44, width: 12, height: 10 },
|
|
33
|
+
{ x: 22, y: 66, width: 28, height: 10 },
|
|
34
|
+
{ x: 58, y: 66, width: 40, height: 10 },
|
|
35
|
+
{ x: 22, y: 88, width: 20, height: 10 },
|
|
36
|
+
{ x: 50, y: 88, width: 20, height: 10 },
|
|
37
|
+
{ x: 78, y: 88, width: 20, height: 10 }
|
|
23
38
|
];
|
|
24
39
|
export const mdbaseMarkAccentRect = {
|
|
25
40
|
x: 42,
|
|
26
41
|
y: 44,
|
|
27
42
|
width: 56,
|
|
28
|
-
height: 10
|
|
29
|
-
rx: 2
|
|
43
|
+
height: 10
|
|
30
44
|
};
|
|
31
|
-
const
|
|
45
|
+
const lineYs = [22, 44, 66, 88];
|
|
46
|
+
const orbitXs = [-34, -6, 22, 50, 78, 106];
|
|
32
47
|
function classes(...names) {
|
|
33
48
|
return names.filter(Boolean).join(" ");
|
|
34
49
|
}
|
|
50
|
+
/** How much of each line is filled when `progress` of the whole is done, in reading order. */
|
|
51
|
+
export function mdbaseMarkLineFill(progress) {
|
|
52
|
+
const clamped = Math.min(1, Math.max(0, progress));
|
|
53
|
+
return lineYs.map((_, line) => Math.min(1, Math.max(0, clamped * lineYs.length - line)));
|
|
54
|
+
}
|
|
35
55
|
function Segment({ rect, index, accent = false }) {
|
|
36
|
-
return _jsx("rect", { className: `mdbase-mark-segment mdbase-mark-segment-${index} ${accent ? "mdbase-mark-accent" : "mdbase-mark-ink"}`,
|
|
56
|
+
return _jsx("rect", { className: `mdbase-mark-segment mdbase-mark-segment-${index} ${accent ? "mdbase-mark-accent" : "mdbase-mark-ink"}`, ...rect });
|
|
37
57
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
export function MdbaseMark({ motion, className }) {
|
|
43
|
-
const clipId = `mdbase-fences-${useId().replaceAll(":", "")}`;
|
|
44
|
-
return _jsxs("svg", { className: classes("mdbase-mark", className) + mdbaseMarkMotionClass(motion), viewBox: MDBASE_MARK_VIEW_BOX, "aria-hidden": "true", focusable: "false", children: [_jsx("defs", { children: _jsxs("clipPath", { id: clipId, children: [_jsx("rect", { x: "22", y: "22", width: "76", height: "10", rx: "2" }), _jsx("rect", { x: "22", y: "88", width: "76", height: "10", rx: "2" })] }) }), _jsx("g", { className: "mdbase-mark-fence mdbase-mark-fence-top", children: mdbaseMarkInkRects.slice(0, 3).map((rect, index) => _jsx(Segment, { rect: rect, index: index + 1 }, `${rect.x}-${rect.y}`)) }), _jsxs("g", { className: "mdbase-mark-row mdbase-mark-row-top", children: [_jsx(Segment, { rect: mdbaseMarkInkRects[3], index: 4 }), _jsx(Segment, { rect: mdbaseMarkAccentRect, index: 5, accent: true })] }), _jsxs("g", { className: "mdbase-mark-row mdbase-mark-row-bottom", children: [_jsx(Segment, { rect: mdbaseMarkInkRects[4], index: 6 }), _jsx(Segment, { rect: mdbaseMarkInkRects[5], index: 7 })] }), _jsx("g", { className: "mdbase-mark-fence mdbase-mark-fence-bottom", children: mdbaseMarkInkRects.slice(6).map((rect, index) => _jsx(Segment, { rect: rect, index: index + 8 }, `${rect.x}-${rect.y}`)) }), _jsx("g", { clipPath: `url(#${clipId})`, children: _jsx("g", { className: "mdbase-mark-conveyor-track", children: conveyorXs.flatMap((x) => [22, 88].map((y) => _jsx("rect", { x: x, y: y, width: "20", height: "10", rx: "2" }, `${x}-${y}`))) }) })] });
|
|
58
|
+
function Lines({ clipRows }) {
|
|
59
|
+
const rows = _jsxs(_Fragment, { children: [_jsxs("g", { className: "mdbase-mark-row mdbase-mark-row-top", children: [_jsx(Segment, { rect: mdbaseMarkInkRects[3], index: 4 }), _jsx(Segment, { rect: mdbaseMarkAccentRect, index: 5, accent: true })] }), _jsxs("g", { className: "mdbase-mark-row mdbase-mark-row-bottom", children: [_jsx(Segment, { rect: mdbaseMarkInkRects[4], index: 6 }), _jsx(Segment, { rect: mdbaseMarkInkRects[5], index: 7 })] })] });
|
|
60
|
+
return _jsxs(_Fragment, { children: [_jsx("g", { className: "mdbase-mark-fence mdbase-mark-fence-top", children: mdbaseMarkInkRects.slice(0, 3).map((rect, index) => _jsx(Segment, { rect: rect, index: index + 1 }, `${rect.x}-${rect.y}`)) }), clipRows ? _jsx("g", { clipPath: `url(#${clipRows})`, children: rows }) : rows, _jsx("g", { className: "mdbase-mark-fence mdbase-mark-fence-bottom", children: mdbaseMarkInkRects.slice(6).map((rect, index) => _jsx(Segment, { rect: rect, index: index + 8 }, `${rect.x}-${rect.y}`)) })] });
|
|
45
61
|
}
|
|
46
62
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
63
|
+
* The Frontmatter mark. Without `app` it is the platform's own; with `app` it is
|
|
64
|
+
* inverted, so the bars take the app's colour and the highlighted line stays ink.
|
|
65
|
+
*
|
|
66
|
+
* `motion` loops or plays an entrance, `signal` plays a one-shot reaction over
|
|
67
|
+
* whatever else is running, and `progress` (0 to 1) fills the lines in reading
|
|
68
|
+
* order over a faint copy of the mark. Animations live in `motion.css`.
|
|
49
69
|
*/
|
|
70
|
+
export function MdbaseMark({ app, motion, signal, progress, className }) {
|
|
71
|
+
const uid = useId().replaceAll(":", "");
|
|
72
|
+
// An entrance plays once per mark. Dropping its class afterwards stops it replaying when a signal
|
|
73
|
+
// remounts the lines, and remembering it stops a replay when a loop or progress hands back to it.
|
|
74
|
+
const [entering, setEntering] = useState(isEntrance(motion));
|
|
75
|
+
const played = useRef(undefined);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!isEntrance(motion) || played.current === motion)
|
|
78
|
+
return setEntering(false);
|
|
79
|
+
setEntering(true);
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
played.current = motion;
|
|
82
|
+
setEntering(false);
|
|
83
|
+
}, ENTRANCE_MS);
|
|
84
|
+
return () => clearTimeout(timer);
|
|
85
|
+
}, [motion]);
|
|
86
|
+
const showsProgress = typeof progress === "number" && Number.isFinite(progress);
|
|
87
|
+
const active = signal ? undefined
|
|
88
|
+
: showsProgress ? undefined
|
|
89
|
+
: isEntrance(motion) ? (entering ? motion : undefined)
|
|
90
|
+
: motion;
|
|
91
|
+
const lines = _jsx(Lines, { clipRows: active === "stream" ? `mdbase-rows-${uid}` : undefined });
|
|
92
|
+
return _jsxs("svg", { className: classes("mdbase-mark", app && "mdbase-app-mark", app && `is-${app}`, className, active && `mdbase-motion-${active}`, signal && `mdbase-signal-${signal.kind}`, showsProgress && "mdbase-mark-progress", !active && !signal && !showsProgress && "mdbase-mark-at-rest"), viewBox: MDBASE_MARK_VIEW_BOX, "aria-hidden": "true", focusable: "false", children: [active === "stream" && _jsx("defs", { children: _jsx("clipPath", { id: `mdbase-rows-${uid}`, children: _jsx("rect", { x: "22", y: "40", width: "76", height: "40" }) }) }), active === "orbit" && _jsxs("defs", { children: [_jsx("clipPath", { id: `mdbase-fence-top-${uid}`, children: _jsx("rect", { x: "22", y: "22", width: "76", height: "10" }) }), _jsx("clipPath", { id: `mdbase-fence-bottom-${uid}`, children: _jsx("rect", { x: "22", y: "88", width: "76", height: "10" }) })] }), showsProgress
|
|
93
|
+
? _jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx("clipPath", { id: `mdbase-progress-${uid}`, children: mdbaseMarkLineFill(progress).map((fill, line) => _jsx("rect", { className: "mdbase-mark-progress-fill", x: "22", y: lineYs[line] - 1, height: "12", style: { width: `${(76 * fill).toFixed(2)}px` } }, line)) }) }), _jsx("g", { className: "mdbase-mark-progress-ghost", children: _jsx(Lines, {}) }), _jsx("g", { clipPath: `url(#mdbase-progress-${uid})`, children: _jsx(Lines, {}) })] })
|
|
94
|
+
// Keyed by the signal so the same reaction can restart without replaying an entrance.
|
|
95
|
+
: _jsx("g", { className: "mdbase-mark-lines", children: lines }, signal?.id ?? 0), active === "orbit" && _jsxs(_Fragment, { children: [_jsx("g", { clipPath: `url(#mdbase-fence-top-${uid})`, children: _jsx("g", { className: "mdbase-mark-track mdbase-mark-track-top", children: orbitXs.map((x) => _jsx("rect", { x: x, y: "22", width: "20", height: "10" }, x)) }) }), _jsx("g", { clipPath: `url(#mdbase-fence-bottom-${uid})`, children: _jsx("g", { className: "mdbase-mark-track mdbase-mark-track-bottom", children: orbitXs.map((x) => _jsx("rect", { x: x, y: "88", width: "20", height: "10" }, x)) }) })] })] });
|
|
96
|
+
}
|
|
97
|
+
/** An app's mark. Kept for callers that only ever draw an app's still mark. */
|
|
50
98
|
export function MdbaseAppMark({ app, className }) {
|
|
51
|
-
return
|
|
99
|
+
return _jsx(MdbaseMark, { app: app, className: className });
|
|
52
100
|
}
|
|
53
101
|
/**
|
|
54
102
|
* `mdbase` followed by the product's quieter name. Editor carries the platform mark
|
|
55
103
|
* as the canonical app; Reader and Writer carry their app marks.
|
|
56
104
|
*/
|
|
57
|
-
export function Wordmark({ app, motion }) {
|
|
58
|
-
return _jsxs("span", { className: "wordmark", children: [app === "editor"
|
|
59
|
-
? _jsx(MdbaseMark, { motion: motion, className: "wordmark-mark" })
|
|
60
|
-
: _jsx(MdbaseAppMark, { app: app, className: "wordmark-mark" }), _jsxs("span", { className: "wordmark-label", children: [_jsx("span", { children: "mdbase" }), _jsx("strong", { children: app })] })] });
|
|
105
|
+
export function Wordmark({ app, motion, signal, progress }) {
|
|
106
|
+
return _jsxs("span", { className: "wordmark", children: [_jsx(MdbaseMark, { app: app === "editor" ? undefined : app, motion: motion, signal: signal, progress: progress, className: "wordmark-mark" }), _jsxs("span", { className: "wordmark-label", children: [_jsx("span", { children: "mdbase" }), _jsx("strong", { children: app })] })] });
|
|
61
107
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export declare const FEEDBACK_MAX_MESSAGE_LENGTH = 5000;
|
|
2
|
+
export declare const FEEDBACK_MAX_SCREENSHOT_BYTES: number;
|
|
3
|
+
export declare const FEEDBACK_ERROR_CODES: readonly ["cancelled", "http_error", "invalid_response", "outcome_unknown", "partial_failure", "timeout", "network_error", "unknown_error", "save_failed", "source_open_failed", "preview_failed"];
|
|
4
|
+
export type FeedbackErrorCode = typeof FEEDBACK_ERROR_CODES[number];
|
|
5
|
+
export type FeedbackTopic = "problem" | "idea" | "appreciation";
|
|
6
|
+
export interface FeedbackApplication {
|
|
7
|
+
product: "mdbase editor" | "mdbase reader" | "mdbase writer" | "mdbase connect";
|
|
8
|
+
source_view: string;
|
|
9
|
+
build_revision: string | null;
|
|
10
|
+
environment: "production" | "staging" | "development" | "lab";
|
|
11
|
+
}
|
|
12
|
+
export interface FeedbackFailure {
|
|
13
|
+
code: FeedbackErrorCode;
|
|
14
|
+
status?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface FeedbackDiagnosticEvent extends FeedbackFailure {
|
|
17
|
+
at: string;
|
|
18
|
+
}
|
|
19
|
+
export interface FeedbackDiagnostics {
|
|
20
|
+
schema_version: 2;
|
|
21
|
+
browser: string;
|
|
22
|
+
operating_system: string;
|
|
23
|
+
viewport: "compact" | "medium" | "wide";
|
|
24
|
+
events: FeedbackDiagnosticEvent[];
|
|
25
|
+
}
|
|
26
|
+
export interface FeedbackScreenshot {
|
|
27
|
+
media_type: "image/png" | "image/jpeg";
|
|
28
|
+
filename: "screenshot.png" | "screenshot.jpg";
|
|
29
|
+
content_base64: string;
|
|
30
|
+
}
|
|
31
|
+
export interface FeedbackSubmission {
|
|
32
|
+
schema_version: 2;
|
|
33
|
+
request_id: string;
|
|
34
|
+
application: FeedbackApplication;
|
|
35
|
+
topic: FeedbackTopic;
|
|
36
|
+
message: string;
|
|
37
|
+
reply_email?: string;
|
|
38
|
+
context?: {
|
|
39
|
+
collection_name: string;
|
|
40
|
+
};
|
|
41
|
+
diagnostics?: FeedbackDiagnostics;
|
|
42
|
+
screenshot?: FeedbackScreenshot;
|
|
43
|
+
turnstile_token?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare const feedbackTopics: {
|
|
46
|
+
problem: {
|
|
47
|
+
label: string;
|
|
48
|
+
prompt: string;
|
|
49
|
+
placeholder: string;
|
|
50
|
+
thanks: string;
|
|
51
|
+
};
|
|
52
|
+
idea: {
|
|
53
|
+
label: string;
|
|
54
|
+
prompt: string;
|
|
55
|
+
placeholder: string;
|
|
56
|
+
thanks: string;
|
|
57
|
+
};
|
|
58
|
+
appreciation: {
|
|
59
|
+
label: string;
|
|
60
|
+
prompt: string;
|
|
61
|
+
placeholder: string;
|
|
62
|
+
thanks: string;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/** Each provider owns its bounded, short-lived error buffer; never accepts messages or request data. */
|
|
66
|
+
export declare function feedbackEvents(events: readonly FeedbackDiagnosticEvent[], now?: number): FeedbackDiagnosticEvent[];
|
|
67
|
+
export declare function feedbackDiagnostics(events: readonly FeedbackDiagnosticEvent[]): FeedbackDiagnostics;
|
|
68
|
+
export declare function resolveFeedbackEndpoint(configured: string | undefined, development?: boolean): string | null;
|
|
69
|
+
export declare function feedbackApplication(product: FeedbackApplication["product"], sourceView: string, revision?: string, environment?: string): FeedbackApplication;
|
|
70
|
+
export declare function sendFeedback(endpoint: string, submission: FeedbackSubmission, signal: AbortSignal): Promise<void>;
|
|
71
|
+
export declare function screenshotUrl(screenshot: FeedbackScreenshot): string;
|
|
72
|
+
export declare function screenshotFromCanvas(canvas: HTMLCanvasElement, type?: "image/png" | "image/jpeg"): Promise<FeedbackScreenshot>;
|
|
73
|
+
export declare function readFeedbackScreenshot(file: File): Promise<FeedbackScreenshot>;
|
|
74
|
+
/** Caller closes the native dialog first. Media is always stopped, including cancellation/unmount. */
|
|
75
|
+
export declare function captureFeedbackScreenshot(signal: AbortSignal): Promise<FeedbackScreenshot>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
export const FEEDBACK_MAX_MESSAGE_LENGTH = 5_000;
|
|
2
|
+
export const FEEDBACK_MAX_SCREENSHOT_BYTES = 3 * 1024 * 1024;
|
|
3
|
+
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
|
+
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." }
|
|
8
|
+
};
|
|
9
|
+
/** Each provider owns its bounded, short-lived error buffer; never accepts messages or request data. */
|
|
10
|
+
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 }) }));
|
|
12
|
+
}
|
|
13
|
+
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
|
+
};
|
|
24
|
+
}
|
|
25
|
+
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
|
+
}
|
|
38
|
+
}
|
|
39
|
+
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" };
|
|
43
|
+
}
|
|
44
|
+
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);
|
|
64
|
+
}
|
|
65
|
+
export function screenshotUrl(screenshot) { return `data:${screenshot.media_type};base64,${screenshot.content_base64}`; }
|
|
66
|
+
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);
|
|
71
|
+
}
|
|
72
|
+
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())) };
|
|
77
|
+
}
|
|
78
|
+
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
|
+
}
|
|
107
|
+
}
|
|
108
|
+
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.");
|
|
111
|
+
}
|
|
112
|
+
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
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new Error("The screenshot does not contain a valid PNG or JPEG image.");
|
|
137
|
+
}
|
|
138
|
+
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
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Caller closes the native dialog first. Media is always stopped, including cancellation/unmount. */
|
|
159
|
+
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
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MdbaseMarkLoop, MdbaseMarkSignal, MdbaseMarkSignalKind } from "./brand.js";
|
|
2
|
+
/**
|
|
3
|
+
* What the app's mark is showing for work happening anywhere in the page. The app
|
|
4
|
+
* switcher reads it, so a save, a failure or an upload deep inside an app reaches
|
|
5
|
+
* the mark without threading state through the tree.
|
|
6
|
+
*/
|
|
7
|
+
export type MdbaseMarkActivity = Readonly<{
|
|
8
|
+
signal: MdbaseMarkSignal | null;
|
|
9
|
+
/** Mean of every tracked operation, 0 to 1; null when nothing is tracked. */
|
|
10
|
+
progress: number | null;
|
|
11
|
+
/** The most recently held loop; null when nothing is busy. */
|
|
12
|
+
busy: MdbaseMarkLoop | null;
|
|
13
|
+
}>;
|
|
14
|
+
export type MdbaseMarkProgress = Readonly<{
|
|
15
|
+
update(fraction: number): void;
|
|
16
|
+
/** Ends the operation and plays `saved`. */
|
|
17
|
+
finish(): void;
|
|
18
|
+
/** Ends the operation and plays `error`. */
|
|
19
|
+
fail(): void;
|
|
20
|
+
/** Ends the operation quietly. */
|
|
21
|
+
cancel(): void;
|
|
22
|
+
}>;
|
|
23
|
+
/** Plays a one-shot reaction on the mark. A fresh error is never covered by a save. */
|
|
24
|
+
export declare function signalMdbaseMark(kind: MdbaseMarkSignalKind): void;
|
|
25
|
+
/** Shows an operation's known progress on the mark until it finishes, fails or is cancelled. */
|
|
26
|
+
export declare function trackMdbaseMarkProgress(initial?: number): MdbaseMarkProgress;
|
|
27
|
+
/** Loops the mark until the returned release is called. Releasing twice is harmless. */
|
|
28
|
+
export declare function holdMdbaseMarkBusy(loop: MdbaseMarkLoop): () => void;
|
|
29
|
+
export declare function getMdbaseMarkActivity(): MdbaseMarkActivity;
|
|
30
|
+
export declare function useMdbaseMarkActivity(): MdbaseMarkActivity;
|
|
31
|
+
/**
|
|
32
|
+
* Shows `fraction` on the mark while it is a number. Becoming null or unmounting ends the
|
|
33
|
+
* operation quietly; call `signalMdbaseMark` separately if the end deserves a reaction.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useMdbaseMarkProgress(fraction: number | null | undefined): void;
|
|
36
|
+
/** Loops the mark while `loop` is set. */
|
|
37
|
+
export declare function useMdbaseMarkBusy(loop: MdbaseMarkLoop | null | undefined | false): void;
|
|
38
|
+
/** For tests: forget every signal, operation and hold. */
|
|
39
|
+
export declare function resetMdbaseMarkActivity(): void;
|