@3sln/nook 0.1.0 → 0.2.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/README.md +97 -46
- package/package.json +1 -1
- package/src/bl/menu.js +10 -7
- package/src/index.js +300 -128
- package/src/providers/onboarding.js +1 -0
- package/src/ui/bounds.js +1 -0
- package/src/ui/feedback.js +14 -3
- package/src/ui/keyboardLabels.js +31 -0
- package/src/ui/styles.js +179 -95
- package/src/ui/swipeDial.js +325 -105
- package/src/ui/tutorialGraphics.js +56 -0
package/src/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { keyboardLabels } from "./ui/keyboardLabels.js";
|
|
2
|
+
import { tutorialGraphic } from "./ui/tutorialGraphics.js";
|
|
3
|
+
import swipeDial, { directionalChoice } from "./ui/swipeDial.js";
|
|
2
4
|
import bounds, { hasAppContent } from "./ui/bounds.js";
|
|
3
5
|
import localization from "./ui/localization.js";
|
|
4
6
|
import feedbackUI from "./ui/feedback.js";
|
|
@@ -28,7 +30,8 @@ import {
|
|
|
28
30
|
import styles from "./ui/styles.js";
|
|
29
31
|
|
|
30
32
|
const mark = icon("nook");
|
|
31
|
-
const tags =
|
|
33
|
+
const tags =
|
|
34
|
+
"nook-item,nook-action,nook-menu,nook-panel,nook-setting,nook-command";
|
|
32
35
|
const make = (tag, part, text) => {
|
|
33
36
|
const node = document.createElement(tag);
|
|
34
37
|
if (part) node.setAttribute("part", part);
|
|
@@ -54,9 +57,36 @@ const slotText = (item, slot) => {
|
|
|
54
57
|
: node?.textContent
|
|
55
58
|
)?.trim();
|
|
56
59
|
};
|
|
60
|
+
const isLeaf = (item) => ["nook-item", "nook-action"].includes(item.localName);
|
|
61
|
+
const commandTarget = (item) => {
|
|
62
|
+
if (!isLeaf(item) || !item.hasAttribute("command")) return null;
|
|
63
|
+
const dock = item.closest("nook-dock"),
|
|
64
|
+
id = item.getAttribute("command")?.trim();
|
|
65
|
+
if (!dock || !id) return null;
|
|
66
|
+
return (
|
|
67
|
+
[...dock.querySelectorAll("nook-command")].find(
|
|
68
|
+
(command) => command.id === id && command.closest("nook-dock") === dock,
|
|
69
|
+
) || null
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
const iconSlot = (item) =>
|
|
73
|
+
childSlot(item, "icon") ||
|
|
74
|
+
(commandTarget(item) && childSlot(commandTarget(item), "icon"));
|
|
57
75
|
const label = (item) =>
|
|
58
|
-
item.getAttribute("label") ||
|
|
59
|
-
|
|
76
|
+
item.getAttribute("label") ||
|
|
77
|
+
slotText(item, "label") ||
|
|
78
|
+
commandTarget(item)?.getAttribute("name") ||
|
|
79
|
+
"Untitled";
|
|
80
|
+
const available = (item) => {
|
|
81
|
+
if (item.closest("[hidden],[disabled],template")) return false;
|
|
82
|
+
if (!isLeaf(item) || !item.hasAttribute("command")) return true;
|
|
83
|
+
const command = commandTarget(item);
|
|
84
|
+
return Boolean(
|
|
85
|
+
command &&
|
|
86
|
+
validCommand(command) &&
|
|
87
|
+
!command.closest("[hidden],[disabled],template"),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
60
90
|
|
|
61
91
|
export class NookItem extends HTMLElement {
|
|
62
92
|
constructor() {
|
|
@@ -84,7 +114,18 @@ export class NookItem extends HTMLElement {
|
|
|
84
114
|
set root(value) {
|
|
85
115
|
this.toggleAttribute("root", Boolean(value));
|
|
86
116
|
}
|
|
117
|
+
get command() {
|
|
118
|
+
return this.getAttribute("command") || "";
|
|
119
|
+
}
|
|
120
|
+
set command(value) {
|
|
121
|
+
if (value == null || value === "") this.removeAttribute("command");
|
|
122
|
+
else this.setAttribute("command", String(value));
|
|
123
|
+
}
|
|
87
124
|
get keybind() {
|
|
125
|
+
if (isLeaf(this) && this.hasAttribute("command"))
|
|
126
|
+
return commandTarget(this)?.getAttribute("keybind") || "";
|
|
127
|
+
// Legacy actions retain their old binding; new items put bindings on commands.
|
|
128
|
+
if (this.localName === "nook-item") return "";
|
|
88
129
|
return this.getAttribute("keybind") || "";
|
|
89
130
|
}
|
|
90
131
|
set keybind(value) {
|
|
@@ -166,7 +207,8 @@ export class NookDock extends HTMLElement {
|
|
|
166
207
|
this.anchor.append(this.handle, this.live, this.rootMeasure);
|
|
167
208
|
this.overlayAnchor = this.make("div", "overlay-anchor");
|
|
168
209
|
this.overlayAnchor.className = "anchor overlay-anchor";
|
|
169
|
-
|
|
210
|
+
// Keep the handle above overlay material when their configured elevations match.
|
|
211
|
+
this.shadowRoot.append(this.overlayAnchor, this.anchor);
|
|
170
212
|
const mirrored = [
|
|
171
213
|
"data-corner",
|
|
172
214
|
"data-position",
|
|
@@ -236,6 +278,7 @@ export class NookDock extends HTMLElement {
|
|
|
236
278
|
});
|
|
237
279
|
}
|
|
238
280
|
connectedCallback() {
|
|
281
|
+
this.queryReady = false;
|
|
239
282
|
this.syncLayers();
|
|
240
283
|
this.layerObserver.observe(this.anchor, {
|
|
241
284
|
attributes: true,
|
|
@@ -415,6 +458,8 @@ export class NookDock extends HTMLElement {
|
|
|
415
458
|
);
|
|
416
459
|
}
|
|
417
460
|
disconnectedCallback() {
|
|
461
|
+
this.queryReady = false;
|
|
462
|
+
this.pendingIntents = [];
|
|
418
463
|
this.layerObserver.disconnect();
|
|
419
464
|
this.altHeld = false;
|
|
420
465
|
this.anchor.removeAttribute("data-alt-held");
|
|
@@ -598,20 +643,13 @@ export class NookDock extends HTMLElement {
|
|
|
598
643
|
? this.getAttribute("root-keybind")
|
|
599
644
|
: "Shift+?";
|
|
600
645
|
const supported = bindingSupported(binding, this.browserKeyboardLayout);
|
|
601
|
-
const text = supported
|
|
602
|
-
? `Menu · ${binding
|
|
603
|
-
.split("+")
|
|
604
|
-
.map((key) => key.trim())
|
|
605
|
-
.join(" + ")}`
|
|
606
|
-
: "Menu";
|
|
607
646
|
this.i18n.text(
|
|
608
647
|
this.handleTipShortcut,
|
|
609
648
|
supported ? "Menu · {keybind}" : "Menu",
|
|
610
649
|
{
|
|
611
|
-
keybind:
|
|
612
|
-
.
|
|
613
|
-
|
|
614
|
-
.join(" + "),
|
|
650
|
+
keybind: keyboardLabels(
|
|
651
|
+
this.ownerDocument.defaultView.navigator,
|
|
652
|
+
).format(binding),
|
|
615
653
|
},
|
|
616
654
|
);
|
|
617
655
|
}
|
|
@@ -632,6 +670,7 @@ export class NookDock extends HTMLElement {
|
|
|
632
670
|
if (this.isConnected) this.reconcile();
|
|
633
671
|
}
|
|
634
672
|
if (name === "root-keybind") this.updateHandleTip();
|
|
673
|
+
if (name.endsWith("-keybind") && this.isConnected) this.reconcile();
|
|
635
674
|
if (
|
|
636
675
|
["feedback-disabled", "settings-href"].includes(name) &&
|
|
637
676
|
this.isConnected
|
|
@@ -649,6 +688,10 @@ export class NookDock extends HTMLElement {
|
|
|
649
688
|
}
|
|
650
689
|
}
|
|
651
690
|
send(detail) {
|
|
691
|
+
if (this.isConnected && !this.queryReady) {
|
|
692
|
+
(this.pendingIntents ??= []).push(detail);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
652
695
|
this.intents.dispatchEvent(new CustomEvent("intent", { detail }));
|
|
653
696
|
}
|
|
654
697
|
open(view = "root") {
|
|
@@ -695,10 +738,10 @@ export class NookDock extends HTMLElement {
|
|
|
695
738
|
for (const [id, title] of [
|
|
696
739
|
["root", "Nook"],
|
|
697
740
|
["full", "More"],
|
|
698
|
-
["all", "
|
|
741
|
+
["all", "Full menu"],
|
|
699
742
|
["settings", "Quick settings"],
|
|
700
743
|
["commands", "Commands"],
|
|
701
|
-
["welcome", "
|
|
744
|
+
["welcome", "Quick tour"],
|
|
702
745
|
["feedback", "Feedback"],
|
|
703
746
|
])
|
|
704
747
|
this.view(id, title);
|
|
@@ -719,45 +762,57 @@ export class NookDock extends HTMLElement {
|
|
|
719
762
|
copy.className = "tutorial";
|
|
720
763
|
const keyboard = this.make("div", "tutorial-desktop");
|
|
721
764
|
keyboard.className = "keyboard-only";
|
|
722
|
-
const lead = this.make(
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
this.make(
|
|
727
|
-
"span",
|
|
728
|
-
null,
|
|
729
|
-
"Click Nook to find actions, settings, and help for this page.",
|
|
730
|
-
),
|
|
765
|
+
const lead = this.make(
|
|
766
|
+
"p",
|
|
767
|
+
null,
|
|
768
|
+
"Arrows move; Enter selects. Backspace goes back; Esc closes.",
|
|
731
769
|
);
|
|
732
770
|
const numberTip = this.make(
|
|
733
771
|
"p",
|
|
734
772
|
null,
|
|
735
|
-
"Hold Alt / Option
|
|
773
|
+
"Hold Alt / Option for labels and numbers.",
|
|
736
774
|
);
|
|
737
775
|
numberTip.className = "layout-hint";
|
|
738
|
-
keyboard
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
null,
|
|
743
|
-
"Use arrows to move and Enter to choose. Backspace goes back; Escape closes the menu.",
|
|
744
|
-
),
|
|
745
|
-
numberTip,
|
|
776
|
+
const keyboardGraphic = tutorialGraphic(this.make, "keyboard");
|
|
777
|
+
this.i18n.text(
|
|
778
|
+
keyboardGraphic.querySelector(".tutorial-enter-label"),
|
|
779
|
+
"Enter",
|
|
746
780
|
);
|
|
781
|
+
const shortcuts = this.make("dl", "tutorial-shortcuts");
|
|
782
|
+
shortcuts.className = "tutorial-shortcuts";
|
|
783
|
+
const shortcutRows = new Map();
|
|
784
|
+
for (const [id, title] of [
|
|
785
|
+
["root", "Menu"],
|
|
786
|
+
["all", "Full menu"],
|
|
787
|
+
["settings", "Quick settings"],
|
|
788
|
+
["commands", "Commands"],
|
|
789
|
+
["feedback", "Feedback"],
|
|
790
|
+
]) {
|
|
791
|
+
const row = this.make("div", "tutorial-shortcut");
|
|
792
|
+
const label = this.make("dt", "tutorial-shortcut-label", title);
|
|
793
|
+
const value = this.make("kbd", "tutorial-shortcut-keybind");
|
|
794
|
+
const description = this.make("dd");
|
|
795
|
+
description.append(value);
|
|
796
|
+
row.append(label, description);
|
|
797
|
+
shortcuts.append(row);
|
|
798
|
+
shortcutRows.set(id, { row, value });
|
|
799
|
+
}
|
|
800
|
+
keyboard.append(shortcuts, keyboardGraphic, lead, numberTip);
|
|
747
801
|
const touch = this.make("div", "tutorial-touch");
|
|
748
802
|
touch.className = "touch-only";
|
|
749
|
-
const touchLead = this.make(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
this.make("span", null, "Tap Nook for actions, settings, and help."),
|
|
803
|
+
const touchLead = this.make(
|
|
804
|
+
"p",
|
|
805
|
+
null,
|
|
806
|
+
"Tap to open. Swipe sideways for the full menu or vertically for settings.",
|
|
754
807
|
);
|
|
808
|
+
const touchGraphic = tutorialGraphic(this.make, "touch");
|
|
755
809
|
touch.append(
|
|
810
|
+
touchGraphic,
|
|
756
811
|
touchLead,
|
|
757
812
|
this.make(
|
|
758
813
|
"p",
|
|
759
814
|
null,
|
|
760
|
-
"
|
|
815
|
+
"Hold a swipe to pick. Release elsewhere to cancel.",
|
|
761
816
|
),
|
|
762
817
|
);
|
|
763
818
|
const welcomeMove = this.make(
|
|
@@ -766,7 +821,8 @@ export class NookDock extends HTMLElement {
|
|
|
766
821
|
"Hold Nook, then drag to move it.",
|
|
767
822
|
);
|
|
768
823
|
welcomeMove.setAttribute("part", "drag-hint tutorial-drag");
|
|
769
|
-
|
|
824
|
+
const dragGraphic = tutorialGraphic(this.make, "drag");
|
|
825
|
+
copy.append(keyboard, touch, welcomeMove, dragGraphic);
|
|
770
826
|
const done = this.make("button", "tutorial-continue", "Got it");
|
|
771
827
|
done.className = "command-run";
|
|
772
828
|
done.onclick = () => {
|
|
@@ -786,11 +842,18 @@ export class NookDock extends HTMLElement {
|
|
|
786
842
|
this.tutorialUI = {
|
|
787
843
|
welcome,
|
|
788
844
|
done,
|
|
845
|
+
lead,
|
|
846
|
+
numberTip,
|
|
847
|
+
shortcuts,
|
|
848
|
+
shortcutRows,
|
|
789
849
|
regions: [
|
|
790
850
|
["tutorial", copy],
|
|
791
851
|
["tutorial-desktop", keyboard],
|
|
792
852
|
["tutorial-touch", touch],
|
|
793
853
|
["tutorial-drag", welcomeMove],
|
|
854
|
+
["tutorial-keyboard-graphic", keyboardGraphic],
|
|
855
|
+
["tutorial-touch-graphic", touchGraphic],
|
|
856
|
+
["tutorial-drag-graphic", dragGraphic],
|
|
794
857
|
].map(([name, element]) => ({
|
|
795
858
|
name,
|
|
796
859
|
element,
|
|
@@ -802,12 +865,21 @@ export class NookDock extends HTMLElement {
|
|
|
802
865
|
for (const [id, title, iconText] of [
|
|
803
866
|
["feedback", "Feedback", icon("feedback")],
|
|
804
867
|
["quick", "More", icon("more")],
|
|
868
|
+
["all", "Full menu", icon("menu")],
|
|
869
|
+
["welcome", "Quick tour", icon("guide")],
|
|
805
870
|
["settings", "Settings", icon("settings")],
|
|
806
871
|
["commands", "Commands", icon("commands")],
|
|
807
872
|
]) {
|
|
808
873
|
const b = this.itemButton(null, title, iconText);
|
|
809
874
|
b.onclick = () =>
|
|
810
875
|
this.send({ type: "enter", view: id === "quick" ? "full" : id });
|
|
876
|
+
if (id === "welcome") {
|
|
877
|
+
const badge = this.make("span", "tutorial-badge");
|
|
878
|
+
badge.className = "tutorial-badge";
|
|
879
|
+
badge.setAttribute("aria-hidden", "true");
|
|
880
|
+
b.append(badge);
|
|
881
|
+
this.i18n.attr(b, "aria-description", "New: learn how to use Nook.");
|
|
882
|
+
}
|
|
811
883
|
this.builtins.set(id, b);
|
|
812
884
|
root.grid.append(b);
|
|
813
885
|
}
|
|
@@ -823,13 +895,17 @@ export class NookDock extends HTMLElement {
|
|
|
823
895
|
bindingSupported(binding, this.browserKeyboardLayout),
|
|
824
896
|
});
|
|
825
897
|
}
|
|
826
|
-
itemButton(
|
|
898
|
+
itemButton(
|
|
899
|
+
item,
|
|
900
|
+
title,
|
|
901
|
+
iconText = icon(item && commandTarget(item) ? "commands" : "file"),
|
|
902
|
+
) {
|
|
827
903
|
const button = this.make("button", "item");
|
|
828
904
|
button.className = "item";
|
|
829
905
|
button.type = "button";
|
|
830
906
|
const icon = this.make("span", "icon");
|
|
831
907
|
icon.className = "icon";
|
|
832
|
-
const template = item &&
|
|
908
|
+
const template = item && iconSlot(item);
|
|
833
909
|
if (template)
|
|
834
910
|
icon.append(
|
|
835
911
|
template instanceof HTMLTemplateElement
|
|
@@ -862,13 +938,63 @@ export class NookDock extends HTMLElement {
|
|
|
862
938
|
this.i18n.attr(button, "aria-label", title);
|
|
863
939
|
return button;
|
|
864
940
|
}
|
|
941
|
+
reconcileTutorialShortcuts(items) {
|
|
942
|
+
const ui = this.tutorialUI;
|
|
943
|
+
if (!ui) return;
|
|
944
|
+
const labels = keyboardLabels(this.ownerDocument.defaultView.navigator);
|
|
945
|
+
this.i18n.text(
|
|
946
|
+
ui.lead,
|
|
947
|
+
"Arrows move; Enter selects. {back} goes back; Esc closes.",
|
|
948
|
+
{ back: this.i18n.t(labels.back) },
|
|
949
|
+
);
|
|
950
|
+
this.i18n.text(ui.numberTip, "Hold {modifier} for labels and numbers.", {
|
|
951
|
+
modifier: this.i18n.t(labels.alt),
|
|
952
|
+
});
|
|
953
|
+
ui.numberTip.hidden =
|
|
954
|
+
!this.browserKeyboardLayout ||
|
|
955
|
+
![...this.browserKeyboardLayout.values()].some((value) =>
|
|
956
|
+
/^[1-9]$/.test(value),
|
|
957
|
+
);
|
|
958
|
+
const defaults = {
|
|
959
|
+
root: "Shift+?",
|
|
960
|
+
all: "Alt+Shift+M",
|
|
961
|
+
settings: "Alt+Shift+S",
|
|
962
|
+
commands: labels.mac ? "Meta+Shift+P" : "Ctrl+Shift+P",
|
|
963
|
+
feedback: "Alt+Shift+F",
|
|
964
|
+
};
|
|
965
|
+
let visible = 0;
|
|
966
|
+
for (const [id, { row, value }] of ui.shortcutRows) {
|
|
967
|
+
const attribute = `${id}-keybind`;
|
|
968
|
+
const binding = this.hasAttribute(attribute)
|
|
969
|
+
? this.getAttribute(attribute)
|
|
970
|
+
: defaults[id];
|
|
971
|
+
const available =
|
|
972
|
+
id === "root" ||
|
|
973
|
+
this.surfaceAvailable(id) ||
|
|
974
|
+
(id === "feedback" &&
|
|
975
|
+
!this.feedbackDisabled() &&
|
|
976
|
+
items.some((item) => item.hasAttribute("feedback")));
|
|
977
|
+
row.hidden =
|
|
978
|
+
!available || !bindingSupported(binding, this.browserKeyboardLayout);
|
|
979
|
+
if (!row.hidden) visible++;
|
|
980
|
+
const text = labels.format(binding);
|
|
981
|
+
if (value.textContent !== text) value.textContent = text;
|
|
982
|
+
}
|
|
983
|
+
ui.shortcuts.hidden = !visible;
|
|
984
|
+
}
|
|
985
|
+
|
|
865
986
|
reconcileTutorial() {
|
|
866
987
|
const ui = this.tutorialUI;
|
|
867
988
|
if (!ui) return;
|
|
868
989
|
const options = this.tutorial || {};
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
990
|
+
const guide = this.builtins?.get("welcome");
|
|
991
|
+
if (guide) {
|
|
992
|
+
const title = options.label ?? "Quick tour";
|
|
993
|
+
this.i18n.attr(guide, "aria-label", title);
|
|
994
|
+
this.i18n.text(guide.querySelector(".item-label"), title);
|
|
995
|
+
this.i18n.text(guide.querySelector(".tooltip-text"), title);
|
|
996
|
+
}
|
|
997
|
+
for (const [view, title] of [[ui.welcome, options.title ?? "Quick tour"]]) {
|
|
872
998
|
this.i18n.text(view.heading, title);
|
|
873
999
|
this.i18n.attr(view.el, "aria-label", title);
|
|
874
1000
|
}
|
|
@@ -907,6 +1033,7 @@ export class NookDock extends HTMLElement {
|
|
|
907
1033
|
const set = new Set(items);
|
|
908
1034
|
const placements = new Map();
|
|
909
1035
|
this.planItems(items);
|
|
1036
|
+
this.reconcileTutorialShortcuts(items);
|
|
910
1037
|
for (const [item, record] of this.records)
|
|
911
1038
|
if (!set.has(item)) {
|
|
912
1039
|
record.enum?.dispose();
|
|
@@ -939,7 +1066,7 @@ export class NookDock extends HTMLElement {
|
|
|
939
1066
|
const topLevel = this.topLevelItems.includes(item);
|
|
940
1067
|
const destinations =
|
|
941
1068
|
item.localName === "nook-setting"
|
|
942
|
-
? [
|
|
1069
|
+
? ["settings"]
|
|
943
1070
|
: topLevel
|
|
944
1071
|
? [this.rootItems.includes(item) ? "root" : "full"]
|
|
945
1072
|
: parent && this.records.has(parent)
|
|
@@ -987,7 +1114,7 @@ export class NookDock extends HTMLElement {
|
|
|
987
1114
|
"|" +
|
|
988
1115
|
item.keybind +
|
|
989
1116
|
"|" +
|
|
990
|
-
|
|
1117
|
+
iconSlot(item)?.outerHTML +
|
|
991
1118
|
"|" +
|
|
992
1119
|
bindingSupported(item.keybind, this.browserKeyboardLayout);
|
|
993
1120
|
if (record.signature !== signature) {
|
|
@@ -1081,9 +1208,9 @@ export class NookDock extends HTMLElement {
|
|
|
1081
1208
|
this.feedbackUI?.setEnabled(feedbackEnabled);
|
|
1082
1209
|
this.feedbackUI?.reconcile();
|
|
1083
1210
|
if (!feedbackEnabled && this.state?.path.includes("feedback")) this.close();
|
|
1084
|
-
this.builtins.get("quick").hidden =
|
|
1211
|
+
this.builtins.get("quick").hidden = true;
|
|
1085
1212
|
const commands = items.filter((item) => item.localName === "nook-command");
|
|
1086
|
-
for (const id of ["feedback", "settings", "commands"])
|
|
1213
|
+
for (const id of ["feedback", "settings", "commands", "all", "welcome"])
|
|
1087
1214
|
this.builtins.get(id).hidden = !this.rootEntries.some(
|
|
1088
1215
|
(entry) => entry.id === id,
|
|
1089
1216
|
);
|
|
@@ -1123,9 +1250,8 @@ export class NookDock extends HTMLElement {
|
|
|
1123
1250
|
: `${id}-presentation`,
|
|
1124
1251
|
)
|
|
1125
1252
|
: id.getAttribute("presentation");
|
|
1126
|
-
view.presentation =
|
|
1127
|
-
|
|
1128
|
-
view.presentation = count > this.rootCapacity ? "expanded" : "compact";
|
|
1253
|
+
view.presentation =
|
|
1254
|
+
id === "root" ? "compact" : presentation(kind, count, override);
|
|
1129
1255
|
if (view.grid && id !== "full" && id !== "all") {
|
|
1130
1256
|
const labelsVisible = id !== "root" || view.presentation === "expanded";
|
|
1131
1257
|
view.el.dataset.layout =
|
|
@@ -1169,39 +1295,18 @@ export class NookDock extends HTMLElement {
|
|
|
1169
1295
|
this.state.path.at(-1) === focusedView &&
|
|
1170
1296
|
this.shadowRoot.activeElement === focused
|
|
1171
1297
|
)
|
|
1172
|
-
|
|
1173
|
-
? this.builtins.get("quick")
|
|
1174
|
-
: this.focusables()[0]
|
|
1175
|
-
)?.focus();
|
|
1298
|
+
this.focusables()[0]?.focus();
|
|
1176
1299
|
});
|
|
1177
1300
|
}
|
|
1178
1301
|
}
|
|
1179
1302
|
}
|
|
1180
1303
|
planItems(items) {
|
|
1181
|
-
const style = this.ownerDocument.defaultView.getComputedStyle(
|
|
1182
|
-
this.rootMeasure,
|
|
1183
|
-
);
|
|
1184
|
-
const size = parseFloat(style.width) || 48;
|
|
1185
|
-
const measuredGap = parseFloat(style.marginInlineEnd);
|
|
1186
|
-
const gap = Number.isFinite(measuredGap) ? measuredGap : 8;
|
|
1304
|
+
const style = this.ownerDocument.defaultView.getComputedStyle(this);
|
|
1187
1305
|
this.rootCapacity = Math.max(
|
|
1188
|
-
|
|
1306
|
+
2,
|
|
1189
1307
|
Math.min(
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
parseInt(style.getPropertyValue("--nook-root-max-items"), 10) || 5,
|
|
1193
|
-
),
|
|
1194
|
-
Math.floor(
|
|
1195
|
-
((this.anchorRect?.width ??
|
|
1196
|
-
this.ownerDocument.defaultView.innerWidth) -
|
|
1197
|
-
(this.position === "left-center" || this.position === "right-center"
|
|
1198
|
-
? (parseFloat(style.marginBlockStart) || 56) +
|
|
1199
|
-
(parseFloat(style.marginBlockEnd) || 12) +
|
|
1200
|
-
16
|
|
1201
|
-
: 32) +
|
|
1202
|
-
gap) /
|
|
1203
|
-
(size + gap),
|
|
1204
|
-
),
|
|
1308
|
+
6,
|
|
1309
|
+
parseInt(style.getPropertyValue("--nook-root-max-items"), 10) || 6,
|
|
1205
1310
|
),
|
|
1206
1311
|
);
|
|
1207
1312
|
this.topLevelItems = items.filter(
|
|
@@ -1218,7 +1323,7 @@ export class NookDock extends HTMLElement {
|
|
|
1218
1323
|
this.availableBuiltins.add("feedback");
|
|
1219
1324
|
if (
|
|
1220
1325
|
this.settingsHref ||
|
|
1221
|
-
items.some((item) => item.localName === "nook-setting"
|
|
1326
|
+
items.some((item) => item.localName === "nook-setting")
|
|
1222
1327
|
)
|
|
1223
1328
|
this.availableBuiltins.add("settings");
|
|
1224
1329
|
if (items.some((item) => item.localName === "nook-command"))
|
|
@@ -1245,32 +1350,36 @@ export class NookDock extends HTMLElement {
|
|
|
1245
1350
|
.filter((id) => this.availableBuiltins.has(id))
|
|
1246
1351
|
.map((id) => builtin(id, id === "settings" ? 20 : 10)),
|
|
1247
1352
|
];
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
const plan = distributeItems(
|
|
1252
|
-
entries,
|
|
1253
|
-
Math.max(1, this.rootCapacity - pinnedSettings),
|
|
1254
|
-
);
|
|
1353
|
+
const tour = this.state?.needsIntro
|
|
1354
|
+
? [{ id: "welcome", priority: Number.MAX_SAFE_INTEGER, group: "" }]
|
|
1355
|
+
: [];
|
|
1255
1356
|
this.allEntries = distributeItems(
|
|
1256
|
-
[
|
|
1257
|
-
...entries,
|
|
1258
|
-
...items
|
|
1259
|
-
.filter((item) => item.localName === "nook-setting" && item.root)
|
|
1260
|
-
.map((item) => ({
|
|
1261
|
-
id: item,
|
|
1262
|
-
priority: item.priority,
|
|
1263
|
-
group: item.group.trim(),
|
|
1264
|
-
})),
|
|
1265
|
-
],
|
|
1357
|
+
[...tour, ...entries],
|
|
1266
1358
|
Number.MAX_SAFE_INTEGER,
|
|
1267
1359
|
).root;
|
|
1268
|
-
this.
|
|
1269
|
-
this.
|
|
1270
|
-
|
|
1360
|
+
if (this.allEntries.length) this.availableBuiltins.add("all");
|
|
1361
|
+
if (this.state?.needsIntro) this.availableBuiltins.add("welcome");
|
|
1362
|
+
const reserved = [
|
|
1363
|
+
...(this.allEntries.length
|
|
1364
|
+
? [{ id: "all", priority: 0, group: "" }]
|
|
1365
|
+
: []),
|
|
1366
|
+
...(this.availableBuiltins.has("settings")
|
|
1367
|
+
? [builtin("settings", 20)]
|
|
1368
|
+
: []),
|
|
1369
|
+
];
|
|
1370
|
+
const extras = this.allEntries.filter((entry) => entry.id !== "settings");
|
|
1371
|
+
this.rootEntries = [
|
|
1372
|
+
...reserved,
|
|
1373
|
+
...extras.slice(0, Math.min(4, this.rootCapacity - reserved.length)),
|
|
1374
|
+
];
|
|
1375
|
+
const picked = new Set(this.rootEntries.map((entry) => entry.id));
|
|
1376
|
+
this.overflowEntries = this.allEntries.filter(
|
|
1377
|
+
(entry) => !picked.has(entry.id),
|
|
1378
|
+
);
|
|
1379
|
+
this.rootItems = this.rootEntries
|
|
1271
1380
|
.map((entry) => entry.id)
|
|
1272
1381
|
.filter((id) => typeof id !== "string");
|
|
1273
|
-
this.overflowItems =
|
|
1382
|
+
this.overflowItems = this.overflowEntries
|
|
1274
1383
|
.map((entry) => entry.id)
|
|
1275
1384
|
.filter((id) => typeof id !== "string");
|
|
1276
1385
|
}
|
|
@@ -1292,7 +1401,8 @@ export class NookDock extends HTMLElement {
|
|
|
1292
1401
|
// Only launch buttons are mirrored. Panels and setting controls have one
|
|
1293
1402
|
// mounted instance, even when a menu is projected as nested sections.
|
|
1294
1403
|
for (const [id, view] of this.views) {
|
|
1295
|
-
if (!view.grid || id === "full" || id === "all")
|
|
1404
|
+
if (!view.grid || id === "root" || id === "full" || id === "all")
|
|
1405
|
+
continue;
|
|
1296
1406
|
if (!view.grouped) {
|
|
1297
1407
|
view.grouped = this.make("div", "groups");
|
|
1298
1408
|
view.grouped.className = "groups";
|
|
@@ -1454,11 +1564,12 @@ export class NookDock extends HTMLElement {
|
|
|
1454
1564
|
const source = typeof entry.id === "string" ? null : entry.id;
|
|
1455
1565
|
const title = source
|
|
1456
1566
|
? label(source)
|
|
1457
|
-
:
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
: "
|
|
1567
|
+
: {
|
|
1568
|
+
feedback: "Feedback",
|
|
1569
|
+
commands: "Commands",
|
|
1570
|
+
settings: "Settings",
|
|
1571
|
+
welcome: this.tutorial?.label ?? "Quick tour",
|
|
1572
|
+
}[entry.id];
|
|
1462
1573
|
const signature = source ? this.records.get(source).signature : title;
|
|
1463
1574
|
let record = view.overflowButtons.get(entry.id);
|
|
1464
1575
|
if (!record) {
|
|
@@ -1466,19 +1577,31 @@ export class NookDock extends HTMLElement {
|
|
|
1466
1577
|
source,
|
|
1467
1578
|
title,
|
|
1468
1579
|
icon(
|
|
1469
|
-
|
|
1470
|
-
?
|
|
1471
|
-
: entry.id === "commands"
|
|
1580
|
+
source
|
|
1581
|
+
? commandTarget(source)
|
|
1472
1582
|
? "commands"
|
|
1473
|
-
: "
|
|
1583
|
+
: "file"
|
|
1584
|
+
: {
|
|
1585
|
+
feedback: "feedback",
|
|
1586
|
+
commands: "commands",
|
|
1587
|
+
settings: "settings",
|
|
1588
|
+
welcome: "guide",
|
|
1589
|
+
}[entry.id],
|
|
1474
1590
|
),
|
|
1475
1591
|
);
|
|
1476
1592
|
button.onclick = () => {
|
|
1477
1593
|
if (source?.localName === "nook-setting") {
|
|
1478
|
-
this.open("
|
|
1594
|
+
this.open("settings");
|
|
1479
1595
|
queueMicrotask(() => source._control?.focus());
|
|
1480
1596
|
} else if (source) this.activate(source);
|
|
1481
|
-
else
|
|
1597
|
+
else if (
|
|
1598
|
+
view === this.views.get("all") &&
|
|
1599
|
+
entry.id === "settings" &&
|
|
1600
|
+
this.settingsHref
|
|
1601
|
+
) {
|
|
1602
|
+
this.views.get("settings").link.click();
|
|
1603
|
+
this.close();
|
|
1604
|
+
} else this.builtins.get(entry.id).click();
|
|
1482
1605
|
};
|
|
1483
1606
|
record = { button, signature };
|
|
1484
1607
|
view.overflowButtons.set(entry.id, record);
|
|
@@ -1648,22 +1771,41 @@ export class NookDock extends HTMLElement {
|
|
|
1648
1771
|
this.chooseCommand(item);
|
|
1649
1772
|
return;
|
|
1650
1773
|
}
|
|
1651
|
-
if (item.localName === "nook-menu" || item.localName === "nook-panel")
|
|
1774
|
+
if (item.localName === "nook-menu" || item.localName === "nook-panel") {
|
|
1652
1775
|
this.send({ type: "enter", view: item });
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
const command = commandTarget(item);
|
|
1779
|
+
if (
|
|
1780
|
+
emit(
|
|
1781
|
+
item,
|
|
1782
|
+
item.localName === "nook-item" ? "nook-select" : "nook-action",
|
|
1783
|
+
{
|
|
1784
|
+
source: item,
|
|
1785
|
+
id: item.id,
|
|
1786
|
+
command,
|
|
1787
|
+
context: this.context,
|
|
1788
|
+
},
|
|
1789
|
+
)
|
|
1790
|
+
) {
|
|
1791
|
+
if (command) this.chooseCommand(command);
|
|
1792
|
+
else this.close();
|
|
1793
|
+
}
|
|
1661
1794
|
}
|
|
1795
|
+
|
|
1662
1796
|
project(state, focus = true) {
|
|
1663
1797
|
const previousCorner = this.state?.corner;
|
|
1664
1798
|
const previousPreferred = this.state?.preferredPosition;
|
|
1665
1799
|
const previous = this.state?.path.at(-1);
|
|
1800
|
+
const previousNeedsIntro = this.state?.needsIntro;
|
|
1666
1801
|
this.state = state;
|
|
1802
|
+
this.queryReady = true;
|
|
1803
|
+
if (this.pendingIntents?.length) {
|
|
1804
|
+
const pending = this.pendingIntents.splice(0);
|
|
1805
|
+
queueMicrotask(() => {
|
|
1806
|
+
if (this.isConnected) for (const detail of pending) this.send(detail);
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1667
1809
|
const movable = this.canDrag();
|
|
1668
1810
|
this.anchor.dataset.draggable = String(movable);
|
|
1669
1811
|
if (
|
|
@@ -1698,7 +1840,7 @@ export class NookDock extends HTMLElement {
|
|
|
1698
1840
|
(id) => this.views.get(id)?.presentation || "compact",
|
|
1699
1841
|
);
|
|
1700
1842
|
for (const [id, view] of this.views) {
|
|
1701
|
-
const visible = id === active;
|
|
1843
|
+
const visible = id === active || (id === "root" && this.swipeDial?.shown);
|
|
1702
1844
|
view.el.dataset.presentation = visible
|
|
1703
1845
|
? effective
|
|
1704
1846
|
: view.presentation || "compact";
|
|
@@ -1748,6 +1890,7 @@ export class NookDock extends HTMLElement {
|
|
|
1748
1890
|
view.picker?.syncOpen(visible);
|
|
1749
1891
|
view.back.hidden = state.path.length <= 1;
|
|
1750
1892
|
}
|
|
1893
|
+
this.swipeDial?.sync();
|
|
1751
1894
|
if (!active) delete this.anchor.dataset.numbering;
|
|
1752
1895
|
if (active && this.anchor.dataset.numbering) this.showNumbers();
|
|
1753
1896
|
this.handle.setAttribute("aria-expanded", String(Boolean(active)));
|
|
@@ -1776,6 +1919,7 @@ export class NookDock extends HTMLElement {
|
|
|
1776
1919
|
).focus();
|
|
1777
1920
|
}
|
|
1778
1921
|
}
|
|
1922
|
+
if (previousNeedsIntro !== state.needsIntro) this.reconcile();
|
|
1779
1923
|
if (previousCorner && previousCorner !== state.corner) {
|
|
1780
1924
|
this.settleCorner();
|
|
1781
1925
|
emit(this, "nook-corner-change", { corner: state.corner });
|
|
@@ -1796,7 +1940,7 @@ export class NookDock extends HTMLElement {
|
|
|
1796
1940
|
}
|
|
1797
1941
|
focusables() {
|
|
1798
1942
|
const view = this.views.get(this.state?.path.at(-1));
|
|
1799
|
-
|
|
1943
|
+
const nodes = view
|
|
1800
1944
|
? [
|
|
1801
1945
|
...view.el.querySelectorAll(
|
|
1802
1946
|
"button,input,select,textarea,a[href],[tabindex]",
|
|
@@ -1805,8 +1949,17 @@ export class NookDock extends HTMLElement {
|
|
|
1805
1949
|
(n) => !n.disabled && n.tabIndex >= 0 && n.getClientRects().length,
|
|
1806
1950
|
)
|
|
1807
1951
|
: [];
|
|
1952
|
+
return this.state?.path.at(-1) === "root"
|
|
1953
|
+
? nodes.sort(
|
|
1954
|
+
(a, b) => Number(a.dataset.dialOrder) - Number(b.dataset.dialOrder),
|
|
1955
|
+
)
|
|
1956
|
+
: nodes;
|
|
1808
1957
|
}
|
|
1809
1958
|
numberedItems() {
|
|
1959
|
+
if (this.state?.path.at(-1) === "root")
|
|
1960
|
+
return this.focusables()
|
|
1961
|
+
.filter((node) => node.classList.contains("item"))
|
|
1962
|
+
.slice(0, 9);
|
|
1810
1963
|
const view = this.views.get(this.state?.path.at(-1));
|
|
1811
1964
|
return view
|
|
1812
1965
|
? [...view.body.querySelectorAll(".item,.command-result")]
|
|
@@ -1947,7 +2100,10 @@ export class NookDock extends HTMLElement {
|
|
|
1947
2100
|
return;
|
|
1948
2101
|
}
|
|
1949
2102
|
for (const item of this.records.keys())
|
|
1950
|
-
if (
|
|
2103
|
+
if (
|
|
2104
|
+
!(isLeaf(item) && item.hasAttribute("command")) &&
|
|
2105
|
+
bindingMatches(item.keybind)
|
|
2106
|
+
) {
|
|
1951
2107
|
e.preventDefault();
|
|
1952
2108
|
this.activate(item);
|
|
1953
2109
|
return;
|
|
@@ -1976,6 +2132,10 @@ export class NookDock extends HTMLElement {
|
|
|
1976
2132
|
].includes(e.key)
|
|
1977
2133
|
) {
|
|
1978
2134
|
e.preventDefault();
|
|
2135
|
+
if (this.state.path.at(-1) === "root" && e.key.startsWith("Arrow")) {
|
|
2136
|
+
directionalChoice(nodes, current, e.key)?.focus();
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
1979
2139
|
const next =
|
|
1980
2140
|
e.key === "Home"
|
|
1981
2141
|
? 0
|
|
@@ -2100,8 +2260,19 @@ export class NookDock extends HTMLElement {
|
|
|
2100
2260
|
? this.swipeDial.release(e.clientX, e.clientY)
|
|
2101
2261
|
: view;
|
|
2102
2262
|
if (view || p.swipeStarted || dialShown) this.suppressClick = true;
|
|
2103
|
-
|
|
2104
|
-
|
|
2263
|
+
this.cancelPointer();
|
|
2264
|
+
if (
|
|
2265
|
+
typeof destination === "string" &&
|
|
2266
|
+
this.surfaceAvailable(destination)
|
|
2267
|
+
) {
|
|
2268
|
+
if (dialShown) {
|
|
2269
|
+
this.open("root");
|
|
2270
|
+
this.send({ type: "enter", view: destination });
|
|
2271
|
+
} else this.open(destination);
|
|
2272
|
+
} else if (destination && this.records.has(destination)) {
|
|
2273
|
+
if (dialShown) this.open("root");
|
|
2274
|
+
this.activate(destination);
|
|
2275
|
+
} else if (dialShown) this.close();
|
|
2105
2276
|
}
|
|
2106
2277
|
this.cancelPointer();
|
|
2107
2278
|
}
|
|
@@ -2122,6 +2293,7 @@ export class NookDock extends HTMLElement {
|
|
|
2122
2293
|
|
|
2123
2294
|
export function defineNook(registry = customElements) {
|
|
2124
2295
|
for (const tag of [
|
|
2296
|
+
"nook-item",
|
|
2125
2297
|
"nook-action",
|
|
2126
2298
|
"nook-menu",
|
|
2127
2299
|
"nook-setting",
|