@leavepulse/ui 0.56.0 → 0.57.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/dist/chunks/{LpAppShell-BEGjSqFl.js → LpAppShell-Bl1Ch68N.js} +1 -1
- package/dist/chunks/{LpDateRangePicker-Drd7EbIz.js → LpDateRangePicker-CxV5jW7p.js} +4 -13
- package/dist/chunks/{LpDrawer-CBFVD624.js → LpDrawer-D9lLOC0C.js} +4 -6
- package/dist/chunks/LpHintLayer-CI1oZ9Pd.js +154 -0
- package/dist/chunks/{LpModalNav-DXBilXr0.js → LpModalNav-DXyAyhQl.js} +1 -1
- package/dist/chunks/{LpPie-B7ti5qCH.js → LpPie-CXka8Jvx.js} +1 -1
- package/dist/chunks/{LpSidebar-C2oolgwy.js → LpSidebar-hZ8D1cVr.js} +1 -1
- package/dist/chunks/{LpTableOfContents-BUcAfGPQ.js → LpTableOfContents-DYuqkByy.js} +2 -8
- package/dist/chunks/{LpTooltip-CXVKcOXT.js → LpTooltip-suFCM34F.js} +2 -2
- package/dist/chunks/LpTour-C1TC2GyY.js +315 -0
- package/dist/chunks/{LpUptimeBar-ugcv25xj.js → LpUptimeBar-B4ltCfno.js} +1 -1
- package/dist/chunks/TourCard-BQS3ztqj.js +100 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.d.ts +33 -0
- package/dist/components/LpHintLayer.vue.js +4 -0
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpTableOfContents.vue.js +1 -1
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTour.vue.d.ts +98 -0
- package/dist/components/LpTour.vue.js +4 -0
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/TourCard.vue.d.ts +28 -0
- package/dist/components/TourCard.vue.js +4 -0
- package/dist/components/tour.d.ts +21 -0
- package/dist/components/tour.js +38 -0
- package/dist/composables/scroll.d.ts +17 -0
- package/dist/composables/scroll.js +29 -0
- package/dist/composables/useGamepadNav.js +2 -7
- package/dist/composables/useHints.d.ts +18 -0
- package/dist/composables/useHints.js +120 -0
- package/dist/composables/useMediaQuery.d.ts +2 -0
- package/dist/composables/useMediaQuery.js +18 -0
- package/dist/composables/usePaintSelect.js +1 -7
- package/dist/index.d.ts +10 -0
- package/dist/index.js +32 -16
- package/package.json +1 -1
- package/src/component-names.ts +2 -0
- package/src/components/LpDateRangePicker.vue +3 -12
- package/src/components/LpDrawer.vue +4 -7
- package/src/components/LpHintLayer.vue +148 -0
- package/src/components/LpTableOfContents.vue +2 -14
- package/src/components/LpTooltip.vue +2 -4
- package/src/components/LpTour.vue +338 -0
- package/src/components/TourCard.vue +41 -0
- package/src/components/tour.ts +57 -0
- package/src/composables/scroll.ts +54 -0
- package/src/composables/useGamepadNav.ts +2 -7
- package/src/composables/useHints.ts +151 -0
- package/src/composables/useMediaQuery.ts +20 -0
- package/src/composables/usePaintSelect.ts +1 -8
- package/src/index.ts +10 -0
- package/src/tokens/tokens.css +1 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { defineComponent, openBlock, createElementBlock, Fragment, createElementVNode, toDisplayString, createVNode, createCommentVNode, createBlock, withCtx, createTextVNode } from "vue";
|
|
2
|
+
import { _ as _sfc_main$2 } from "./LpButton-BdfZFNqi.js";
|
|
3
|
+
import { _ as _sfc_main$1 } from "./LpIcon-DYtaid0K.js";
|
|
4
|
+
import LpKeys from "../components/LpKeys.vue.js";
|
|
5
|
+
const _hoisted_1 = { class: "flex items-start justify-between gap-3" };
|
|
6
|
+
const _hoisted_2 = {
|
|
7
|
+
class: "font-medium",
|
|
8
|
+
"aria-live": "polite"
|
|
9
|
+
};
|
|
10
|
+
const _hoisted_3 = ["aria-label"];
|
|
11
|
+
const _hoisted_4 = {
|
|
12
|
+
key: 0,
|
|
13
|
+
class: "mt-1.5 text-muted"
|
|
14
|
+
};
|
|
15
|
+
const _hoisted_5 = {
|
|
16
|
+
key: 2,
|
|
17
|
+
class: "mt-2 text-xs text-brand"
|
|
18
|
+
};
|
|
19
|
+
const _hoisted_6 = { class: "mt-4 flex items-center justify-between gap-2" };
|
|
20
|
+
const _hoisted_7 = { class: "text-xs tabular-nums text-muted" };
|
|
21
|
+
const _hoisted_8 = { class: "flex gap-2" };
|
|
22
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
23
|
+
__name: "TourCard",
|
|
24
|
+
props: {
|
|
25
|
+
step: {},
|
|
26
|
+
index: {},
|
|
27
|
+
last: { type: Boolean },
|
|
28
|
+
progress: {},
|
|
29
|
+
labels: {}
|
|
30
|
+
},
|
|
31
|
+
emits: ["next", "back", "perform", "dismiss"],
|
|
32
|
+
setup(__props) {
|
|
33
|
+
return (_ctx, _cache) => {
|
|
34
|
+
return openBlock(), createElementBlock(Fragment, null, [
|
|
35
|
+
createElementVNode("div", _hoisted_1, [
|
|
36
|
+
createElementVNode("p", _hoisted_2, toDisplayString(__props.step?.title), 1),
|
|
37
|
+
createElementVNode("button", {
|
|
38
|
+
type: "button",
|
|
39
|
+
class: "text-muted hover:text-ink",
|
|
40
|
+
"aria-label": __props.labels.close,
|
|
41
|
+
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("dismiss"))
|
|
42
|
+
}, [
|
|
43
|
+
createVNode(_sfc_main$1, {
|
|
44
|
+
name: "lucide:x",
|
|
45
|
+
class: "size-4"
|
|
46
|
+
})
|
|
47
|
+
], 8, _hoisted_3)
|
|
48
|
+
]),
|
|
49
|
+
__props.step?.text ? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString(__props.step.text), 1)) : createCommentVNode("", true),
|
|
50
|
+
__props.step?.keys ? (openBlock(), createBlock(LpKeys, {
|
|
51
|
+
key: 1,
|
|
52
|
+
combo: __props.step.keys,
|
|
53
|
+
size: "sm",
|
|
54
|
+
class: "mt-2"
|
|
55
|
+
}, null, 8, ["combo"])) : createCommentVNode("", true),
|
|
56
|
+
__props.step?.try ? (openBlock(), createElementBlock("p", _hoisted_5, toDisplayString(__props.labels.tryIt), 1)) : createCommentVNode("", true),
|
|
57
|
+
createElementVNode("div", _hoisted_6, [
|
|
58
|
+
createElementVNode("span", _hoisted_7, toDisplayString(__props.progress), 1),
|
|
59
|
+
createElementVNode("div", _hoisted_8, [
|
|
60
|
+
__props.index > 0 ? (openBlock(), createBlock(_sfc_main$2, {
|
|
61
|
+
key: 0,
|
|
62
|
+
size: "sm",
|
|
63
|
+
variant: "ghost",
|
|
64
|
+
onClick: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("back"))
|
|
65
|
+
}, {
|
|
66
|
+
default: withCtx(() => [
|
|
67
|
+
createTextVNode(toDisplayString(__props.labels.back), 1)
|
|
68
|
+
]),
|
|
69
|
+
_: 1
|
|
70
|
+
})) : createCommentVNode("", true),
|
|
71
|
+
__props.step?.try && __props.step.perform ? (openBlock(), createBlock(_sfc_main$2, {
|
|
72
|
+
key: 1,
|
|
73
|
+
size: "sm",
|
|
74
|
+
variant: "outline",
|
|
75
|
+
onClick: _cache[2] || (_cache[2] = ($event) => _ctx.$emit("perform"))
|
|
76
|
+
}, {
|
|
77
|
+
default: withCtx(() => [
|
|
78
|
+
createTextVNode(toDisplayString(__props.labels.perform), 1)
|
|
79
|
+
]),
|
|
80
|
+
_: 1
|
|
81
|
+
})) : createCommentVNode("", true),
|
|
82
|
+
createVNode(_sfc_main$2, {
|
|
83
|
+
size: "sm",
|
|
84
|
+
variant: __props.step?.try ? "ghost" : void 0,
|
|
85
|
+
onClick: _cache[3] || (_cache[3] = ($event) => _ctx.$emit("next"))
|
|
86
|
+
}, {
|
|
87
|
+
default: withCtx(() => [
|
|
88
|
+
createTextVNode(toDisplayString(__props.last ? __props.labels.done : __props.labels.next), 1)
|
|
89
|
+
]),
|
|
90
|
+
_: 1
|
|
91
|
+
}, 8, ["variant"])
|
|
92
|
+
])
|
|
93
|
+
])
|
|
94
|
+
], 64);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
export {
|
|
99
|
+
_sfc_main as _
|
|
100
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const COMPONENT_NAMES: readonly ["LayoutCanvas", "LpActionBar", "LpAddressInput", "LpAlert", "LpAppShell", "LpAutocomplete", "LpAvatar", "LpBadge", "LpBreadcrumbs", "LpButton", "LpCalendar", "LpCard", "LpChart", "LpCheckbox", "LpCodeBlock", "LpColorPicker", "LpCommandPalette", "LpConfirmDialog", "LpConfirmHost", "LpContextMenu", "LpDatePicker", "LpDateRangePicker", "LpDisclosure", "LpDivider", "LpDrawer", "LpDropdownMenu", "LpDropzone", "LpEmptyState", "LpFieldNotice", "LpFileTree", "LpFormField", "LpIcon", "LpImageEditor", "LpInfraNode", "LpInput", "LpKeys", "LpLightbox", "LpLink", "LpLogViewer", "LpMapPicker", "LpMediaGallery", "LpModal", "LpModalNav", "LpNotificationBell", "LpNumberField", "LpNumberFlow", "LpOtpInput", "LpPagination", "LpPaletteFacts", "LpPaletteHeader", "LpPalettePreview", "LpPaletteStats", "LpPaletteSwatches", "LpPasswordInput", "LpPhoneInput", "LpPie", "LpPopover", "LpProgress", "LpRadio", "LpRadioGroup", "LpRangeCalendar", "LpRangeSlider", "LpRollingText", "LpScrollArea", "LpSection", "LpSegmented", "LpSelect", "LpServiceNode", "LpShift", "LpShortcutsDialog", "LpSidebar", "LpSkeleton", "LpSlider", "LpSparkline", "LpStat", "LpStepper", "LpSwitch", "LpTable", "LpTableOfContents", "LpTabs", "LpTagsInput", "LpTextarea", "LpThemeProvider", "LpThemeSwitcher", "LpTilt", "LpTimeField", "LpToaster", "LpTooltip", "LpTooltipProvider", "LpTopologyCanvas", "LpUptimeBar", "LpVideoBackdrop"];
|
|
1
|
+
export declare const COMPONENT_NAMES: readonly ["LayoutCanvas", "LpActionBar", "LpAddressInput", "LpAlert", "LpAppShell", "LpAutocomplete", "LpAvatar", "LpBadge", "LpBreadcrumbs", "LpButton", "LpCalendar", "LpCard", "LpChart", "LpCheckbox", "LpCodeBlock", "LpColorPicker", "LpCommandPalette", "LpConfirmDialog", "LpConfirmHost", "LpContextMenu", "LpDatePicker", "LpDateRangePicker", "LpDisclosure", "LpDivider", "LpDrawer", "LpDropdownMenu", "LpDropzone", "LpEmptyState", "LpFieldNotice", "LpFileTree", "LpFormField", "LpHintLayer", "LpIcon", "LpImageEditor", "LpInfraNode", "LpInput", "LpKeys", "LpLightbox", "LpLink", "LpLogViewer", "LpMapPicker", "LpMediaGallery", "LpModal", "LpModalNav", "LpNotificationBell", "LpNumberField", "LpNumberFlow", "LpOtpInput", "LpPagination", "LpPaletteFacts", "LpPaletteHeader", "LpPalettePreview", "LpPaletteStats", "LpPaletteSwatches", "LpPasswordInput", "LpPhoneInput", "LpPie", "LpPopover", "LpProgress", "LpRadio", "LpRadioGroup", "LpRangeCalendar", "LpRangeSlider", "LpRollingText", "LpScrollArea", "LpSection", "LpSegmented", "LpSelect", "LpServiceNode", "LpShift", "LpShortcutsDialog", "LpSidebar", "LpSkeleton", "LpSlider", "LpSparkline", "LpStat", "LpStepper", "LpSwitch", "LpTable", "LpTableOfContents", "LpTabs", "LpTagsInput", "LpTextarea", "LpThemeProvider", "LpThemeSwitcher", "LpTilt", "LpTimeField", "LpToaster", "LpTooltip", "LpTooltipProvider", "LpTopologyCanvas", "LpTour", "LpUptimeBar", "LpVideoBackdrop"];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface HintNote {
|
|
2
|
+
title: string;
|
|
3
|
+
text?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const _default: typeof __VLS_export;
|
|
6
|
+
export default _default;
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<{
|
|
8
|
+
/** Ids to mark, most important first. */
|
|
9
|
+
fresh: string[];
|
|
10
|
+
notes: Record<string, HintNote>;
|
|
11
|
+
/** At most this many dots at once, so a page full of news stays calm. */
|
|
12
|
+
limit?: number;
|
|
13
|
+
labels?: Partial<{
|
|
14
|
+
gotIt: string;
|
|
15
|
+
badge: string;
|
|
16
|
+
}>;
|
|
17
|
+
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
18
|
+
read: (id: string) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<{
|
|
20
|
+
/** Ids to mark, most important first. */
|
|
21
|
+
fresh: string[];
|
|
22
|
+
notes: Record<string, HintNote>;
|
|
23
|
+
/** At most this many dots at once, so a page full of news stays calm. */
|
|
24
|
+
limit?: number;
|
|
25
|
+
labels?: Partial<{
|
|
26
|
+
gotIt: string;
|
|
27
|
+
badge: string;
|
|
28
|
+
}>;
|
|
29
|
+
}> & Readonly<{
|
|
30
|
+
onRead?: ((id: string) => any) | undefined;
|
|
31
|
+
}>, {
|
|
32
|
+
limit: number;
|
|
33
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface TourContext {
|
|
2
|
+
/** Undo something when this step is left, in either direction (close the menu it opened). */
|
|
3
|
+
onLeave(cleanup: () => void): void;
|
|
4
|
+
/** Undo something when the tour ends, for what several steps share (a dialog they all live in). */
|
|
5
|
+
onEnd(cleanup: () => void): void;
|
|
6
|
+
}
|
|
7
|
+
export interface TourStep {
|
|
8
|
+
/**
|
|
9
|
+
* What to point at: a `v-hint` id, a CSS selector, an element or a getter.
|
|
10
|
+
* Left out, the step is a centred card with nothing highlighted (an intro).
|
|
11
|
+
* A target that never appears is skipped, so a step for a control the user
|
|
12
|
+
* cannot see costs nothing.
|
|
13
|
+
*/
|
|
14
|
+
target?: string | Element | (() => Element | null);
|
|
15
|
+
title: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
/** A shortcut for the same action, shown as keycaps ("Ctrl+K"). */
|
|
18
|
+
keys?: string;
|
|
19
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
20
|
+
/**
|
|
21
|
+
* Ask the user to press the highlighted control themselves; the tour moves
|
|
22
|
+
* on when they do. Doing it beats reading about it.
|
|
23
|
+
*/
|
|
24
|
+
try?: boolean;
|
|
25
|
+
/** With `try`, a "do it for me" button runs this instead. */
|
|
26
|
+
perform?: () => void | Promise<void>;
|
|
27
|
+
/** Runs before the step shows: open the dialog or tab its target lives in. */
|
|
28
|
+
enter?: (context: TourContext) => void | Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
declare const _default: typeof __VLS_export;
|
|
31
|
+
export default _default;
|
|
32
|
+
declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
|
|
33
|
+
steps: TourStep[];
|
|
34
|
+
/** Room around the target inside the lit hole, px. */
|
|
35
|
+
padding?: number;
|
|
36
|
+
labels?: Partial<{
|
|
37
|
+
next: string;
|
|
38
|
+
back: string;
|
|
39
|
+
done: string;
|
|
40
|
+
close: string;
|
|
41
|
+
progress: string;
|
|
42
|
+
perform: string;
|
|
43
|
+
tryIt: string;
|
|
44
|
+
}>;
|
|
45
|
+
} & {
|
|
46
|
+
open?: boolean;
|
|
47
|
+
step?: number;
|
|
48
|
+
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
49
|
+
"update:open": (value: boolean) => any;
|
|
50
|
+
"update:step": (value: number) => any;
|
|
51
|
+
} & {
|
|
52
|
+
dismiss: (index: number) => any;
|
|
53
|
+
finish: () => any;
|
|
54
|
+
}, string, import("vue").PublicProps, Readonly<{
|
|
55
|
+
steps: TourStep[];
|
|
56
|
+
/** Room around the target inside the lit hole, px. */
|
|
57
|
+
padding?: number;
|
|
58
|
+
labels?: Partial<{
|
|
59
|
+
next: string;
|
|
60
|
+
back: string;
|
|
61
|
+
done: string;
|
|
62
|
+
close: string;
|
|
63
|
+
progress: string;
|
|
64
|
+
perform: string;
|
|
65
|
+
tryIt: string;
|
|
66
|
+
}>;
|
|
67
|
+
} & {
|
|
68
|
+
open?: boolean;
|
|
69
|
+
step?: number;
|
|
70
|
+
}> & Readonly<{
|
|
71
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
72
|
+
onDismiss?: ((index: number) => any) | undefined;
|
|
73
|
+
"onUpdate:step"?: ((value: number) => any) | undefined;
|
|
74
|
+
onFinish?: (() => any) | undefined;
|
|
75
|
+
}>, {
|
|
76
|
+
padding: number;
|
|
77
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
|
|
78
|
+
card?: (props: {
|
|
79
|
+
step: TourStep | undefined;
|
|
80
|
+
index: number;
|
|
81
|
+
next: () => void;
|
|
82
|
+
back: () => void;
|
|
83
|
+
dismiss: () => void;
|
|
84
|
+
}) => any;
|
|
85
|
+
} & {
|
|
86
|
+
card?: (props: {
|
|
87
|
+
step: TourStep | undefined;
|
|
88
|
+
index: number;
|
|
89
|
+
next: () => void;
|
|
90
|
+
back: () => void;
|
|
91
|
+
dismiss: () => void;
|
|
92
|
+
}) => any;
|
|
93
|
+
}>;
|
|
94
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
95
|
+
new (): {
|
|
96
|
+
$slots: S;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TourStep } from "./LpTour.vue";
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
step: TourStep | undefined;
|
|
4
|
+
index: number;
|
|
5
|
+
last: boolean;
|
|
6
|
+
progress: string;
|
|
7
|
+
labels: {
|
|
8
|
+
next: string;
|
|
9
|
+
back: string;
|
|
10
|
+
done: string;
|
|
11
|
+
close: string;
|
|
12
|
+
perform: string;
|
|
13
|
+
tryIt: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
17
|
+
next: () => any;
|
|
18
|
+
dismiss: () => any;
|
|
19
|
+
back: () => any;
|
|
20
|
+
perform: () => any;
|
|
21
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
22
|
+
onNext?: (() => any) | undefined;
|
|
23
|
+
onDismiss?: (() => any) | undefined;
|
|
24
|
+
onBack?: (() => any) | undefined;
|
|
25
|
+
onPerform?: (() => any) | undefined;
|
|
26
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
27
|
+
declare const _default: typeof __VLS_export;
|
|
28
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Box } from "../composables/spatialNav";
|
|
2
|
+
type Size = {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
};
|
|
6
|
+
/** The hole around a target: padded, then clipped to the viewport so a tall
|
|
7
|
+
* target never pushes the dim panels off-screen. */
|
|
8
|
+
export declare function holeOf(target: Box, padding: number, viewport: Size): Box;
|
|
9
|
+
/** The four dim panels around a hole: top, bottom, left, right. */
|
|
10
|
+
export declare function panelsAround(hole: Box, viewport: Size): Box[];
|
|
11
|
+
/**
|
|
12
|
+
* Open the context menu of an element as a right-click would, at its centre.
|
|
13
|
+
* Lets a tour step show a row's menu without the user hunting for it; the menu
|
|
14
|
+
* itself is whatever the app already wires to that element.
|
|
15
|
+
*
|
|
16
|
+
* Returns the way to close it again: a press outside the menu, which every
|
|
17
|
+
* dismissable layer honours. Not Escape — that key also closes whatever is on
|
|
18
|
+
* top, a tour included, and would end the walk instead of the menu.
|
|
19
|
+
*/
|
|
20
|
+
export declare function openContextMenu(el: Element): () => void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function holeOf(target, padding, viewport) {
|
|
2
|
+
const x = Math.max(0, target.x - padding);
|
|
3
|
+
const y = Math.max(0, target.y - padding);
|
|
4
|
+
const right = Math.min(viewport.width, target.x + target.width + padding);
|
|
5
|
+
const bottom = Math.min(viewport.height, target.y + target.height + padding);
|
|
6
|
+
return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
|
|
7
|
+
}
|
|
8
|
+
function panelsAround(hole, viewport) {
|
|
9
|
+
const bottom = hole.y + hole.height;
|
|
10
|
+
return [
|
|
11
|
+
{ x: 0, y: 0, width: viewport.width, height: hole.y },
|
|
12
|
+
{ x: 0, y: bottom, width: viewport.width, height: Math.max(0, viewport.height - bottom) },
|
|
13
|
+
{ x: 0, y: hole.y, width: hole.x, height: hole.height },
|
|
14
|
+
{ x: hole.x + hole.width, y: hole.y, width: Math.max(0, viewport.width - hole.x - hole.width), height: hole.height }
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
function openContextMenu(el) {
|
|
18
|
+
const box = el.getBoundingClientRect();
|
|
19
|
+
el.dispatchEvent(
|
|
20
|
+
new MouseEvent("contextmenu", {
|
|
21
|
+
bubbles: true,
|
|
22
|
+
cancelable: true,
|
|
23
|
+
button: 2,
|
|
24
|
+
clientX: box.left + box.width / 2,
|
|
25
|
+
clientY: box.top + box.height / 2
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
return () => {
|
|
29
|
+
const outside = { bubbles: true, cancelable: true, clientX: 0, clientY: 0 };
|
|
30
|
+
document.body.dispatchEvent(new PointerEvent("pointerdown", outside));
|
|
31
|
+
document.body.dispatchEvent(new PointerEvent("pointerup", outside));
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
holeOf,
|
|
36
|
+
openContextMenu,
|
|
37
|
+
panelsAround
|
|
38
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ScrollParentOptions {
|
|
2
|
+
/** Which overflow counts: vertical (default), horizontal, or either. */
|
|
3
|
+
axis?: "y" | "x" | "both";
|
|
4
|
+
/** Start the search at the element itself rather than its parent. */
|
|
5
|
+
self?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** The nearest ancestor that scrolls along `axis` and has something to scroll, or null. */
|
|
8
|
+
export declare function scrollParent(el: Element | null | undefined, { axis, self }?: ScrollParentOptions): HTMLElement | null;
|
|
9
|
+
export interface ScrollToElementOptions {
|
|
10
|
+
/** `nearest` scrolls only when the element is out of view. */
|
|
11
|
+
block?: "nearest" | "center" | "start";
|
|
12
|
+
behavior?: ScrollBehavior;
|
|
13
|
+
/** Room kept between the element and the scroller's edge. */
|
|
14
|
+
margin?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Scroll the nearest scroller, and only it, so that `el` is in view. */
|
|
17
|
+
export declare function scrollToElement(el: Element | null | undefined, { block, behavior, margin }?: ScrollToElementOptions): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const SCROLLS = /(auto|scroll|overlay)/;
|
|
2
|
+
function scrollParent(el, { axis = "y", self = false } = {}) {
|
|
3
|
+
for (let node = self ? el : el?.parentElement; node; node = node.parentElement) {
|
|
4
|
+
if (node === document.body || node === document.documentElement) return null;
|
|
5
|
+
const style = getComputedStyle(node);
|
|
6
|
+
const y = axis !== "x" && SCROLLS.test(style.overflowY) && node.scrollHeight > node.clientHeight;
|
|
7
|
+
const x = axis !== "y" && SCROLLS.test(style.overflowX) && node.scrollWidth > node.clientWidth;
|
|
8
|
+
if (y || x) return node;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function scrollToElement(el, { block = "nearest", behavior = "auto", margin = 0 } = {}) {
|
|
13
|
+
if (!el) return;
|
|
14
|
+
const box = scrollParent(el) ?? document.scrollingElement;
|
|
15
|
+
if (!box) return;
|
|
16
|
+
const outer = box === document.scrollingElement ? new DOMRect(0, 0, window.innerWidth, window.innerHeight) : box.getBoundingClientRect();
|
|
17
|
+
const inner = el.getBoundingClientRect();
|
|
18
|
+
const top = inner.top - outer.top + box.scrollTop;
|
|
19
|
+
let target = box.scrollTop;
|
|
20
|
+
if (block === "start") target = top - margin;
|
|
21
|
+
else if (block === "center") target = top - (box.clientHeight - inner.height) / 2;
|
|
22
|
+
else if (inner.top < outer.top + margin) target = top - margin;
|
|
23
|
+
else if (inner.bottom > outer.bottom - margin) target = top - box.clientHeight + inner.height + margin;
|
|
24
|
+
if (target !== box.scrollTop) box.scrollTo({ top: target, behavior });
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
scrollParent,
|
|
28
|
+
scrollToElement
|
|
29
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ref, onMounted, onBeforeUnmount, toValue } from "vue";
|
|
2
2
|
import { focusableIn, nextInDirection } from "./spatialNav.js";
|
|
3
|
+
import { scrollParent } from "./scroll.js";
|
|
3
4
|
const BUTTON = { a: 0, b: 1, lb: 4, rb: 5, start: 9, up: 12, down: 13, left: 14, right: 15 };
|
|
4
5
|
const STICK_THRESHOLD = 0.55;
|
|
5
6
|
const REPEAT_DELAY_MS = 380;
|
|
@@ -76,13 +77,7 @@ function useGamepadNav(options = {}) {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
function scrollBy(dx, dy) {
|
|
79
|
-
|
|
80
|
-
while (element && element !== document.body) {
|
|
81
|
-
const style = getComputedStyle(element);
|
|
82
|
-
if (/(auto|scroll)/.test(style.overflowY + style.overflowX) && element.scrollHeight > element.clientHeight) break;
|
|
83
|
-
element = element.parentElement;
|
|
84
|
-
}
|
|
85
|
-
(element && element !== document.body ? element : document.scrollingElement)?.scrollBy(dx, dy);
|
|
80
|
+
(scrollParent(document.activeElement, { axis: "both", self: true }) ?? document.scrollingElement)?.scrollBy(dx, dy);
|
|
86
81
|
}
|
|
87
82
|
function poll(now) {
|
|
88
83
|
frame = requestAnimationFrame(poll);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Directive } from "vue";
|
|
2
|
+
export type HintEvent = "seen" | "used" | "hover";
|
|
3
|
+
type Listener = (id: string, el: HTMLElement) => void;
|
|
4
|
+
/** `v-hint="'page.feature'"` on the element that represents a feature. */
|
|
5
|
+
export declare const vHint: Directive<HTMLElement, string | null | undefined>;
|
|
6
|
+
export declare const hints: {
|
|
7
|
+
/** Ids of everything hinted that is in the DOM now. Reactive. */
|
|
8
|
+
ids(): string[];
|
|
9
|
+
/** Whether an element with this id is in the DOM. Reactive. */
|
|
10
|
+
present(id: string): boolean;
|
|
11
|
+
/** The first connected element with this id that has a box, or null. */
|
|
12
|
+
element(id: string): HTMLElement | null;
|
|
13
|
+
/** Subscribe to a signal; returns the unsubscribe. */
|
|
14
|
+
on(event: HintEvent, listener: Listener): () => void;
|
|
15
|
+
/** Report use that is not a click on the element. */
|
|
16
|
+
use(id: string): void;
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { shallowReactive } from "vue";
|
|
2
|
+
const SEEN_MS = 800;
|
|
3
|
+
const SEEN_RATIO = 0.5;
|
|
4
|
+
const mounted = shallowReactive(/* @__PURE__ */ new Map());
|
|
5
|
+
const listeners = { seen: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set(), hover: /* @__PURE__ */ new Set() };
|
|
6
|
+
const idOf = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
const timers = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
const seenOnce = /* @__PURE__ */ new WeakSet();
|
|
9
|
+
function emit(event, id, el) {
|
|
10
|
+
for (const listener of listeners[event]) listener(id, el);
|
|
11
|
+
}
|
|
12
|
+
let observer = null;
|
|
13
|
+
function watchSight(el) {
|
|
14
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
15
|
+
seenOnce.add(el);
|
|
16
|
+
emit("seen", idOf.get(el), el);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
observer ??= new IntersectionObserver(
|
|
20
|
+
(entries) => {
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const target = entry.target;
|
|
23
|
+
const id = idOf.get(target);
|
|
24
|
+
if (!id || seenOnce.has(target)) continue;
|
|
25
|
+
clearTimeout(timers.get(target));
|
|
26
|
+
if (entry.intersectionRatio >= SEEN_RATIO) {
|
|
27
|
+
timers.set(
|
|
28
|
+
target,
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
seenOnce.add(target);
|
|
31
|
+
observer?.unobserve(target);
|
|
32
|
+
emit("seen", id, target);
|
|
33
|
+
}, SEEN_MS)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{ threshold: [0, SEEN_RATIO] }
|
|
39
|
+
);
|
|
40
|
+
observer.observe(el);
|
|
41
|
+
}
|
|
42
|
+
function onClick(event) {
|
|
43
|
+
const el = event.currentTarget;
|
|
44
|
+
const id = idOf.get(el);
|
|
45
|
+
if (id) emit("used", id, el);
|
|
46
|
+
}
|
|
47
|
+
function onHover(event) {
|
|
48
|
+
const el = event.currentTarget;
|
|
49
|
+
const id = idOf.get(el);
|
|
50
|
+
if (id) emit("hover", id, el);
|
|
51
|
+
}
|
|
52
|
+
function attach(el, id) {
|
|
53
|
+
idOf.set(el, id);
|
|
54
|
+
el.dataset.hint = id;
|
|
55
|
+
const set = mounted.get(id) ?? /* @__PURE__ */ new Set();
|
|
56
|
+
set.add(el);
|
|
57
|
+
mounted.set(id, new Set(set));
|
|
58
|
+
el.addEventListener("click", onClick, { capture: true });
|
|
59
|
+
el.addEventListener("pointerenter", onHover);
|
|
60
|
+
el.addEventListener("focusin", onHover);
|
|
61
|
+
watchSight(el);
|
|
62
|
+
}
|
|
63
|
+
function detach(el) {
|
|
64
|
+
const id = idOf.get(el);
|
|
65
|
+
if (!id) return;
|
|
66
|
+
clearTimeout(timers.get(el));
|
|
67
|
+
observer?.unobserve(el);
|
|
68
|
+
el.removeEventListener("click", onClick, { capture: true });
|
|
69
|
+
el.removeEventListener("pointerenter", onHover);
|
|
70
|
+
el.removeEventListener("focusin", onHover);
|
|
71
|
+
const set = new Set(mounted.get(id));
|
|
72
|
+
set.delete(el);
|
|
73
|
+
if (set.size) mounted.set(id, set);
|
|
74
|
+
else mounted.delete(id);
|
|
75
|
+
idOf.delete(el);
|
|
76
|
+
}
|
|
77
|
+
const vHint = {
|
|
78
|
+
mounted(el, binding) {
|
|
79
|
+
if (binding.value) attach(el, binding.value);
|
|
80
|
+
},
|
|
81
|
+
updated(el, binding) {
|
|
82
|
+
if (binding.value === idOf.get(el)) return;
|
|
83
|
+
detach(el);
|
|
84
|
+
if (binding.value) attach(el, binding.value);
|
|
85
|
+
},
|
|
86
|
+
beforeUnmount(el) {
|
|
87
|
+
detach(el);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const hints = {
|
|
91
|
+
/** Ids of everything hinted that is in the DOM now. Reactive. */
|
|
92
|
+
ids() {
|
|
93
|
+
return [...mounted.keys()];
|
|
94
|
+
},
|
|
95
|
+
/** Whether an element with this id is in the DOM. Reactive. */
|
|
96
|
+
present(id) {
|
|
97
|
+
return mounted.has(id);
|
|
98
|
+
},
|
|
99
|
+
/** The first connected element with this id that has a box, or null. */
|
|
100
|
+
element(id) {
|
|
101
|
+
for (const el of mounted.get(id) ?? []) {
|
|
102
|
+
if (el.isConnected && el.getClientRects().length) return el;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
},
|
|
106
|
+
/** Subscribe to a signal; returns the unsubscribe. */
|
|
107
|
+
on(event, listener) {
|
|
108
|
+
listeners[event].add(listener);
|
|
109
|
+
return () => listeners[event].delete(listener);
|
|
110
|
+
},
|
|
111
|
+
/** Report use that is not a click on the element. */
|
|
112
|
+
use(id) {
|
|
113
|
+
const el = hints.element(id);
|
|
114
|
+
for (const listener of listeners.used) listener(id, el ?? document.body);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
export {
|
|
118
|
+
hints,
|
|
119
|
+
vHint
|
|
120
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ref, onMounted, onBeforeUnmount } from "vue";
|
|
2
|
+
function useMediaQuery(query) {
|
|
3
|
+
const matches = ref(false);
|
|
4
|
+
let list;
|
|
5
|
+
const update = () => {
|
|
6
|
+
matches.value = !!list?.matches;
|
|
7
|
+
};
|
|
8
|
+
onMounted(() => {
|
|
9
|
+
list = window.matchMedia?.(query);
|
|
10
|
+
update();
|
|
11
|
+
list?.addEventListener("change", update);
|
|
12
|
+
});
|
|
13
|
+
onBeforeUnmount(() => list?.removeEventListener("change", update));
|
|
14
|
+
return matches;
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
useMediaQuery
|
|
18
|
+
};
|