@3sln/nook 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +615 -2
- package/package.json +52 -4
- package/src/bl/commands.js +27 -0
- package/src/bl/menu.js +311 -0
- package/src/capture.js +129 -0
- package/src/index.js +2136 -0
- package/src/providers/attention.js +99 -0
- package/src/providers/onboarding.js +35 -0
- package/src/ui/bounds.js +159 -0
- package/src/ui/commandPalette.js +236 -0
- package/src/ui/enumPicker.js +307 -0
- package/src/ui/feedback.js +565 -0
- package/src/ui/icons.js +46 -0
- package/src/ui/localization.js +95 -0
- package/src/ui/motion.js +49 -0
- package/src/ui/styles.js +1876 -0
- package/src/ui/swipeDial.js +169 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { icon } from "./icons.js";
|
|
2
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
3
|
+
export default function swipeDial({ dock, make, i18n, signal }) {
|
|
4
|
+
const root = make("div", "swipe-dial");
|
|
5
|
+
root.className = "swipe-dial";
|
|
6
|
+
root.hidden = true;
|
|
7
|
+
root.setAttribute("aria-hidden", "true");
|
|
8
|
+
const options = [
|
|
9
|
+
{ id: "all", label: "All actions", icon: "menu" },
|
|
10
|
+
{ id: "settings", label: "Settings", icon: "settings" },
|
|
11
|
+
].map((option) => {
|
|
12
|
+
const el = make("div", "swipe-dial-option");
|
|
13
|
+
el.className = "swipe-dial-option";
|
|
14
|
+
const mark = make("span", "swipe-dial-icon");
|
|
15
|
+
mark.innerHTML = icon(option.icon);
|
|
16
|
+
const text = make("span", "swipe-dial-label", option.label);
|
|
17
|
+
el.append(mark, text);
|
|
18
|
+
root.append(el);
|
|
19
|
+
return { ...option, el };
|
|
20
|
+
});
|
|
21
|
+
dock.overlayAnchor.append(root);
|
|
22
|
+
let timer, pending, selected;
|
|
23
|
+
const rect = () =>
|
|
24
|
+
dock.anchorRect || {
|
|
25
|
+
left: 0,
|
|
26
|
+
top: 0,
|
|
27
|
+
right: dock.ownerDocument.defaultView.innerWidth,
|
|
28
|
+
bottom: dock.ownerDocument.defaultView.innerHeight,
|
|
29
|
+
width: dock.ownerDocument.defaultView.innerWidth,
|
|
30
|
+
height: dock.ownerDocument.defaultView.innerHeight,
|
|
31
|
+
};
|
|
32
|
+
const render = (pointer) => {
|
|
33
|
+
const bounds = rect();
|
|
34
|
+
const distance = Math.hypot(
|
|
35
|
+
pointer.lastX - pointer.x,
|
|
36
|
+
pointer.lastY - pointer.y,
|
|
37
|
+
);
|
|
38
|
+
root.style.setProperty(
|
|
39
|
+
"--nook-dial-progress",
|
|
40
|
+
String(clamp((distance - 28) / 72, 0, 1)),
|
|
41
|
+
);
|
|
42
|
+
root.style.setProperty(
|
|
43
|
+
"--nook-dial-origin-x",
|
|
44
|
+
`${pointer.x - bounds.left}px`,
|
|
45
|
+
);
|
|
46
|
+
root.style.setProperty(
|
|
47
|
+
"--nook-dial-origin-y",
|
|
48
|
+
`${pointer.y - bounds.top}px`,
|
|
49
|
+
);
|
|
50
|
+
const position = dock.position;
|
|
51
|
+
const sx = position.includes("right") ? -1 : 1,
|
|
52
|
+
sy = position.startsWith("bottom")
|
|
53
|
+
? -1
|
|
54
|
+
: position.startsWith("top")
|
|
55
|
+
? 1
|
|
56
|
+
: -1;
|
|
57
|
+
const points = {
|
|
58
|
+
all: [pointer.x + sx * 152, pointer.y],
|
|
59
|
+
settings: [pointer.x, pointer.y + sy * 152],
|
|
60
|
+
};
|
|
61
|
+
const placed = [];
|
|
62
|
+
let next = null;
|
|
63
|
+
for (const option of options) {
|
|
64
|
+
option.el.hidden = !dock.surfaceAvailable(option.id);
|
|
65
|
+
if (option.el.hidden) continue;
|
|
66
|
+
const point = points[option.id];
|
|
67
|
+
option.x = clamp(point[0], bounds.left + 52, bounds.right - 52);
|
|
68
|
+
option.y = clamp(point[1], bounds.top + 44, bounds.bottom - 44);
|
|
69
|
+
// Preserve a clear inner ring even when viewport clamping moves a target.
|
|
70
|
+
// Omit targets that cannot fit rather than overlapping the handle or peers.
|
|
71
|
+
if (
|
|
72
|
+
Math.hypot(option.x - pointer.x, option.y - pointer.y) < 104 ||
|
|
73
|
+
placed.some(
|
|
74
|
+
(other) =>
|
|
75
|
+
Math.abs(option.x - other.x) < 96 &&
|
|
76
|
+
Math.abs(option.y - other.y) < 80,
|
|
77
|
+
)
|
|
78
|
+
) {
|
|
79
|
+
option.el.hidden = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
placed.push(option);
|
|
83
|
+
option.el.style.left = `${option.x - bounds.left}px`;
|
|
84
|
+
option.el.style.top = `${option.y - bounds.top}px`;
|
|
85
|
+
const target = option.el.getBoundingClientRect();
|
|
86
|
+
if (
|
|
87
|
+
target.width > 0 &&
|
|
88
|
+
target.height > 0 &&
|
|
89
|
+
pointer.lastX >= target.left &&
|
|
90
|
+
pointer.lastX <= target.right &&
|
|
91
|
+
pointer.lastY >= target.top &&
|
|
92
|
+
pointer.lastY <= target.bottom
|
|
93
|
+
)
|
|
94
|
+
next = option;
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
distance < 56 ||
|
|
98
|
+
pointer.lastX < bounds.left - 16 ||
|
|
99
|
+
pointer.lastX > bounds.right + 16 ||
|
|
100
|
+
pointer.lastY < bounds.top - 16 ||
|
|
101
|
+
pointer.lastY > bounds.bottom + 16
|
|
102
|
+
)
|
|
103
|
+
next = null;
|
|
104
|
+
for (const option of options)
|
|
105
|
+
option.el.toggleAttribute("data-selected", option === next);
|
|
106
|
+
if (next !== selected) {
|
|
107
|
+
selected = next;
|
|
108
|
+
i18n.text(
|
|
109
|
+
dock.live,
|
|
110
|
+
next ? "Release to open {menu}." : "",
|
|
111
|
+
next ? () => ({ menu: i18n.t(next.label) }) : undefined,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const cancel = () => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
timer = undefined;
|
|
118
|
+
pending = null;
|
|
119
|
+
selected = null;
|
|
120
|
+
root.hidden = true;
|
|
121
|
+
root.style.setProperty("--nook-dial-progress", "0");
|
|
122
|
+
};
|
|
123
|
+
const move = (pointer) => {
|
|
124
|
+
if (pointer.drag) return;
|
|
125
|
+
if (!root.hidden) {
|
|
126
|
+
render(pointer);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (Math.hypot(pointer.lastX - pointer.x, pointer.lastY - pointer.y) < 28)
|
|
130
|
+
return;
|
|
131
|
+
pointer.swipeStarted = true;
|
|
132
|
+
if (timer !== undefined) return;
|
|
133
|
+
pending = pointer;
|
|
134
|
+
timer = setTimeout(() => {
|
|
135
|
+
timer = undefined;
|
|
136
|
+
if (signal.aborted || dock.pointer !== pending || pending.drag) return;
|
|
137
|
+
const bounds = rect();
|
|
138
|
+
if (bounds.width < 144 || bounds.height < 144) return;
|
|
139
|
+
root.hidden = false;
|
|
140
|
+
render(pending);
|
|
141
|
+
}, 180);
|
|
142
|
+
};
|
|
143
|
+
signal.addEventListener(
|
|
144
|
+
"abort",
|
|
145
|
+
() => {
|
|
146
|
+
cancel();
|
|
147
|
+
root.remove();
|
|
148
|
+
},
|
|
149
|
+
{ once: true },
|
|
150
|
+
);
|
|
151
|
+
return {
|
|
152
|
+
move,
|
|
153
|
+
cancel,
|
|
154
|
+
release(x, y) {
|
|
155
|
+
if (root.hidden || !pending) return null;
|
|
156
|
+
pending.lastX = x;
|
|
157
|
+
pending.lastY = y;
|
|
158
|
+
render(pending);
|
|
159
|
+
return selected?.id || null;
|
|
160
|
+
},
|
|
161
|
+
get shown() {
|
|
162
|
+
return !root.hidden;
|
|
163
|
+
},
|
|
164
|
+
get selection() {
|
|
165
|
+
return selected?.id || null;
|
|
166
|
+
},
|
|
167
|
+
root,
|
|
168
|
+
};
|
|
169
|
+
}
|