@leavepulse/ui 0.20.8 → 0.21.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/assets/{LpCommandPalette-C3LdDNHk.css → LpCommandPalette-CEY-pajO.css} +4 -4
- package/dist/assets/LpShortcutsDialog-B7H-RZLQ.css +55 -0
- package/dist/chunks/{LpAddressInput-DaJ97mQ4.js → LpAddressInput-B411MOJa.js} +1 -1
- package/dist/chunks/{LpAppShell-Ds3WkurD.js → LpAppShell-CfM7fdr3.js} +2 -2
- package/dist/chunks/{LpConfirmDialog-Cwqzbfgo.js → LpConfirmDialog-D8pBGPOI.js} +1 -1
- package/dist/chunks/{LpDrawer-DcxlpxJ7.js → LpDrawer-B4cNj39a.js} +1 -1
- package/dist/chunks/{LpFileTree-BGNzXknB.js → LpFileTree-d9nXtSVo.js} +1 -1
- package/dist/chunks/{LpLogViewer-DQod8kXW.js → LpLogViewer-BXPBUV21.js} +1 -1
- package/dist/chunks/{LpModal-2YEJI45Q.js → LpModal-DKFdzKwV.js} +1 -1
- package/dist/chunks/{LpNotificationBell-D1gdEtuI.js → LpNotificationBell-Nges4kJk.js} +1 -1
- package/dist/chunks/{LpScrollArea-CXy8dkTN.js → LpScrollArea-Du8tW8c5.js} +2 -2
- package/dist/chunks/{LpSidebar-B_UD_yUx.js → LpSidebar-Bd0148dd.js} +2 -2
- package/dist/chunks/{LpSidebarNav-S1QPTGcg.js → LpSidebarNav-BqGpNr4S.js} +1 -1
- package/dist/chunks/{LpTable-pZmjnEti.js → LpTable-BBKlNR9j.js} +1 -1
- package/dist/chunks/_plugin-vue_export-helper-1tPrXgE0.js +10 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +17 -21
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +25 -0
- package/dist/components/LpShortcutsDialog.vue.js +63 -0
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpTable.vue.js +1 -1
- package/dist/composables/useHotkeys.d.ts +45 -0
- package/dist/composables/useHotkeys.js +53 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +17 -11
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpCommandPalette.vue +18 -11
- package/src/components/LpScrollArea.vue +7 -2
- package/src/components/LpShortcutsDialog.vue +127 -0
- package/src/composables/useHotkeys.ts +131 -0
- package/src/index.ts +4 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
2
|
/* Each result eases in; honours reduced motion. */
|
|
3
|
-
.lp-cmd-item[data-v-
|
|
4
|
-
animation: lp-cmd-in-
|
|
3
|
+
.lp-cmd-item[data-v-a23a2f5f] {
|
|
4
|
+
animation: lp-cmd-in-a23a2f5f 140ms ease-out both;
|
|
5
5
|
}
|
|
6
|
-
@keyframes lp-cmd-in-
|
|
6
|
+
@keyframes lp-cmd-in-a23a2f5f {
|
|
7
7
|
from {
|
|
8
8
|
opacity: 0;
|
|
9
9
|
transform: translateY(3px);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
@media (prefers-reduced-motion: reduce) {
|
|
13
|
-
.lp-cmd-item[data-v-
|
|
13
|
+
.lp-cmd-item[data-v-a23a2f5f] {
|
|
14
14
|
animation: none;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
/* Two columns where there is room: the list is long enough that one column
|
|
3
|
+
pushes the last group off a laptop screen. */
|
|
4
|
+
.shortcut-groups[data-v-d86b724b] {
|
|
5
|
+
display: grid;
|
|
6
|
+
gap: 18px;
|
|
7
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
8
|
+
}
|
|
9
|
+
.shortcut-group h3[data-v-d86b724b] {
|
|
10
|
+
margin: 0 0 6px;
|
|
11
|
+
color: var(--color-muted);
|
|
12
|
+
font-size: 0.7rem;
|
|
13
|
+
font-weight: 600;
|
|
14
|
+
letter-spacing: 0.06em;
|
|
15
|
+
text-transform: uppercase;
|
|
16
|
+
}
|
|
17
|
+
.shortcut-group dl[data-v-d86b724b] {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 1px;
|
|
21
|
+
margin: 0;
|
|
22
|
+
}
|
|
23
|
+
.shortcut-row[data-v-d86b724b] {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
gap: 12px;
|
|
28
|
+
padding: 5px 0;
|
|
29
|
+
}
|
|
30
|
+
.shortcut-row dt[data-v-d86b724b] {
|
|
31
|
+
color: var(--color-ink);
|
|
32
|
+
font-size: 0.82rem;
|
|
33
|
+
min-width: 0;
|
|
34
|
+
}
|
|
35
|
+
.shortcut-row dd[data-v-d86b724b] {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-shrink: 0;
|
|
38
|
+
gap: 3px;
|
|
39
|
+
margin: 0;
|
|
40
|
+
}
|
|
41
|
+
kbd[data-v-d86b724b] {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
min-width: 20px;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
padding: 2px 6px;
|
|
47
|
+
border: 1px solid var(--color-line-strong);
|
|
48
|
+
border-bottom-width: 2px;
|
|
49
|
+
border-radius: 5px;
|
|
50
|
+
background: color-mix(in srgb, var(--color-ink) 6%, transparent);
|
|
51
|
+
color: var(--color-muted-strong);
|
|
52
|
+
font-family: inherit;
|
|
53
|
+
font-size: 0.7rem;
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
}
|
|
@@ -6,7 +6,7 @@ import { _ as _sfc_main$3 } from "./LpButton-5VXXCjwF.js";
|
|
|
6
6
|
import { _ as _sfc_main$1 } from "./LpFormField-Bn-ZwC6z.js";
|
|
7
7
|
import { _ as _sfc_main$4 } from "./LpIcon-CCnX5_2j.js";
|
|
8
8
|
import { _ as _sfc_main$5 } from "./LpInput-BauZRyzm.js";
|
|
9
|
-
import { _ as _sfc_main$7 } from "./LpModal-
|
|
9
|
+
import { _ as _sfc_main$7 } from "./LpModal-DKFdzKwV.js";
|
|
10
10
|
import { _ as _sfc_main$6 } from "./LpSelect-D5mU4cZO.js";
|
|
11
11
|
const _hoisted_1 = { class: "flex flex-col gap-3" };
|
|
12
12
|
const _hoisted_2 = { class: "flex items-stretch gap-2" };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { defineComponent, computed, watch, openBlock, createElementBlock, createVNode, createSlots, withCtx, renderSlot, normalizeProps, guardReactiveProps, createElementVNode, normalizeClass, toDisplayString, createCommentVNode, createBlock } from "vue";
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
|
|
3
3
|
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
4
|
-
import { _ as _sfc_main$4 } from "./LpScrollArea-
|
|
5
|
-
import { _ as _sfc_main$1 } from "./LpSidebar-
|
|
4
|
+
import { _ as _sfc_main$4 } from "./LpScrollArea-Du8tW8c5.js";
|
|
5
|
+
import { _ as _sfc_main$1 } from "./LpSidebar-Bd0148dd.js";
|
|
6
6
|
const _hoisted_1 = { class: "flex h-dvh overflow-hidden bg-surface text-ink" };
|
|
7
7
|
const _hoisted_2 = { class: "flex min-w-0 flex-1 flex-col" };
|
|
8
8
|
const _hoisted_3 = { class: "flex h-16 shrink-0 items-center gap-3 border-b border-line bg-surface px-4 md:px-6" };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineComponent, openBlock, createBlock, withCtx, renderSlot, createVNode, createTextVNode, toDisplayString } from "vue";
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
|
|
3
|
-
import { _ as _sfc_main$1 } from "./LpModal-
|
|
3
|
+
import { _ as _sfc_main$1 } from "./LpModal-DKFdzKwV.js";
|
|
4
4
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
5
5
|
__name: "LpConfirmDialog",
|
|
6
6
|
props: {
|
|
@@ -3,7 +3,7 @@ import { usePointerSwipe } from "@vueuse/core";
|
|
|
3
3
|
import { DrawerRoot, DrawerPortal, DrawerOverlay, DrawerContent, DrawerHandle, DrawerTitle, DrawerDescription, DrawerClose } from "vaul-vue";
|
|
4
4
|
import { CLOSE_ICON } from "../components/dropdown.js";
|
|
5
5
|
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
6
|
-
import { _ as _sfc_main$2 } from "./LpScrollArea-
|
|
6
|
+
import { _ as _sfc_main$2 } from "./LpScrollArea-Du8tW8c5.js";
|
|
7
7
|
const _hoisted_1 = { class: "text-base font-semibold text-ink" };
|
|
8
8
|
const _hoisted_2 = { class: "flex flex-col gap-1" };
|
|
9
9
|
const _hoisted_3 = ["data-vaul-no-drag"];
|
|
@@ -2,7 +2,7 @@ import { defineComponent, ref, watch, computed, openBlock, createElementBlock, F
|
|
|
2
2
|
import { formatSize, sortNodes, computeStats, subtreeIds, ancestorIds } from "../components/fileTree.js";
|
|
3
3
|
import { _ as _sfc_main$1 } from "./LpEmptyState-CrfyRJUG.js";
|
|
4
4
|
import { _ as _sfc_main$3 } from "./LpFileTreeNode-DFg4PhOK.js";
|
|
5
|
-
import { _ as _sfc_main$2 } from "./LpScrollArea-
|
|
5
|
+
import { _ as _sfc_main$2 } from "./LpScrollArea-Du8tW8c5.js";
|
|
6
6
|
const _hoisted_1 = {
|
|
7
7
|
key: 0,
|
|
8
8
|
class: "flex h-full w-full min-w-0 flex-col gap-1 overflow-hidden"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineComponent, computed, ref, watch, nextTick, onMounted, onBeforeUnmount, openBlock, createElementBlock, createVNode, normalizeStyle, withCtx, createElementVNode, toDisplayString, createCommentVNode, Fragment, createBlock, resolveDynamicComponent, TransitionGroup, mergeProps, renderList, normalizeClass, createTextVNode, Transition } from "vue";
|
|
2
2
|
import { _ as _sfc_main$3 } from "./LpContextMenu-D0mzXqA7.js";
|
|
3
3
|
import { _ as _sfc_main$2 } from "./LpIcon-CCnX5_2j.js";
|
|
4
|
-
import { _ as _sfc_main$1 } from "./LpScrollArea-
|
|
4
|
+
import { _ as _sfc_main$1 } from "./LpScrollArea-Du8tW8c5.js";
|
|
5
5
|
const _hoisted_1 = { class: "relative overflow-hidden rounded-card border border-line bg-surface font-mono text-xs leading-relaxed" };
|
|
6
6
|
const _hoisted_2 = {
|
|
7
7
|
key: 0,
|
|
@@ -2,7 +2,7 @@ import { defineComponent, computed, useSlots, ref, watch, onBeforeUnmount, openB
|
|
|
2
2
|
import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from "reka-ui";
|
|
3
3
|
import { CLOSE_ICON } from "../components/dropdown.js";
|
|
4
4
|
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
5
|
-
import { _ as _sfc_main$2 } from "./LpScrollArea-
|
|
5
|
+
import { _ as _sfc_main$2 } from "./LpScrollArea-Du8tW8c5.js";
|
|
6
6
|
const _hoisted_1 = { class: "fixed inset-0 z-(--z-modal) flex items-center justify-center pointer-events-none" };
|
|
7
7
|
const _hoisted_2 = {
|
|
8
8
|
key: 0,
|
|
@@ -5,7 +5,7 @@ import { _ as _sfc_main$4 } from "./LpContextMenu-D0mzXqA7.js";
|
|
|
5
5
|
import { _ as _sfc_main$2 } from "./LpEmptyState-CrfyRJUG.js";
|
|
6
6
|
import { _ as _sfc_main$5 } from "./LpIcon-CCnX5_2j.js";
|
|
7
7
|
import { _ as _sfc_main$1 } from "./LpPopover-8268UmlC.js";
|
|
8
|
-
import { _ as _sfc_main$3 } from "./LpScrollArea-
|
|
8
|
+
import { _ as _sfc_main$3 } from "./LpScrollArea-Du8tW8c5.js";
|
|
9
9
|
const _hoisted_1 = { class: "flex items-center justify-between gap-2 px-3 py-2.5" };
|
|
10
10
|
const _hoisted_2 = { class: "text-sm font-semibold" };
|
|
11
11
|
const _hoisted_3 = {
|
|
@@ -90,7 +90,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
90
90
|
createVNode(unref(ScrollAreaScrollbar), {
|
|
91
91
|
orientation: "vertical",
|
|
92
92
|
"force-mount": "",
|
|
93
|
-
class: normalizeClass(["group flex w-3.5 touch-none select-none justify-end py-px pl-2.5 pr-px", barFade]),
|
|
93
|
+
class: normalizeClass(["group z-1 flex w-3.5 touch-none select-none justify-end py-px pl-2.5 pr-px", barFade]),
|
|
94
94
|
style: normalizeStyle(__props.barInsetTop ? { marginTop: __props.barInsetTop } : void 0)
|
|
95
95
|
}, {
|
|
96
96
|
default: withCtx(() => [
|
|
@@ -103,7 +103,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
103
103
|
}, 8, ["style"]),
|
|
104
104
|
createVNode(unref(ScrollAreaScrollbar), {
|
|
105
105
|
orientation: "horizontal",
|
|
106
|
-
class: normalizeClass(["group flex h-3.5 flex-col touch-none select-none justify-end px-px pb-px pt-2.5", barFade])
|
|
106
|
+
class: normalizeClass(["group z-1 flex h-3.5 flex-col touch-none select-none justify-end px-px pb-px pt-2.5", barFade])
|
|
107
107
|
}, {
|
|
108
108
|
default: withCtx(() => [
|
|
109
109
|
createVNode(unref(ScrollAreaThumb), {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineComponent, computed, useId, openBlock, createElementBlock, Fragment, createElementVNode, mergeProps, normalizeClass, renderSlot, createCommentVNode, createVNode, createSlots, withCtx, normalizeProps, guardReactiveProps, createBlock } from "vue";
|
|
2
|
-
import { _ as _sfc_main$2 } from "./LpDrawer-
|
|
3
|
-
import { _ as _sfc_main$1 } from "./LpSidebarNav-
|
|
2
|
+
import { _ as _sfc_main$2 } from "./LpDrawer-B4cNj39a.js";
|
|
3
|
+
import { _ as _sfc_main$1 } from "./LpSidebarNav-BqGpNr4S.js";
|
|
4
4
|
const _hoisted_1 = {
|
|
5
5
|
key: 1,
|
|
6
6
|
class: "mt-2 flex shrink-0 flex-col gap-2"
|
|
@@ -4,7 +4,7 @@ import { usePillTransition } from "../composables/usePillTransition.js";
|
|
|
4
4
|
import { _ as _sfc_main$4 } from "./LpBadge-CXzBPnwO.js";
|
|
5
5
|
import { _ as _sfc_main$2 } from "./LpContextMenu-D0mzXqA7.js";
|
|
6
6
|
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
7
|
-
import { _ as _sfc_main$1 } from "./LpScrollArea-
|
|
7
|
+
import { _ as _sfc_main$1 } from "./LpScrollArea-Du8tW8c5.js";
|
|
8
8
|
const _hoisted_1 = {
|
|
9
9
|
key: 0,
|
|
10
10
|
class: "flex min-h-0 flex-1 flex-col gap-4"
|
|
@@ -2,7 +2,7 @@ import { defineComponent, computed, ref, watch, onBeforeUnmount, openBlock, crea
|
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpCheckbox-BO0zuuPh.js";
|
|
3
3
|
import { _ as _sfc_main$4 } from "./LpContextMenu-D0mzXqA7.js";
|
|
4
4
|
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
5
|
-
import { _ as _sfc_main$1 } from "./LpScrollArea-
|
|
5
|
+
import { _ as _sfc_main$1 } from "./LpScrollArea-Du8tW8c5.js";
|
|
6
6
|
const _hoisted_1 = { class: "w-full border-collapse text-sm" };
|
|
7
7
|
const _hoisted_2 = { class: "border-b border-line bg-surface-soft" };
|
|
8
8
|
const _hoisted_3 = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const COMPONENT_NAMES: readonly ["LayoutCanvas", "LayoutNode", "LpAddressInput", "LpAlert", "LpAppShell", "LpAutocomplete", "LpAvatar", "LpBadge", "LpBreadcrumbs", "LpButton", "LpCalendar", "LpCard", "LpCheckbox", "LpCodeBlock", "LpCommandPalette", "LpConfirmDialog", "LpContextMenu", "LpDatePicker", "LpDisclosure", "LpDivider", "LpDrawer", "LpDropdownMenu", "LpEmptyState", "LpFileTree", "LpFileTreeNode", "LpFormField", "LpIcon", "LpInfraNode", "LpInput", "LpLaneNode", "LpLightbox", "LpLink", "LpLogViewer", "LpMapPicker", "LpModal", "LpNotificationBell", "LpNumberField", "LpOtpInput", "LpPagination", "LpPasswordInput", "LpPhoneInput", "LpPopover", "LpProgress", "LpRadio", "LpRadioGroup", "LpScrollArea", "LpSegmented", "LpSelect", "LpServiceNode", "LpSidebar", "LpSidebarNav", "LpSkeleton", "LpSlider", "LpStat", "LpStepper", "LpSwitch", "LpTable", "LpTableOfContents", "LpTabs", "LpTextarea", "LpThemeSwitcher", "LpTilt", "LpToaster", "LpTooltip", "LpTopologyCanvas", "LpUptimeBar"];
|
|
1
|
+
export declare const COMPONENT_NAMES: readonly ["LayoutCanvas", "LayoutNode", "LpAddressInput", "LpAlert", "LpAppShell", "LpAutocomplete", "LpAvatar", "LpBadge", "LpBreadcrumbs", "LpButton", "LpCalendar", "LpCard", "LpCheckbox", "LpCodeBlock", "LpCommandPalette", "LpConfirmDialog", "LpContextMenu", "LpDatePicker", "LpDisclosure", "LpDivider", "LpDrawer", "LpDropdownMenu", "LpEmptyState", "LpFileTree", "LpFileTreeNode", "LpFormField", "LpIcon", "LpInfraNode", "LpInput", "LpLaneNode", "LpLightbox", "LpLink", "LpLogViewer", "LpMapPicker", "LpModal", "LpNotificationBell", "LpNumberField", "LpOtpInput", "LpPagination", "LpPasswordInput", "LpPhoneInput", "LpPopover", "LpProgress", "LpRadio", "LpRadioGroup", "LpScrollArea", "LpSegmented", "LpSelect", "LpServiceNode", "LpShortcutsDialog", "LpSidebar", "LpSidebarNav", "LpSkeleton", "LpSlider", "LpStat", "LpStepper", "LpSwitch", "LpTable", "LpTableOfContents", "LpTabs", "LpTextarea", "LpThemeSwitcher", "LpTilt", "LpToaster", "LpTooltip", "LpTopologyCanvas", "LpUptimeBar"];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { defineComponent, ref, computed, watch, nextTick,
|
|
1
|
+
import { defineComponent, ref, computed, watch, nextTick, openBlock, createBlock, unref, withCtx, createVNode, withModifiers, createTextVNode, createElementVNode, withDirectives, withKeys, vModelText, createElementBlock, toDisplayString, createCommentVNode, Fragment, renderList, normalizeClass } from "vue";
|
|
2
2
|
import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription } from "reka-ui";
|
|
3
3
|
import { _ as _sfc_main$1 } from "../chunks/LpIcon-CCnX5_2j.js";
|
|
4
|
-
import { _ as _sfc_main$2 } from "../chunks/LpScrollArea-
|
|
5
|
-
import
|
|
4
|
+
import { _ as _sfc_main$2 } from "../chunks/LpScrollArea-Du8tW8c5.js";
|
|
5
|
+
import { useHotkeys } from "../composables/useHotkeys.js";
|
|
6
|
+
import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
|
|
7
|
+
import '../assets/LpCommandPalette-CEY-pajO.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
|
|
6
8
|
const _hoisted_2 = ["placeholder", "onKeydown"];
|
|
7
9
|
const _hoisted_3 = {
|
|
8
10
|
key: 0,
|
|
@@ -149,16 +151,17 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
154
|
+
useHotkeys(
|
|
155
|
+
() => props.hotkey ? [
|
|
156
|
+
{
|
|
157
|
+
key: "k",
|
|
158
|
+
code: "KeyK",
|
|
159
|
+
ctrl: true,
|
|
160
|
+
allowInInput: true,
|
|
161
|
+
handler: () => emit("update:open", !props.open)
|
|
162
|
+
}
|
|
163
|
+
] : []
|
|
164
|
+
);
|
|
162
165
|
return (_ctx, _cache) => {
|
|
163
166
|
return openBlock(), createBlock(unref(DialogRoot), {
|
|
164
167
|
open: __props.open,
|
|
@@ -297,14 +300,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
297
300
|
};
|
|
298
301
|
}
|
|
299
302
|
});
|
|
300
|
-
const
|
|
301
|
-
const target = sfc.__vccOpts || sfc;
|
|
302
|
-
for (const [key, val] of props) {
|
|
303
|
-
target[key] = val;
|
|
304
|
-
}
|
|
305
|
-
return target;
|
|
306
|
-
};
|
|
307
|
-
const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-75a17488"]]);
|
|
303
|
+
const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-a23a2f5f"]]);
|
|
308
304
|
export {
|
|
309
305
|
LpCommandPalette as default
|
|
310
306
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ShortcutEntry {
|
|
2
|
+
/** Rendered one <kbd> per "+"-separated key, so write it the way it reads. */
|
|
3
|
+
combo: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ShortcutGroup {
|
|
7
|
+
title: string;
|
|
8
|
+
entries: ShortcutEntry[];
|
|
9
|
+
}
|
|
10
|
+
type __VLS_Props = {
|
|
11
|
+
open?: boolean;
|
|
12
|
+
groups: ShortcutGroup[];
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
|
|
17
|
+
"update:open": (value: boolean) => any;
|
|
18
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
19
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
20
|
+
}>, {
|
|
21
|
+
title: string;
|
|
22
|
+
open: boolean;
|
|
23
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
24
|
+
declare const _default: typeof __VLS_export;
|
|
25
|
+
export default _default;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { defineComponent, openBlock, createBlock, withCtx, createElementVNode, createElementBlock, Fragment, renderList, toDisplayString, unref } from "vue";
|
|
2
|
+
import { _ as _sfc_main$1 } from "../chunks/LpModal-DKFdzKwV.js";
|
|
3
|
+
import { appleModifierLabel } from "../composables/useHotkeys.js";
|
|
4
|
+
import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
|
|
5
|
+
import '../assets/LpShortcutsDialog-B7H-RZLQ.css';const _hoisted_1 = { class: "shortcut-groups" };
|
|
6
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
7
|
+
__name: "LpShortcutsDialog",
|
|
8
|
+
props: {
|
|
9
|
+
open: { type: Boolean, default: false },
|
|
10
|
+
groups: {},
|
|
11
|
+
title: { default: "Keyboard shortcuts" },
|
|
12
|
+
description: {}
|
|
13
|
+
},
|
|
14
|
+
emits: ["update:open"],
|
|
15
|
+
setup(__props, { emit: __emit }) {
|
|
16
|
+
const props = __props;
|
|
17
|
+
const emit = __emit;
|
|
18
|
+
return (_ctx, _cache) => {
|
|
19
|
+
return openBlock(), createBlock(_sfc_main$1, {
|
|
20
|
+
open: props.open,
|
|
21
|
+
size: "lg",
|
|
22
|
+
title: props.title,
|
|
23
|
+
description: props.description,
|
|
24
|
+
"onUpdate:open": _cache[0] || (_cache[0] = (value) => emit("update:open", value))
|
|
25
|
+
}, {
|
|
26
|
+
default: withCtx(() => [
|
|
27
|
+
createElementVNode("div", _hoisted_1, [
|
|
28
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(props.groups, (group) => {
|
|
29
|
+
return openBlock(), createElementBlock("section", {
|
|
30
|
+
key: group.title,
|
|
31
|
+
class: "shortcut-group"
|
|
32
|
+
}, [
|
|
33
|
+
createElementVNode("h3", null, toDisplayString(group.title), 1),
|
|
34
|
+
createElementVNode("dl", null, [
|
|
35
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(group.entries, (entry) => {
|
|
36
|
+
return openBlock(), createElementBlock("div", {
|
|
37
|
+
key: entry.combo,
|
|
38
|
+
class: "shortcut-row"
|
|
39
|
+
}, [
|
|
40
|
+
createElementVNode("dt", null, toDisplayString(entry.label), 1),
|
|
41
|
+
createElementVNode("dd", null, [
|
|
42
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(entry.combo.split("+"), (key, index) => {
|
|
43
|
+
return openBlock(), createElementBlock("kbd", {
|
|
44
|
+
key: `${key}-${index}`
|
|
45
|
+
}, toDisplayString(unref(appleModifierLabel)(key.trim())), 1);
|
|
46
|
+
}), 128))
|
|
47
|
+
])
|
|
48
|
+
]);
|
|
49
|
+
}), 128))
|
|
50
|
+
])
|
|
51
|
+
]);
|
|
52
|
+
}), 128))
|
|
53
|
+
])
|
|
54
|
+
]),
|
|
55
|
+
_: 1
|
|
56
|
+
}, 8, ["open", "title", "description"]);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
const LpShortcutsDialog = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d86b724b"]]);
|
|
61
|
+
export {
|
|
62
|
+
LpShortcutsDialog as default
|
|
63
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MaybeRefOrGetter } from 'vue';
|
|
2
|
+
export interface HotkeyBinding {
|
|
3
|
+
/** Layout-produced key, compared case-insensitively ("l", "f2", "escape"). */
|
|
4
|
+
key: string;
|
|
5
|
+
/**
|
|
6
|
+
* Physical key position ("KeyL", "F2", "Digit1"). Matches on its own, which
|
|
7
|
+
* is what makes non-Latin layouts work.
|
|
8
|
+
*/
|
|
9
|
+
code?: string;
|
|
10
|
+
/** Ctrl on Windows/Linux, ⌘ on macOS — the two are treated as one. */
|
|
11
|
+
ctrl?: boolean;
|
|
12
|
+
shift?: boolean;
|
|
13
|
+
alt?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Runs even while a text field has focus. Only for shortcuts that mean
|
|
16
|
+
* something there — Escape, and the palette the user reaches from anywhere.
|
|
17
|
+
*/
|
|
18
|
+
allowInInput?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Returning `false` declines the key: the event keeps its default and the
|
|
21
|
+
* search moves on. This is what lets a binding defer to a layer that owns the
|
|
22
|
+
* key better than it does — Escape belongs to whichever overlay is on top,
|
|
23
|
+
* and swallowing it here would close all of them.
|
|
24
|
+
*/
|
|
25
|
+
handler: () => void | boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether this machine prints ⌘ where the rest print Ctrl. Read once: the
|
|
29
|
+
* keyboard does not change under a running window, and the check is only ever
|
|
30
|
+
* used to pick a label.
|
|
31
|
+
*/
|
|
32
|
+
export declare const isAppleKeyboard: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The symbol to print for a modifier on this platform. Bindings treat Ctrl and
|
|
35
|
+
* Meta as one modifier, so only the label may differ — never the binding.
|
|
36
|
+
*/
|
|
37
|
+
export declare function appleModifierLabel(key: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Bind a list of hotkeys for as long as the calling component is mounted.
|
|
40
|
+
*
|
|
41
|
+
* The list is read on every keystroke rather than captured once, so bindings
|
|
42
|
+
* may come and go with the state that owns them — a getter is the point, not a
|
|
43
|
+
* formality.
|
|
44
|
+
*/
|
|
45
|
+
export declare function useHotkeys(bindings: MaybeRefOrGetter<HotkeyBinding[]>): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { onMounted, onBeforeUnmount, toValue } from "vue";
|
|
2
|
+
const isAppleKeyboard = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform || "");
|
|
3
|
+
function appleModifierLabel(key) {
|
|
4
|
+
if (!isAppleKeyboard) return key;
|
|
5
|
+
if (key === "Ctrl") return "⌘";
|
|
6
|
+
if (key === "Alt") return "⌥";
|
|
7
|
+
return key;
|
|
8
|
+
}
|
|
9
|
+
const TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
10
|
+
"text",
|
|
11
|
+
"search",
|
|
12
|
+
"email",
|
|
13
|
+
"number",
|
|
14
|
+
"password",
|
|
15
|
+
"tel",
|
|
16
|
+
"url"
|
|
17
|
+
]);
|
|
18
|
+
function isTextEntry(target) {
|
|
19
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
20
|
+
if (target.isContentEditable) return true;
|
|
21
|
+
if (target instanceof HTMLTextAreaElement) return true;
|
|
22
|
+
if (target instanceof HTMLInputElement) {
|
|
23
|
+
return TEXT_INPUT_TYPES.has(target.type);
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function matches(event, binding) {
|
|
28
|
+
const ctrlHeld = event.ctrlKey || event.metaKey;
|
|
29
|
+
if (ctrlHeld !== Boolean(binding.ctrl)) return false;
|
|
30
|
+
if (event.shiftKey !== Boolean(binding.shift)) return false;
|
|
31
|
+
if (event.altKey !== Boolean(binding.alt)) return false;
|
|
32
|
+
return event.key.toLowerCase() === binding.key.toLowerCase() || binding.code !== void 0 && event.code === binding.code;
|
|
33
|
+
}
|
|
34
|
+
function useHotkeys(bindings) {
|
|
35
|
+
function onKeydown(event) {
|
|
36
|
+
if (event.isComposing) return;
|
|
37
|
+
const inTextEntry = isTextEntry(event.target);
|
|
38
|
+
for (const binding of toValue(bindings)) {
|
|
39
|
+
if (inTextEntry && !binding.allowInInput) continue;
|
|
40
|
+
if (!matches(event, binding)) continue;
|
|
41
|
+
if (binding.handler() === false) continue;
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
onMounted(() => window.addEventListener("keydown", onKeydown));
|
|
47
|
+
onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
appleModifierLabel,
|
|
51
|
+
isAppleKeyboard,
|
|
52
|
+
useHotkeys
|
|
53
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export { default as LpSelect } from './components/LpSelect.vue';
|
|
|
67
67
|
export type { SelectOption, SelectValue } from './components/LpSelect.vue';
|
|
68
68
|
export { default as LpServiceNode } from './components/LpServiceNode.vue';
|
|
69
69
|
export type { ServiceNodeData } from './components/LpServiceNode.vue';
|
|
70
|
+
export { default as LpShortcutsDialog } from './components/LpShortcutsDialog.vue';
|
|
71
|
+
export type { ShortcutEntry, ShortcutGroup } from './components/LpShortcutsDialog.vue';
|
|
70
72
|
export { default as LpSidebar } from './components/LpSidebar.vue';
|
|
71
73
|
export type { SidebarItem, SidebarSection } from './components/LpSidebar.vue';
|
|
72
74
|
export { default as LpSkeleton } from './components/LpSkeleton.vue';
|
|
@@ -107,6 +109,8 @@ export { amber, dark, leavepulse, light, lime, nord, presets, rose, violet } fro
|
|
|
107
109
|
export type { PresetName } from './theme/presets';
|
|
108
110
|
export { parseConfig, serializeConfig, UI_CONFIG_VERSION } from './theme/config';
|
|
109
111
|
export type { UiConfig } from './theme/config';
|
|
112
|
+
export { useHotkeys, isAppleKeyboard, appleModifierLabel } from './composables/useHotkeys';
|
|
113
|
+
export type { HotkeyBinding } from './composables/useHotkeys';
|
|
110
114
|
export { useInputFilter } from './composables/useInputFilter';
|
|
111
115
|
export type { UseInputFilterOptions } from './composables/useInputFilter';
|
|
112
116
|
export { useSettled } from './composables/useSettled';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { _ } from "./chunks/LayoutCanvas-Dd20boUG.js";
|
|
2
|
-
import { _ as _2 } from "./chunks/LpAddressInput-
|
|
2
|
+
import { _ as _2 } from "./chunks/LpAddressInput-B411MOJa.js";
|
|
3
3
|
import { _ as _3 } from "./chunks/LpMapPicker-DFKrd1Ne.js";
|
|
4
4
|
import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
|
|
5
5
|
import { _ as _4 } from "./chunks/LpAlert-BAYHDSS1.js";
|
|
6
|
-
import { _ as _5 } from "./chunks/LpAppShell-
|
|
6
|
+
import { _ as _5 } from "./chunks/LpAppShell-CfM7fdr3.js";
|
|
7
7
|
import { _ as _6 } from "./chunks/LpAutocomplete-DIgXmtCA.js";
|
|
8
8
|
import { _ as _7 } from "./chunks/LpAvatar-B1Ez0ZAx.js";
|
|
9
9
|
import { _ as _8 } from "./chunks/LpBadge-CXzBPnwO.js";
|
|
@@ -14,15 +14,15 @@ import { _ as _12 } from "./chunks/LpCard-BmrTvWOe.js";
|
|
|
14
14
|
import { _ as _13 } from "./chunks/LpCheckbox-BO0zuuPh.js";
|
|
15
15
|
import { _ as _14 } from "./chunks/LpCodeBlock-CSiOSPs8.js";
|
|
16
16
|
import { default as default2 } from "./components/LpCommandPalette.vue.js";
|
|
17
|
-
import { _ as _15 } from "./chunks/LpConfirmDialog-
|
|
17
|
+
import { _ as _15 } from "./chunks/LpConfirmDialog-D8pBGPOI.js";
|
|
18
18
|
import { _ as _16 } from "./chunks/LpContextMenu-D0mzXqA7.js";
|
|
19
19
|
import { _ as _17 } from "./chunks/LpDatePicker-dY2q4dr4.js";
|
|
20
20
|
import { _ as _18 } from "./chunks/LpDisclosure-DP6yfgfM.js";
|
|
21
21
|
import { _ as _19 } from "./chunks/LpDivider-CUX46KR0.js";
|
|
22
|
-
import { _ as _20 } from "./chunks/LpDrawer-
|
|
22
|
+
import { _ as _20 } from "./chunks/LpDrawer-B4cNj39a.js";
|
|
23
23
|
import { _ as _21 } from "./chunks/LpDropdownMenu-DJpoqRCb.js";
|
|
24
24
|
import { _ as _22 } from "./chunks/LpEmptyState-CrfyRJUG.js";
|
|
25
|
-
import { _ as _23 } from "./chunks/LpFileTree-
|
|
25
|
+
import { _ as _23 } from "./chunks/LpFileTree-d9nXtSVo.js";
|
|
26
26
|
import { ancestorIds, checkStateOf, checkedCount, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
|
|
27
27
|
import { _ as _24 } from "./chunks/LpFormField-Bn-ZwC6z.js";
|
|
28
28
|
import { _ as _25 } from "./chunks/LpIcon-CCnX5_2j.js";
|
|
@@ -31,9 +31,9 @@ import { _ as _27 } from "./chunks/LpInput-BauZRyzm.js";
|
|
|
31
31
|
import { _ as _28 } from "./chunks/LpLightbox-Ci35Y_xP.js";
|
|
32
32
|
import { fileNameOf } from "./components/lightbox.js";
|
|
33
33
|
import { _ as _29 } from "./chunks/LpLink-DHwOEp4e.js";
|
|
34
|
-
import { _ as _30 } from "./chunks/LpLogViewer-
|
|
35
|
-
import { _ as _31 } from "./chunks/LpModal-
|
|
36
|
-
import { _ as _32 } from "./chunks/LpNotificationBell-
|
|
34
|
+
import { _ as _30 } from "./chunks/LpLogViewer-BXPBUV21.js";
|
|
35
|
+
import { _ as _31 } from "./chunks/LpModal-DKFdzKwV.js";
|
|
36
|
+
import { _ as _32 } from "./chunks/LpNotificationBell-Nges4kJk.js";
|
|
37
37
|
import { _ as _33 } from "./chunks/LpNumberField-C5ar-OwE.js";
|
|
38
38
|
import { _ as _34 } from "./chunks/LpOtpInput-DqaT0Z75.js";
|
|
39
39
|
import { _ as _35 } from "./chunks/LpPagination-CeGuNe_q.js";
|
|
@@ -44,17 +44,18 @@ import { _ as _38 } from "./chunks/LpPopover-8268UmlC.js";
|
|
|
44
44
|
import { _ as _39 } from "./chunks/LpProgress-BhI0IbjC.js";
|
|
45
45
|
import { _ as _40 } from "./chunks/LpRadio-CL-pI0_O.js";
|
|
46
46
|
import { _ as _41 } from "./chunks/LpRadioGroup-B4yDTmi6.js";
|
|
47
|
-
import { _ as _42 } from "./chunks/LpScrollArea-
|
|
47
|
+
import { _ as _42 } from "./chunks/LpScrollArea-Du8tW8c5.js";
|
|
48
48
|
import { _ as _43 } from "./chunks/LpSegmented-BmpanyAo.js";
|
|
49
49
|
import { _ as _44 } from "./chunks/LpSelect-D5mU4cZO.js";
|
|
50
50
|
import { _ as _45 } from "./chunks/LpServiceNode-Dc-Ut5RS.js";
|
|
51
|
-
import {
|
|
51
|
+
import { default as default3 } from "./components/LpShortcutsDialog.vue.js";
|
|
52
|
+
import { _ as _46 } from "./chunks/LpSidebar-Bd0148dd.js";
|
|
52
53
|
import { _ as _47 } from "./chunks/LpSkeleton-eSFIo4kD.js";
|
|
53
54
|
import { _ as _48 } from "./chunks/LpSlider-B1SF3pRV.js";
|
|
54
55
|
import { _ as _49 } from "./chunks/LpStat-CdbEFOvn.js";
|
|
55
56
|
import { _ as _50 } from "./chunks/LpStepper-5dFTrW21.js";
|
|
56
57
|
import { _ as _51 } from "./chunks/LpSwitch-v6fnccSM.js";
|
|
57
|
-
import { _ as _52 } from "./chunks/LpTable-
|
|
58
|
+
import { _ as _52 } from "./chunks/LpTable-BBKlNR9j.js";
|
|
58
59
|
import { _ as _53 } from "./chunks/LpTableOfContents-CjvxPMpU.js";
|
|
59
60
|
import { _ as _54 } from "./chunks/LpTabs-Cefe_lys.js";
|
|
60
61
|
import { _ as _55 } from "./chunks/LpTextarea-CRAkhemZ.js";
|
|
@@ -69,6 +70,7 @@ import { deserializeLayout, serializeLayout } from "./layout/tree.js";
|
|
|
69
70
|
import { addLeaf, addTab, countBlocks, countLeaves, makeLayout, moveLeaf, removeBlock, removeLeaf, reorderTab, resizeAt, setActiveTab } from "./layout/tree.js";
|
|
70
71
|
import { blockTitle, defineBlocks } from "./layout/registry.js";
|
|
71
72
|
import { useLayout } from "./layout/useLayout.js";
|
|
73
|
+
import { appleModifierLabel, isAppleKeyboard, useHotkeys } from "./composables/useHotkeys.js";
|
|
72
74
|
import { useInputFilter } from "./composables/useInputFilter.js";
|
|
73
75
|
import { useSettled } from "./composables/useSettled.js";
|
|
74
76
|
import { useTilt } from "./composables/useTilt.js";
|
|
@@ -152,6 +154,7 @@ export {
|
|
|
152
154
|
_43 as LpSegmented,
|
|
153
155
|
_44 as LpSelect,
|
|
154
156
|
_45 as LpServiceNode,
|
|
157
|
+
default3 as LpShortcutsDialog,
|
|
155
158
|
_46 as LpSidebar,
|
|
156
159
|
_47 as LpSkeleton,
|
|
157
160
|
_48 as LpSlider,
|
|
@@ -176,6 +179,7 @@ export {
|
|
|
176
179
|
addTab,
|
|
177
180
|
b as amber,
|
|
178
181
|
ancestorIds,
|
|
182
|
+
appleModifierLabel,
|
|
179
183
|
c as applyTheme,
|
|
180
184
|
d as applyThemeWithTransition,
|
|
181
185
|
blockTitle,
|
|
@@ -198,6 +202,7 @@ export {
|
|
|
198
202
|
formatAddress,
|
|
199
203
|
formatModified,
|
|
200
204
|
formatSize,
|
|
205
|
+
isAppleKeyboard,
|
|
201
206
|
i as isBrokenBackdropEngine,
|
|
202
207
|
l as leavepulse,
|
|
203
208
|
h as light,
|
|
@@ -228,6 +233,7 @@ export {
|
|
|
228
233
|
m as themeToCssVars,
|
|
229
234
|
tileUrl,
|
|
230
235
|
unproject,
|
|
236
|
+
useHotkeys,
|
|
231
237
|
useInputFilter,
|
|
232
238
|
useLayout,
|
|
233
239
|
useSettled,
|
package/package.json
CHANGED
package/src/component-names.ts
CHANGED
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
DialogRoot,
|
|
19
19
|
DialogTitle,
|
|
20
20
|
} from "reka-ui"
|
|
21
|
-
import { computed, nextTick,
|
|
21
|
+
import { computed, nextTick, ref, watch } from "vue"
|
|
22
22
|
import LpIcon from "./LpIcon.vue"
|
|
23
23
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
24
|
+
import { useHotkeys } from "../composables/useHotkeys"
|
|
24
25
|
|
|
25
26
|
export interface Command {
|
|
26
27
|
id: string
|
|
@@ -196,16 +197,22 @@ watch(
|
|
|
196
197
|
},
|
|
197
198
|
)
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
200
|
+
// Through useHotkeys rather than a listener of its own: the palette is the one
|
|
201
|
+
// shortcut that has to work from inside a text field, and `code` is what makes
|
|
202
|
+
// it fire on a layout where Ctrl-K arrives as "л".
|
|
203
|
+
useHotkeys(() =>
|
|
204
|
+
props.hotkey
|
|
205
|
+
? [
|
|
206
|
+
{
|
|
207
|
+
key: "k",
|
|
208
|
+
code: "KeyK",
|
|
209
|
+
ctrl: true,
|
|
210
|
+
allowInInput: true,
|
|
211
|
+
handler: () => emit("update:open", !props.open),
|
|
212
|
+
},
|
|
213
|
+
]
|
|
214
|
+
: [],
|
|
215
|
+
)
|
|
209
216
|
</script>
|
|
210
217
|
|
|
211
218
|
<template>
|
|
@@ -184,10 +184,15 @@ function onThumbPointerDown() {
|
|
|
184
184
|
first hover — so before the pointer ever entered, the region silently
|
|
185
185
|
refused to scroll (wheel included). Mounting it up front fixes that;
|
|
186
186
|
the bar still only becomes visible on hover, via `barFade` below. -->
|
|
187
|
+
<!-- z-1: reka positions the bar absolutely but leaves it on `z-index: auto`,
|
|
188
|
+
and it sits BEFORE the viewport in the DOM — so any positioned row in
|
|
189
|
+
the content painted over it, and a row with an opaque hover background
|
|
190
|
+
swallowed the thumb wherever the pointer went. The bar only has to
|
|
191
|
+
clear its own content, not the page, so a local step is enough. -->
|
|
187
192
|
<ScrollAreaScrollbar
|
|
188
193
|
orientation="vertical"
|
|
189
194
|
force-mount
|
|
190
|
-
class="group flex w-3.5 touch-none select-none justify-end py-px pl-2.5 pr-px"
|
|
195
|
+
class="group z-1 flex w-3.5 touch-none select-none justify-end py-px pl-2.5 pr-px"
|
|
191
196
|
:class="barFade"
|
|
192
197
|
:style="barInsetTop ? { marginTop: barInsetTop } : undefined"
|
|
193
198
|
>
|
|
@@ -205,7 +210,7 @@ function onThumbPointerDown() {
|
|
|
205
210
|
|
|
206
211
|
<ScrollAreaScrollbar
|
|
207
212
|
orientation="horizontal"
|
|
208
|
-
class="group flex h-3.5 flex-col touch-none select-none justify-end px-px pb-px pt-2.5"
|
|
213
|
+
class="group z-1 flex h-3.5 flex-col touch-none select-none justify-end px-px pb-px pt-2.5"
|
|
209
214
|
:class="barFade"
|
|
210
215
|
>
|
|
211
216
|
<ScrollAreaThumb
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* A keyboard reference, conventionally opened with F1.
|
|
4
|
+
*
|
|
5
|
+
* Shortcuts are passed in as data rather than read off a live binding list: a
|
|
6
|
+
* binding knows its key but not what to call it, and in most apps half of them
|
|
7
|
+
* are only mounted in the section they belong to — a reference that lists what
|
|
8
|
+
* happens to be active is not a reference.
|
|
9
|
+
*
|
|
10
|
+
* Combos are written the way they read ("Ctrl+Shift+M", "←→↑↓") and split on
|
|
11
|
+
* "+" into one <kbd> each.
|
|
12
|
+
*/
|
|
13
|
+
import LpModal from "./LpModal.vue"
|
|
14
|
+
import { appleModifierLabel } from "../composables/useHotkeys"
|
|
15
|
+
|
|
16
|
+
export interface ShortcutEntry {
|
|
17
|
+
/** Rendered one <kbd> per "+"-separated key, so write it the way it reads. */
|
|
18
|
+
combo: string
|
|
19
|
+
label: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ShortcutGroup {
|
|
23
|
+
title: string
|
|
24
|
+
entries: ShortcutEntry[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(
|
|
28
|
+
defineProps<{
|
|
29
|
+
open?: boolean
|
|
30
|
+
groups: ShortcutGroup[]
|
|
31
|
+
title?: string
|
|
32
|
+
description?: string
|
|
33
|
+
}>(),
|
|
34
|
+
{ open: false, title: "Keyboard shortcuts" },
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<LpModal
|
|
42
|
+
:open="props.open"
|
|
43
|
+
size="lg"
|
|
44
|
+
:title="props.title"
|
|
45
|
+
:description="props.description"
|
|
46
|
+
@update:open="(value: boolean) => emit('update:open', value)"
|
|
47
|
+
>
|
|
48
|
+
<div class="shortcut-groups">
|
|
49
|
+
<section v-for="group in props.groups" :key="group.title" class="shortcut-group">
|
|
50
|
+
<h3>{{ group.title }}</h3>
|
|
51
|
+
<dl>
|
|
52
|
+
<div v-for="entry in group.entries" :key="entry.combo" class="shortcut-row">
|
|
53
|
+
<dt>{{ entry.label }}</dt>
|
|
54
|
+
<dd>
|
|
55
|
+
<kbd v-for="(key, index) in entry.combo.split('+')" :key="`${key}-${index}`">
|
|
56
|
+
{{ appleModifierLabel(key.trim()) }}
|
|
57
|
+
</kbd>
|
|
58
|
+
</dd>
|
|
59
|
+
</div>
|
|
60
|
+
</dl>
|
|
61
|
+
</section>
|
|
62
|
+
</div>
|
|
63
|
+
</LpModal>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
/* Two columns where there is room: the list is long enough that one column
|
|
68
|
+
pushes the last group off a laptop screen. */
|
|
69
|
+
.shortcut-groups {
|
|
70
|
+
display: grid;
|
|
71
|
+
gap: 18px;
|
|
72
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.shortcut-group h3 {
|
|
76
|
+
margin: 0 0 6px;
|
|
77
|
+
color: var(--color-muted);
|
|
78
|
+
font-size: 0.7rem;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
letter-spacing: 0.06em;
|
|
81
|
+
text-transform: uppercase;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.shortcut-group dl {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: 1px;
|
|
88
|
+
margin: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.shortcut-row {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
gap: 12px;
|
|
96
|
+
padding: 5px 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.shortcut-row dt {
|
|
100
|
+
color: var(--color-ink);
|
|
101
|
+
font-size: 0.82rem;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.shortcut-row dd {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
gap: 3px;
|
|
109
|
+
margin: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
kbd {
|
|
113
|
+
display: inline-flex;
|
|
114
|
+
min-width: 20px;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
padding: 2px 6px;
|
|
118
|
+
border: 1px solid var(--color-line-strong);
|
|
119
|
+
border-bottom-width: 2px;
|
|
120
|
+
border-radius: 5px;
|
|
121
|
+
background: color-mix(in srgb, var(--color-ink) 6%, transparent);
|
|
122
|
+
color: var(--color-muted-strong);
|
|
123
|
+
font-family: inherit;
|
|
124
|
+
font-size: 0.7rem;
|
|
125
|
+
line-height: 1.5;
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted, type MaybeRefOrGetter, toValue } from "vue"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* One keydown listener over a registry of bindings, so a shortcut is declared
|
|
5
|
+
* next to the action it runs instead of growing another window-level handler.
|
|
6
|
+
*
|
|
7
|
+
* Three things every hand-rolled shortcut handler tends to get wrong, and the
|
|
8
|
+
* reason this is worth sharing rather than rewriting per app:
|
|
9
|
+
*
|
|
10
|
+
* layout — a binding names its key twice over. `key` is what the layout
|
|
11
|
+
* produced, `code` is the physical position, and either matching counts. On
|
|
12
|
+
* a Cyrillic layout Ctrl+K arrives as `key: "л"` with `code: "KeyK"`, and a
|
|
13
|
+
* user pressing the key printed "K" expects it to work either way.
|
|
14
|
+
*
|
|
15
|
+
* IME — a composing input delivers every keystroke as its own event, so
|
|
16
|
+
* acting on them fires shortcuts mid-word.
|
|
17
|
+
*
|
|
18
|
+
* text entry — a shortcut that steals a key from someone typing is a bug,
|
|
19
|
+
* except for the few that mean something in a field (Escape, a palette).
|
|
20
|
+
*
|
|
21
|
+
* Modifier note: Ctrl and Meta are one modifier here, since macOS binds ⌘ where
|
|
22
|
+
* the rest bind Ctrl. Label the difference in the UI, not in the binding.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface HotkeyBinding {
|
|
26
|
+
/** Layout-produced key, compared case-insensitively ("l", "f2", "escape"). */
|
|
27
|
+
key: string
|
|
28
|
+
/**
|
|
29
|
+
* Physical key position ("KeyL", "F2", "Digit1"). Matches on its own, which
|
|
30
|
+
* is what makes non-Latin layouts work.
|
|
31
|
+
*/
|
|
32
|
+
code?: string
|
|
33
|
+
/** Ctrl on Windows/Linux, ⌘ on macOS — the two are treated as one. */
|
|
34
|
+
ctrl?: boolean
|
|
35
|
+
shift?: boolean
|
|
36
|
+
alt?: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Runs even while a text field has focus. Only for shortcuts that mean
|
|
39
|
+
* something there — Escape, and the palette the user reaches from anywhere.
|
|
40
|
+
*/
|
|
41
|
+
allowInInput?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* Returning `false` declines the key: the event keeps its default and the
|
|
44
|
+
* search moves on. This is what lets a binding defer to a layer that owns the
|
|
45
|
+
* key better than it does — Escape belongs to whichever overlay is on top,
|
|
46
|
+
* and swallowing it here would close all of them.
|
|
47
|
+
*/
|
|
48
|
+
handler: () => void | boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether this machine prints ⌘ where the rest print Ctrl. Read once: the
|
|
53
|
+
* keyboard does not change under a running window, and the check is only ever
|
|
54
|
+
* used to pick a label.
|
|
55
|
+
*/
|
|
56
|
+
export const isAppleKeyboard =
|
|
57
|
+
typeof navigator !== "undefined"
|
|
58
|
+
&& /Mac|iPhone|iPad|iPod/.test(navigator.platform || "")
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The symbol to print for a modifier on this platform. Bindings treat Ctrl and
|
|
62
|
+
* Meta as one modifier, so only the label may differ — never the binding.
|
|
63
|
+
*/
|
|
64
|
+
export function appleModifierLabel(key: string): string {
|
|
65
|
+
if (!isAppleKeyboard) return key
|
|
66
|
+
if (key === "Ctrl") return "⌘"
|
|
67
|
+
if (key === "Alt") return "⌥"
|
|
68
|
+
return key
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const TEXT_INPUT_TYPES = new Set([
|
|
72
|
+
"text",
|
|
73
|
+
"search",
|
|
74
|
+
"email",
|
|
75
|
+
"number",
|
|
76
|
+
"password",
|
|
77
|
+
"tel",
|
|
78
|
+
"url",
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether the event lands in something the user is typing into. Checked off the
|
|
83
|
+
* event target rather than `document.activeElement` so it stays right inside a
|
|
84
|
+
* dialog's focus trap.
|
|
85
|
+
*/
|
|
86
|
+
function isTextEntry(target: EventTarget | null): boolean {
|
|
87
|
+
if (!(target instanceof HTMLElement)) return false
|
|
88
|
+
if (target.isContentEditable) return true
|
|
89
|
+
if (target instanceof HTMLTextAreaElement) return true
|
|
90
|
+
if (target instanceof HTMLInputElement) {
|
|
91
|
+
return TEXT_INPUT_TYPES.has(target.type)
|
|
92
|
+
}
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function matches(event: KeyboardEvent, binding: HotkeyBinding): boolean {
|
|
97
|
+
const ctrlHeld = event.ctrlKey || event.metaKey
|
|
98
|
+
if (ctrlHeld !== Boolean(binding.ctrl)) return false
|
|
99
|
+
if (event.shiftKey !== Boolean(binding.shift)) return false
|
|
100
|
+
if (event.altKey !== Boolean(binding.alt)) return false
|
|
101
|
+
return (
|
|
102
|
+
event.key.toLowerCase() === binding.key.toLowerCase()
|
|
103
|
+
|| (binding.code !== undefined && event.code === binding.code)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Bind a list of hotkeys for as long as the calling component is mounted.
|
|
109
|
+
*
|
|
110
|
+
* The list is read on every keystroke rather than captured once, so bindings
|
|
111
|
+
* may come and go with the state that owns them — a getter is the point, not a
|
|
112
|
+
* formality.
|
|
113
|
+
*/
|
|
114
|
+
export function useHotkeys(bindings: MaybeRefOrGetter<HotkeyBinding[]>) {
|
|
115
|
+
function onKeydown(event: KeyboardEvent) {
|
|
116
|
+
if (event.isComposing) return
|
|
117
|
+
const inTextEntry = isTextEntry(event.target)
|
|
118
|
+
for (const binding of toValue(bindings)) {
|
|
119
|
+
if (inTextEntry && !binding.allowInInput) continue
|
|
120
|
+
if (!matches(event, binding)) continue
|
|
121
|
+
// Claimed only once the handler has run and not declined, so a binding
|
|
122
|
+
// that stands down leaves the key untouched for whoever handles it next.
|
|
123
|
+
if (binding.handler() === false) continue
|
|
124
|
+
event.preventDefault()
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onMounted(() => window.addEventListener("keydown", onKeydown))
|
|
130
|
+
onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown))
|
|
131
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -99,6 +99,8 @@ export { default as LpSelect } from "./components/LpSelect.vue"
|
|
|
99
99
|
export type { SelectOption, SelectValue } from "./components/LpSelect.vue"
|
|
100
100
|
export { default as LpServiceNode } from "./components/LpServiceNode.vue"
|
|
101
101
|
export type { ServiceNodeData } from "./components/LpServiceNode.vue"
|
|
102
|
+
export { default as LpShortcutsDialog } from "./components/LpShortcutsDialog.vue"
|
|
103
|
+
export type { ShortcutEntry, ShortcutGroup } from "./components/LpShortcutsDialog.vue"
|
|
102
104
|
export { default as LpSidebar } from "./components/LpSidebar.vue"
|
|
103
105
|
export type { SidebarItem, SidebarSection } from "./components/LpSidebar.vue"
|
|
104
106
|
export { default as LpSkeleton } from "./components/LpSkeleton.vue"
|
|
@@ -182,6 +184,8 @@ export type { PresetName } from "./theme/presets"
|
|
|
182
184
|
export { parseConfig, serializeConfig, UI_CONFIG_VERSION } from "./theme/config"
|
|
183
185
|
export type { UiConfig } from "./theme/config"
|
|
184
186
|
|
|
187
|
+
export { useHotkeys, isAppleKeyboard, appleModifierLabel } from "./composables/useHotkeys"
|
|
188
|
+
export type { HotkeyBinding } from "./composables/useHotkeys"
|
|
185
189
|
export { useInputFilter } from "./composables/useInputFilter"
|
|
186
190
|
export type { UseInputFilterOptions } from "./composables/useInputFilter"
|
|
187
191
|
export { useSettled } from "./composables/useSettled"
|