@mdbase-dev/ui 0.1.0-beta.123 → 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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
/** Matches the signal animations in `motion.css`. */
|
|
3
|
+
const SIGNAL_MS = { saved: 900, error: 1500 };
|
|
4
|
+
/** Several notices often report the same save; one reaction is enough. */
|
|
5
|
+
const REPEAT_MS = 400;
|
|
6
|
+
const IDLE = { signal: null, progress: null, busy: null };
|
|
7
|
+
let snapshot = IDLE;
|
|
8
|
+
let signal = null;
|
|
9
|
+
let signalAt = 0;
|
|
10
|
+
let signalTimer;
|
|
11
|
+
let nextId = 1;
|
|
12
|
+
const tasks = new Map();
|
|
13
|
+
const holds = [];
|
|
14
|
+
const listeners = new Set();
|
|
15
|
+
function publish() {
|
|
16
|
+
const progress = tasks.size
|
|
17
|
+
? [...tasks.values()].reduce((sum, fraction) => sum + fraction, 0) / tasks.size
|
|
18
|
+
: null;
|
|
19
|
+
snapshot = { signal, progress, busy: holds.at(-1)?.loop ?? null };
|
|
20
|
+
for (const listener of listeners)
|
|
21
|
+
listener();
|
|
22
|
+
}
|
|
23
|
+
function clamp(fraction) {
|
|
24
|
+
return Number.isFinite(fraction) ? Math.min(1, Math.max(0, fraction)) : 0;
|
|
25
|
+
}
|
|
26
|
+
/** Plays a one-shot reaction on the mark. A fresh error is never covered by a save. */
|
|
27
|
+
export function signalMdbaseMark(kind) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (signal && now - signalAt < REPEAT_MS && (signal.kind === kind || signal.kind === "error"))
|
|
30
|
+
return;
|
|
31
|
+
signal = { kind, id: nextId++ };
|
|
32
|
+
signalAt = now;
|
|
33
|
+
clearTimeout(signalTimer);
|
|
34
|
+
signalTimer = setTimeout(() => {
|
|
35
|
+
signal = null;
|
|
36
|
+
publish();
|
|
37
|
+
}, SIGNAL_MS[kind]);
|
|
38
|
+
publish();
|
|
39
|
+
}
|
|
40
|
+
/** Shows an operation's known progress on the mark until it finishes, fails or is cancelled. */
|
|
41
|
+
export function trackMdbaseMarkProgress(initial = 0) {
|
|
42
|
+
const id = nextId++;
|
|
43
|
+
tasks.set(id, clamp(initial));
|
|
44
|
+
publish();
|
|
45
|
+
const end = (kind) => {
|
|
46
|
+
if (!tasks.delete(id))
|
|
47
|
+
return;
|
|
48
|
+
publish();
|
|
49
|
+
if (kind)
|
|
50
|
+
signalMdbaseMark(kind);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
update(fraction) {
|
|
54
|
+
if (!tasks.has(id))
|
|
55
|
+
return;
|
|
56
|
+
tasks.set(id, clamp(fraction));
|
|
57
|
+
publish();
|
|
58
|
+
},
|
|
59
|
+
finish: () => end("saved"),
|
|
60
|
+
fail: () => end("error"),
|
|
61
|
+
cancel: () => end()
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Loops the mark until the returned release is called. Releasing twice is harmless. */
|
|
65
|
+
export function holdMdbaseMarkBusy(loop) {
|
|
66
|
+
const id = nextId++;
|
|
67
|
+
holds.push({ id, loop });
|
|
68
|
+
publish();
|
|
69
|
+
return () => {
|
|
70
|
+
const index = holds.findIndex((hold) => hold.id === id);
|
|
71
|
+
if (index < 0)
|
|
72
|
+
return;
|
|
73
|
+
holds.splice(index, 1);
|
|
74
|
+
publish();
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function subscribe(listener) {
|
|
78
|
+
listeners.add(listener);
|
|
79
|
+
return () => listeners.delete(listener);
|
|
80
|
+
}
|
|
81
|
+
export function getMdbaseMarkActivity() {
|
|
82
|
+
return snapshot;
|
|
83
|
+
}
|
|
84
|
+
export function useMdbaseMarkActivity() {
|
|
85
|
+
return useSyncExternalStore(subscribe, getMdbaseMarkActivity, () => IDLE);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Shows `fraction` on the mark while it is a number. Becoming null or unmounting ends the
|
|
89
|
+
* operation quietly; call `signalMdbaseMark` separately if the end deserves a reaction.
|
|
90
|
+
*/
|
|
91
|
+
export function useMdbaseMarkProgress(fraction) {
|
|
92
|
+
const handle = useRef(null);
|
|
93
|
+
const tracking = typeof fraction === "number";
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!tracking)
|
|
96
|
+
return;
|
|
97
|
+
const progress = trackMdbaseMarkProgress();
|
|
98
|
+
handle.current = progress;
|
|
99
|
+
return () => {
|
|
100
|
+
progress.cancel();
|
|
101
|
+
handle.current = null;
|
|
102
|
+
};
|
|
103
|
+
}, [tracking]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (typeof fraction === "number")
|
|
106
|
+
handle.current?.update(fraction);
|
|
107
|
+
}, [fraction]);
|
|
108
|
+
}
|
|
109
|
+
/** Loops the mark while `loop` is set. */
|
|
110
|
+
export function useMdbaseMarkBusy(loop) {
|
|
111
|
+
useEffect(() => (loop ? holdMdbaseMarkBusy(loop) : undefined), [loop]);
|
|
112
|
+
}
|
|
113
|
+
/** For tests: forget every signal, operation and hold. */
|
|
114
|
+
export function resetMdbaseMarkActivity() {
|
|
115
|
+
clearTimeout(signalTimer);
|
|
116
|
+
signal = null;
|
|
117
|
+
signalAt = 0;
|
|
118
|
+
tasks.clear();
|
|
119
|
+
holds.length = 0;
|
|
120
|
+
publish();
|
|
121
|
+
}
|
package/dist/screens.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { JSX, ReactNode } from "react";
|
|
2
2
|
import type { MdbaseAppId } from "./apps.js";
|
|
3
3
|
/**
|
|
4
|
-
* What an app shows while it opens a collection: its wordmark,
|
|
5
|
-
* saying what it is waiting for. A failure keeps the same place
|
|
4
|
+
* What an app shows while it opens a collection: its wordmark, scanning, and a quiet line
|
|
5
|
+
* saying what it is waiting for. A failure keeps the same place, shakes the mark once and
|
|
6
|
+
* offers a retry.
|
|
6
7
|
*/
|
|
7
8
|
export declare function OpeningScreen({ app, title, detail, error, onRetry }: {
|
|
8
9
|
readonly app: MdbaseAppId;
|
package/dist/screens.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Wordmark } from "./brand.js";
|
|
3
3
|
/**
|
|
4
|
-
* What an app shows while it opens a collection: its wordmark,
|
|
5
|
-
* saying what it is waiting for. A failure keeps the same place
|
|
4
|
+
* What an app shows while it opens a collection: its wordmark, scanning, and a quiet line
|
|
5
|
+
* saying what it is waiting for. A failure keeps the same place, shakes the mark once and
|
|
6
|
+
* offers a retry.
|
|
6
7
|
*/
|
|
7
8
|
export function OpeningScreen({ app, title, detail, error, onRetry }) {
|
|
8
|
-
return _jsx("main", { className: "mdbase-opening", "data-loading-state": error ? "failed" : "opening", "aria-label": title, "aria-busy": !error, children: _jsxs("div", { className: "mdbase-opening-message", role: error ? "alert" : "status", children: [_jsx(Wordmark, { app: app }), _jsxs("div", { children: [_jsx("p", { children: error ?? title }), !error && detail && _jsx("small", { children: detail })] }), error && onRetry && _jsx("button", { type: "button", className: "mdbase-button", onClick: onRetry, children: "Try again" })] }) });
|
|
9
|
+
return _jsx("main", { className: "mdbase-opening", "data-loading-state": error ? "failed" : "opening", "aria-label": title, "aria-busy": !error, children: _jsxs("div", { className: "mdbase-opening-message", role: error ? "alert" : "status", children: [_jsx(Wordmark, { app: app, motion: "scan", signal: error ? openingFailed : null }), _jsxs("div", { children: [_jsx("p", { children: error ?? title }), !error && detail && _jsx("small", { children: detail })] }), error && onRetry && _jsx("button", { type: "button", className: "mdbase-button", onClick: onRetry, children: "Try again" })] }) });
|
|
9
10
|
}
|
|
11
|
+
const openingFailed = { kind: "error", id: 1 };
|
|
10
12
|
/**
|
|
11
13
|
* The screen before a collection is open: the app's wordmark, a headline, what the app will
|
|
12
14
|
* do, what went wrong if anything, and then the actions (children) for connecting. A status
|
|
@@ -14,5 +16,5 @@ export function OpeningScreen({ app, title, detail, error, onRetry }) {
|
|
|
14
16
|
* disclosure.
|
|
15
17
|
*/
|
|
16
18
|
export function ConnectLayout({ app, title, lead, status, error, detail, footnote, children }) {
|
|
17
|
-
return _jsx("main", { className: "mdbase-connect-screen", children: _jsxs("section", { children: [_jsx(Wordmark, { app: app }), _jsx("h1", { children: title }), lead && _jsx("p", { className: "mdbase-connect-lead", children: lead }), status && status !== error && _jsx("p", { className: "mdbase-connect-status", role: "status", children: status }), error && _jsx("p", { className: "mdbase-connect-error", role: "alert", children: error }), detail && _jsxs("details", { className: "mdbase-connect-detail", children: [_jsx("summary", { children: "Details" }), _jsx("code", { children: detail })] }), children, footnote && _jsx("p", { className: "mdbase-connect-footnote", children: footnote })] }) });
|
|
19
|
+
return _jsx("main", { className: "mdbase-connect-screen", children: _jsxs("section", { children: [_jsx(Wordmark, { app: app, motion: "assemble" }), _jsx("h1", { children: title }), lead && _jsx("p", { className: "mdbase-connect-lead", children: lead }), status && status !== error && _jsx("p", { className: "mdbase-connect-status", role: "status", children: status }), error && _jsx("p", { className: "mdbase-connect-error", role: "alert", children: error }), detail && _jsxs("details", { className: "mdbase-connect-detail", children: [_jsx("summary", { children: "Details" }), _jsx("code", { children: detail })] }), children, footnote && _jsx("p", { className: "mdbase-connect-footnote", children: footnote })] }) });
|
|
18
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mdbase-dev/ui",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.124",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,10 +34,18 @@
|
|
|
34
34
|
"types": "./dist/CommandPalette.d.ts",
|
|
35
35
|
"import": "./dist/CommandPalette.js"
|
|
36
36
|
},
|
|
37
|
+
"./feedback": {
|
|
38
|
+
"types": "./dist/Feedback.d.ts",
|
|
39
|
+
"import": "./dist/Feedback.js"
|
|
40
|
+
},
|
|
37
41
|
"./dialog": {
|
|
38
42
|
"types": "./dist/Dialog.d.ts",
|
|
39
43
|
"import": "./dist/Dialog.js"
|
|
40
44
|
},
|
|
45
|
+
"./mark-activity": {
|
|
46
|
+
"types": "./dist/mark-activity.d.ts",
|
|
47
|
+
"import": "./dist/mark-activity.js"
|
|
48
|
+
},
|
|
41
49
|
"./permission-selection": {
|
|
42
50
|
"types": "./dist/permission-selection.d.ts",
|
|
43
51
|
"import": "./dist/permission-selection.js"
|
|
@@ -73,6 +81,7 @@
|
|
|
73
81
|
"./brand.css": "./css/brand.css",
|
|
74
82
|
"./controls.css": "./css/controls.css",
|
|
75
83
|
"./fonts.css": "./css/fonts.css",
|
|
84
|
+
"./feedback.css": "./css/feedback.css",
|
|
76
85
|
"./motion.css": "./css/motion.css",
|
|
77
86
|
"./palette.css": "./css/palette.css",
|
|
78
87
|
"./screens.css": "./css/screens.css",
|