@leavepulse/ui 0.45.1 → 0.46.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-HzAA4gri.css → LpCommandPalette-5miKYsb-.css} +21 -21
- package/dist/assets/{LpDropzone-i4BdMi1H.css → LpDropzone-Ev6X03zj.css} +12 -12
- package/dist/assets/{LpTable-CPwWxTJS.css → LpTable-BoeA_TgK.css} +17 -17
- package/dist/chunks/{LpAddressInput-CJOYGZlI.js → LpAddressInput-iOJhmRzy.js} +1 -1
- package/dist/chunks/{LpAppShell-B07Lf48e.js → LpAppShell-BmMYcEVx.js} +2 -2
- package/dist/chunks/{LpAvatar-C1XStdOo.js → LpAvatar-DeSb6H0c.js} +1 -1
- package/dist/chunks/{LpCard-C2IvucmX.js → LpCard-BdxkMXjo.js} +1 -1
- package/dist/chunks/{LpColorPicker-zz4VGkG4.js → LpColorPicker-BLqPX5Vw.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-BTVAR4tF.js → LpConfirmDialog-Ca-CTlxT.js} +1 -1
- package/dist/chunks/{LpConfirmHost-Ci_uCHgQ.js → LpConfirmHost-BAaSaT2b.js} +1 -1
- package/dist/chunks/{LpContextMenu-BqkAYa9X.js → LpContextMenu-1cKew_Dc.js} +17 -4
- package/dist/chunks/{LpDrawer-CfRhHwmE.js → LpDrawer-CBFVD624.js} +1 -1
- package/dist/chunks/{LpDropdownMenu-DdJGQzPn.js → LpDropdownMenu-DrpJJBp0.js} +22 -8
- package/dist/chunks/{LpFileTree-CNurOvkV.js → LpFileTree-bk5VjBey.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-D_qRRdsH.js → LpFileTreeNode-BvMSBM7X.js} +1 -1
- package/dist/chunks/{LpLogViewer-KypbgOyo.js → LpLogViewer-BDFXzvcl.js} +2 -2
- package/dist/chunks/{LpModal-BzTVeNqe.js → LpModal-JNa3B_Lr.js} +12 -5
- package/dist/chunks/{LpModalNav-CjABSGCu.js → LpModalNav-BAxC9et7.js} +10 -3
- package/dist/chunks/{LpNotificationBell-CCwyJP5C.js → LpNotificationBell-DsqSyMwS.js} +2 -2
- package/dist/chunks/{LpPie-BEJ2SfI6.js → LpPie-BdSnta11.js} +1 -1
- package/dist/chunks/{LpScrollArea-Bry1fdOM.js → LpScrollArea-Hz3-JWUG.js} +23 -5
- package/dist/chunks/{LpSidebar-Bwws_X29.js → LpSidebar-DpEiGi8L.js} +2 -2
- package/dist/chunks/{LpSidebarNav-kx-LdqNU.js → LpSidebarNav-BUuxipX-.js} +2 -2
- package/dist/chunks/{LpThemeSwitcher-B4aVPbNC.js → LpThemeSwitcher-BbYFFT5w.js} +1 -1
- package/dist/chunks/MenuTitle-J3ZDYjpV.js +56 -0
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +6 -3
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.d.ts +11 -0
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.d.ts +6 -0
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.d.ts +14 -0
- package/dist/components/LpDropzone.vue.js +9 -7
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +7 -5
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.d.ts +5 -3
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +2 -2
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpTable.vue.js +5 -5
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/MenuTitle.vue.d.ts +9 -0
- package/dist/components/MenuTitle.vue.js +4 -0
- package/dist/composables/useClipboard.js +35 -2
- package/dist/index.js +19 -19
- package/package.json +1 -1
- package/src/components/LpCommandPalette.vue +6 -0
- package/src/components/LpContextMenu.vue +19 -1
- package/src/components/LpDropdownMenu.vue +14 -2
- package/src/components/LpDropzone.vue +13 -5
- package/src/components/LpModal.vue +7 -1
- package/src/components/LpModalNav.vue +4 -0
- package/src/components/LpScrollArea.vue +30 -2
- package/src/components/LpTable.vue +1 -1
- package/src/components/MenuTitle.vue +34 -0
- package/src/composables/useClipboard.ts +49 -7
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { ref, onScopeDispose } from "vue";
|
|
2
2
|
import { useToast } from "./useToast.js";
|
|
3
|
+
async function writeAsync(text) {
|
|
4
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) return false;
|
|
5
|
+
try {
|
|
6
|
+
await navigator.clipboard.writeText(text);
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function writeLegacy(text) {
|
|
13
|
+
if (typeof document === "undefined") return false;
|
|
14
|
+
const active = document.activeElement;
|
|
15
|
+
const selection = document.getSelection();
|
|
16
|
+
const ranges = selection ? Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i)) : [];
|
|
17
|
+
const field = document.createElement("textarea");
|
|
18
|
+
field.value = text;
|
|
19
|
+
field.setAttribute("readonly", "");
|
|
20
|
+
field.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none";
|
|
21
|
+
document.body.appendChild(field);
|
|
22
|
+
field.select();
|
|
23
|
+
let ok = false;
|
|
24
|
+
try {
|
|
25
|
+
ok = document.execCommand("copy");
|
|
26
|
+
} catch {
|
|
27
|
+
ok = false;
|
|
28
|
+
}
|
|
29
|
+
field.remove();
|
|
30
|
+
if (selection) {
|
|
31
|
+
selection.removeAllRanges();
|
|
32
|
+
for (const range of ranges) selection.addRange(range);
|
|
33
|
+
}
|
|
34
|
+
active?.focus?.({ preventScroll: true });
|
|
35
|
+
return ok;
|
|
36
|
+
}
|
|
3
37
|
const COPIED_MS = 1400;
|
|
4
38
|
function useClipboard() {
|
|
5
39
|
const copied = ref(false);
|
|
@@ -8,8 +42,7 @@ function useClipboard() {
|
|
|
8
42
|
async function copy(text, options = {}) {
|
|
9
43
|
const { toast = false, errorToast = false } = options;
|
|
10
44
|
try {
|
|
11
|
-
await
|
|
12
|
-
if (!navigator.clipboard) throw new Error("clipboard unavailable");
|
|
45
|
+
if (!await writeAsync(text) && !writeLegacy(text)) throw new Error("clipboard unavailable");
|
|
13
46
|
copied.value = true;
|
|
14
47
|
clearTimeout(timer);
|
|
15
48
|
timer = setTimeout(() => copied.value = false, COPIED_MS);
|
package/dist/index.js
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
1
|
import { _ } from "./chunks/LayoutCanvas-BkaGXTjx.js";
|
|
2
|
-
import { _ as _2 } from "./chunks/LpAddressInput-
|
|
2
|
+
import { _ as _2 } from "./chunks/LpAddressInput-iOJhmRzy.js";
|
|
3
3
|
import { _ as _3 } from "./chunks/LpMapPicker-Cc84LaM3.js";
|
|
4
4
|
import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
|
|
5
5
|
import { _ as _4 } from "./chunks/LpActionBar-CnfFVIwC.js";
|
|
6
6
|
import { _ as _5 } from "./chunks/LpAlert-B6ilrM7T.js";
|
|
7
|
-
import { _ as _6 } from "./chunks/LpAppShell-
|
|
7
|
+
import { _ as _6 } from "./chunks/LpAppShell-BmMYcEVx.js";
|
|
8
8
|
import { _ as _7 } from "./chunks/LpAutocomplete-C8IgSMsi.js";
|
|
9
|
-
import { _ as _8 } from "./chunks/LpAvatar-
|
|
9
|
+
import { _ as _8 } from "./chunks/LpAvatar-DeSb6H0c.js";
|
|
10
10
|
import { _ as _9 } from "./chunks/LpBadge-Fe0D1bYx.js";
|
|
11
11
|
import { _ as _10 } from "./chunks/LpBreadcrumbs-CQJd73Gm.js";
|
|
12
12
|
import { _ as _11 } from "./chunks/LpButton-CKSwC6Mu.js";
|
|
13
13
|
import { _ as _12 } from "./chunks/LpCalendar-DrA2McTU.js";
|
|
14
|
-
import { _ as _13 } from "./chunks/LpCard-
|
|
14
|
+
import { _ as _13 } from "./chunks/LpCard-BdxkMXjo.js";
|
|
15
15
|
import { default as default2 } from "./components/LpChart.vue.js";
|
|
16
16
|
import { backgroundOf, csvField, downloadFile, fileStem, inlineSvg, resolveCssColor, svgToPng, toCsv } from "./components/exportData.js";
|
|
17
17
|
import { CHART_PALETTE, axisTicks, chartSummary, formatTick, markerIndex, niceScale, resolveSlices } from "./components/chart.js";
|
|
18
18
|
import { _ as _14 } from "./chunks/LpCheckbox-Bkj13CbW.js";
|
|
19
19
|
import { _ as _15 } from "./chunks/LpCodeBlock-ca7ANFyW.js";
|
|
20
20
|
import { tokenizeLines } from "./components/codeHighlight.js";
|
|
21
|
-
import { _ as _16 } from "./chunks/LpColorPicker-
|
|
21
|
+
import { _ as _16 } from "./chunks/LpColorPicker-BLqPX5Vw.js";
|
|
22
22
|
import { DEFAULT_SWATCHES, clampHsv, formatColor, hsvToHsl, hsvToRgb, isLight, normalizeHue, opaqueCss, parseColor, parseHex, rgbToHex, rgbToHsv } from "./components/color.js";
|
|
23
23
|
import { default as default3 } from "./components/LpCommandPalette.vue.js";
|
|
24
24
|
import { parseCommandQuery, rankCommandItems } from "./components/commandSearch.js";
|
|
25
|
-
import { _ as _17 } from "./chunks/LpConfirmDialog-
|
|
26
|
-
import { _ as _18 } from "./chunks/LpConfirmHost-
|
|
25
|
+
import { _ as _17 } from "./chunks/LpConfirmDialog-Ca-CTlxT.js";
|
|
26
|
+
import { _ as _18 } from "./chunks/LpConfirmHost-BAaSaT2b.js";
|
|
27
27
|
import { confirm, useConfirm } from "./components/confirm.js";
|
|
28
|
-
import { _ as _19 } from "./chunks/LpContextMenu-
|
|
28
|
+
import { _ as _19 } from "./chunks/LpContextMenu-1cKew_Dc.js";
|
|
29
29
|
import { _ as _20 } from "./chunks/LpDatePicker-BrdFtSTy.js";
|
|
30
30
|
import { _ as _21 } from "./chunks/LpDateRangePicker-Drd7EbIz.js";
|
|
31
31
|
import { _ as _22 } from "./chunks/LpRangeCalendar-DZuaRV0W.js";
|
|
32
32
|
import { clampRange, defaultRangePresets, formatRange, normalizeRange, sameRange } from "./components/dateRange.js";
|
|
33
33
|
import { _ as _23 } from "./chunks/LpDisclosure-Cc23-JOT.js";
|
|
34
34
|
import { _ as _24 } from "./chunks/LpDivider-CUX46KR0.js";
|
|
35
|
-
import { _ as _25 } from "./chunks/LpDrawer-
|
|
36
|
-
import { _ as _26 } from "./chunks/LpDropdownMenu-
|
|
35
|
+
import { _ as _25 } from "./chunks/LpDrawer-CBFVD624.js";
|
|
36
|
+
import { _ as _26 } from "./chunks/LpDropdownMenu-DrpJJBp0.js";
|
|
37
37
|
import { default as default4 } from "./components/LpDropzone.vue.js";
|
|
38
38
|
import { _ as _27 } from "./chunks/LpEmptyState-BJbZXC8D.js";
|
|
39
|
-
import { _ as _28 } from "./chunks/LpFileTree-
|
|
39
|
+
import { _ as _28 } from "./chunks/LpFileTree-bk5VjBey.js";
|
|
40
40
|
import { ancestorIds, checkStateOf, checkedCount, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
|
|
41
41
|
import { _ as _29 } from "./chunks/LpFieldNotice-wtieLUTj.js";
|
|
42
42
|
import { _ as _30 } from "./chunks/LpFormField-CtVYj6tO.js";
|
|
@@ -48,19 +48,19 @@ import { _ as _33 } from "./chunks/LpInput-D-YGQbEv.js";
|
|
|
48
48
|
import { _ as _34 } from "./chunks/LpLightbox-DSDahF23.js";
|
|
49
49
|
import { fileNameOf } from "./components/lightbox.js";
|
|
50
50
|
import { _ as _35 } from "./chunks/LpLink-Krkjhe_y.js";
|
|
51
|
-
import { _ as _36 } from "./chunks/LpLogViewer-
|
|
51
|
+
import { _ as _36 } from "./chunks/LpLogViewer-BDFXzvcl.js";
|
|
52
52
|
import { countLevels, matchPositions, matchesTerm, ringWindow, stepMatch } from "./components/logSearch.js";
|
|
53
53
|
import { _ as _37 } from "./chunks/LpMediaGallery-YKr-cQPN.js";
|
|
54
|
-
import { _ as _38 } from "./chunks/LpModal-
|
|
55
|
-
import { _ as _39 } from "./chunks/LpModalNav-
|
|
56
|
-
import { _ as _40 } from "./chunks/LpNotificationBell-
|
|
54
|
+
import { _ as _38 } from "./chunks/LpModal-JNa3B_Lr.js";
|
|
55
|
+
import { _ as _39 } from "./chunks/LpModalNav-BAxC9et7.js";
|
|
56
|
+
import { _ as _40 } from "./chunks/LpNotificationBell-DsqSyMwS.js";
|
|
57
57
|
import { _ as _41 } from "./chunks/LpNumberField-B77O-z28.js";
|
|
58
58
|
import { _ as _42 } from "./chunks/LpTimeField-Cqe7fXnh.js";
|
|
59
59
|
import { _ as _43 } from "./chunks/LpNumberFlow-z4WPZcZu.js";
|
|
60
60
|
import { _ as _44 } from "./chunks/LpOtpInput-CziPF3Oq.js";
|
|
61
61
|
import { _ as _45 } from "./chunks/LpPagination-BcDlo3r_.js";
|
|
62
62
|
import { _ as _46 } from "./chunks/LpPasswordInput-eKzQhbQP.js";
|
|
63
|
-
import { _ as _47 } from "./chunks/LpPie-
|
|
63
|
+
import { _ as _47 } from "./chunks/LpPie-BdSnta11.js";
|
|
64
64
|
import { _ as _48 } from "./chunks/LpPhoneInput-BCbrrhgE.js";
|
|
65
65
|
import { filterCountries, flagEmoji, loadCountries, matchCountryByValue } from "./components/countries.js";
|
|
66
66
|
import { _ as _49 } from "./chunks/LpPopover-DCKeqiVn.js";
|
|
@@ -68,14 +68,14 @@ import { _ as _50 } from "./chunks/LpProgress-CBU1iI69.js";
|
|
|
68
68
|
import { _ as _51 } from "./chunks/LpRadio-B5AfqTpZ.js";
|
|
69
69
|
import { _ as _52 } from "./chunks/LpRadioGroup-DmEW0l78.js";
|
|
70
70
|
import { _ as _53 } from "./chunks/LpRollingText-DB7upf-5.js";
|
|
71
|
-
import { _ as _54 } from "./chunks/LpScrollArea-
|
|
71
|
+
import { _ as _54 } from "./chunks/LpScrollArea-Hz3-JWUG.js";
|
|
72
72
|
import { _ as _55 } from "./chunks/LpSegmented-C09EEam2.js";
|
|
73
73
|
import { _ as _56 } from "./chunks/LpSection-tKEH0jYf.js";
|
|
74
74
|
import { _ as _57 } from "./chunks/LpSelect-DNIL6MB9.js";
|
|
75
75
|
import { _ as _58 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
|
|
76
76
|
import { _ as _59 } from "./chunks/LpShift-DXNsauQZ.js";
|
|
77
77
|
import { default as default6 } from "./components/LpShortcutsDialog.vue.js";
|
|
78
|
-
import { _ as _60 } from "./chunks/LpSidebar-
|
|
78
|
+
import { _ as _60 } from "./chunks/LpSidebar-DpEiGi8L.js";
|
|
79
79
|
import { _ as _61 } from "./chunks/LpSkeleton-BB6XIV-w.js";
|
|
80
80
|
import { _ as _62 } from "./chunks/LpSlider-Cuzmrd7p.js";
|
|
81
81
|
import { _ as _63 } from "./chunks/LpSparkline-CB80N8SJ.js";
|
|
@@ -89,7 +89,7 @@ import { _ as _68 } from "./chunks/LpTabs-29iW1XOx.js";
|
|
|
89
89
|
import { _ as _69 } from "./chunks/LpTagsInput-D1SuqbhB.js";
|
|
90
90
|
import { addTags, splitTags } from "./components/tags.js";
|
|
91
91
|
import { _ as _70 } from "./chunks/LpTextarea-uuScWo6-.js";
|
|
92
|
-
import { _ as _71 } from "./chunks/LpThemeSwitcher-
|
|
92
|
+
import { _ as _71 } from "./chunks/LpThemeSwitcher-BbYFFT5w.js";
|
|
93
93
|
import { _ as _72 } from "./chunks/LpTilt-B7KHX1zH.js";
|
|
94
94
|
import { _ as _73 } from "./chunks/LpToaster-DTtWuKvq.js";
|
|
95
95
|
import { _ as _74 } from "./chunks/LpTooltip-CXVKcOXT.js";
|
package/package.json
CHANGED
|
@@ -594,6 +594,10 @@ const grouped = computed<{ name: string; items: Command[] }[]>(() => {
|
|
|
594
594
|
*/
|
|
595
595
|
const expanded = ref(new Set<string>())
|
|
596
596
|
const moreOf = new WeakMap<Command, string>()
|
|
597
|
+
// The first row each "Show more" hides. Unfolding removes the "Show more" row
|
|
598
|
+
// itself, so the highlight has to be handed to this one: an id that is gone
|
|
599
|
+
// from the list sends the selection back to the top, and the list with it.
|
|
600
|
+
const firstHidden = new WeakMap<Command, string>()
|
|
597
601
|
|
|
598
602
|
/*
|
|
599
603
|
* Folding hides at least two rows: a "Show 1 more" row takes the same space
|
|
@@ -609,6 +613,7 @@ function folded(name: string, items: Command[]): Command[] {
|
|
|
609
613
|
keepOpen: true,
|
|
610
614
|
}
|
|
611
615
|
moreOf.set(more, name)
|
|
616
|
+
firstHidden.set(more, items[limit].id)
|
|
612
617
|
return [...items.slice(0, limit), more]
|
|
613
618
|
}
|
|
614
619
|
|
|
@@ -822,6 +827,7 @@ function run(c: Command | undefined) {
|
|
|
822
827
|
// was hidden: the arrows carry on from where the fold was.
|
|
823
828
|
const group = moreOf.get(c)
|
|
824
829
|
if (group !== undefined) {
|
|
830
|
+
selectedId.value = firstHidden.get(c) ?? null
|
|
825
831
|
expanded.value = new Set([...expanded.value, group])
|
|
826
832
|
inputRef.value?.focus()
|
|
827
833
|
return
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { ContextMenuContent, ContextMenuPortal, ContextMenuRoot, ContextMenuTrigger } from "reka-ui"
|
|
10
10
|
import { computed } from "vue"
|
|
11
11
|
import ContextMenuItems from "./ContextMenuItems.vue"
|
|
12
|
+
import MenuTitle from "./MenuTitle.vue"
|
|
12
13
|
import { POPOVER_PANEL } from "./dropdown"
|
|
13
14
|
|
|
14
15
|
export interface ContextMenuItemDef {
|
|
@@ -40,6 +41,17 @@ const props = defineProps<{
|
|
|
40
41
|
* native-menu mode on that first click and swallow it.
|
|
41
42
|
*/
|
|
42
43
|
always?: boolean
|
|
44
|
+
/**
|
|
45
|
+
* What the menu acts on, shown as a heading above the items: the row's name
|
|
46
|
+
* in a list of similar rows, so the user sees which one the right-click hit.
|
|
47
|
+
* Also names the menu for assistive tech.
|
|
48
|
+
*/
|
|
49
|
+
title?: string
|
|
50
|
+
/** A muted second line under `title`, e.g. a version or a path. */
|
|
51
|
+
subtitle?: string
|
|
52
|
+
/** An icon beside the title, or `titleImage` for a picture (an avatar, a logo). */
|
|
53
|
+
titleIcon?: string
|
|
54
|
+
titleImage?: string
|
|
43
55
|
}>()
|
|
44
56
|
|
|
45
57
|
// With no items the trigger is disabled, so the element keeps the browser's
|
|
@@ -87,7 +99,13 @@ function returnFocus(event: Event) {
|
|
|
87
99
|
<slot />
|
|
88
100
|
</ContextMenuTrigger>
|
|
89
101
|
<ContextMenuPortal v-if="items.length">
|
|
90
|
-
<ContextMenuContent
|
|
102
|
+
<ContextMenuContent
|
|
103
|
+
:collision-padding="8"
|
|
104
|
+
:class="PANEL"
|
|
105
|
+
:aria-label="title"
|
|
106
|
+
@close-auto-focus="returnFocus"
|
|
107
|
+
>
|
|
108
|
+
<MenuTitle v-if="title" :title="title" :subtitle="subtitle" :icon="titleIcon" :image="titleImage" />
|
|
91
109
|
<ContextMenuItems :items="items" :panel="PANEL" />
|
|
92
110
|
</ContextMenuContent>
|
|
93
111
|
</ContextMenuPortal>
|
|
@@ -9,16 +9,25 @@ import {
|
|
|
9
9
|
} from "reka-ui"
|
|
10
10
|
import { POPOVER_PANEL } from "./dropdown"
|
|
11
11
|
import LpIcon from "./LpIcon.vue"
|
|
12
|
+
import MenuTitle from "./MenuTitle.vue"
|
|
12
13
|
|
|
13
14
|
export interface MenuItem {
|
|
14
15
|
label: string
|
|
15
16
|
icon?: string
|
|
16
17
|
danger?: boolean
|
|
17
18
|
separatorBefore?: boolean
|
|
19
|
+
disabled?: boolean
|
|
18
20
|
onSelect?: () => void
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
defineProps<{
|
|
23
|
+
defineProps<{
|
|
24
|
+
items: MenuItem[]
|
|
25
|
+
/** A heading naming what the menu acts on; see LpContextMenu's `title`. */
|
|
26
|
+
title?: string
|
|
27
|
+
subtitle?: string
|
|
28
|
+
titleIcon?: string
|
|
29
|
+
titleImage?: string
|
|
30
|
+
}>()
|
|
22
31
|
</script>
|
|
23
32
|
|
|
24
33
|
<template>
|
|
@@ -32,12 +41,15 @@ defineProps<{ items: MenuItem[] }>()
|
|
|
32
41
|
:side-offset="6"
|
|
33
42
|
align="end"
|
|
34
43
|
:class="[POPOVER_PANEL, 'z-(--z-popover) min-w-44 p-1 outline-none']"
|
|
44
|
+
:aria-label="title"
|
|
35
45
|
>
|
|
46
|
+
<MenuTitle v-if="title" :title="title" :subtitle="subtitle" :icon="titleIcon" :image="titleImage" />
|
|
36
47
|
<template v-for="(item, i) in items" :key="i">
|
|
37
48
|
<DropdownMenuSeparator v-if="item.separatorBefore" class="my-1 h-px bg-line" />
|
|
38
49
|
<DropdownMenuItem
|
|
39
|
-
class="group/item flex cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-sm outline-none transition-[background-color,color] duration-[var(--duration-fast)] data-[highlighted]:bg-brand-soft"
|
|
50
|
+
class="group/item flex cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-sm outline-none transition-[background-color,color] duration-[var(--duration-fast)] data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[highlighted]:bg-brand-soft"
|
|
40
51
|
:class="item.danger ? 'text-danger data-[highlighted]:bg-danger-soft data-[highlighted]:text-danger' : 'text-ink data-[highlighted]:text-brand'"
|
|
52
|
+
:disabled="item.disabled"
|
|
41
53
|
@select="item.onSelect?.()"
|
|
42
54
|
>
|
|
43
55
|
<LpIcon
|
|
@@ -23,7 +23,7 @@ export default { inheritAttrs: false }
|
|
|
23
23
|
* invisible to a keyboard: nothing focusable, nothing that Enter activates, and
|
|
24
24
|
* the file dialog unreachable without a mouse.
|
|
25
25
|
*/
|
|
26
|
-
import { onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
26
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
27
27
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
28
28
|
import { useReveal } from "../composables/useReveal"
|
|
29
29
|
import LpIcon from "./LpIcon.vue"
|
|
@@ -35,6 +35,13 @@ const props = withDefaults(
|
|
|
35
35
|
/** Take more than one at a time. */
|
|
36
36
|
multiple?: boolean
|
|
37
37
|
disabled?: boolean
|
|
38
|
+
/**
|
|
39
|
+
* A drag the page itself cannot see is over the zone. Desktop shells
|
|
40
|
+
* (Tauri, Electron) often take file drags natively, and the page then gets
|
|
41
|
+
* no dragenter or dragover at all, only the shell's own events; the app
|
|
42
|
+
* hit-tests those and passes the answer here so the zone lights up the same.
|
|
43
|
+
*/
|
|
44
|
+
dragOver?: boolean
|
|
38
45
|
/**
|
|
39
46
|
* Also accept a paste anywhere on the page, not only while focused.
|
|
40
47
|
* Off by default: two zones both listening would each take the same
|
|
@@ -95,11 +102,12 @@ const input = ref<HTMLInputElement | null>(null)
|
|
|
95
102
|
* may not — one stuck increment and the zone stays lit after the drag is long
|
|
96
103
|
* gone, which is exactly what it did.
|
|
97
104
|
*/
|
|
98
|
-
const
|
|
105
|
+
const pointerOver = ref(false)
|
|
106
|
+
const over = computed(() => pointerOver.value || (props.dragOver && !props.disabled))
|
|
99
107
|
|
|
100
108
|
function onDragOver(event: DragEvent) {
|
|
101
109
|
if (props.disabled) return
|
|
102
|
-
|
|
110
|
+
pointerOver.value = true
|
|
103
111
|
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy"
|
|
104
112
|
}
|
|
105
113
|
|
|
@@ -108,7 +116,7 @@ function onDragLeave(event: DragEvent) {
|
|
|
108
116
|
// drag merely crossed a child, and the highlight stays.
|
|
109
117
|
const to = event.relatedTarget as Node | null
|
|
110
118
|
if (to && (event.currentTarget as HTMLElement).contains(to)) return
|
|
111
|
-
|
|
119
|
+
pointerOver.value = false
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
/** Does the file match `accept`? Mirrors the input's own matching so a drop
|
|
@@ -146,7 +154,7 @@ function take(list: FileList | File[] | null | undefined) {
|
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
function onDrop(event: DragEvent) {
|
|
149
|
-
|
|
157
|
+
pointerOver.value = false
|
|
150
158
|
take(event.dataTransfer?.files)
|
|
151
159
|
}
|
|
152
160
|
|
|
@@ -332,7 +332,7 @@ watch(
|
|
|
332
332
|
{{ ariaLabel }}
|
|
333
333
|
</DialogTitle>
|
|
334
334
|
<header v-if="title || $slots.title" class="flex shrink-0 items-start justify-between gap-4 p-5 pb-3">
|
|
335
|
-
<div class="flex flex-col gap-1">
|
|
335
|
+
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
336
336
|
<DialogTitle class="text-base font-semibold text-ink">
|
|
337
337
|
<slot name="title">{{ title }}</slot>
|
|
338
338
|
</DialogTitle>
|
|
@@ -340,6 +340,12 @@ watch(
|
|
|
340
340
|
{{ description }}
|
|
341
341
|
</DialogDescription>
|
|
342
342
|
</div>
|
|
343
|
+
<!-- Controls that act on the whole dialog (a period, a filter) sit
|
|
344
|
+
beside the close button, not inside the title: the title is a
|
|
345
|
+
heading, and a button in it would be read out as part of it. -->
|
|
346
|
+
<div v-if="$slots.actions" class="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
|
347
|
+
<slot name="actions" />
|
|
348
|
+
</div>
|
|
343
349
|
<!-- Marked so the auto-focus pass skips it: it is first in the DOM
|
|
344
350
|
and last in usefulness. Still reachable by Tab and Escape. -->
|
|
345
351
|
<DialogClose
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ScrollAreaThumb,
|
|
14
14
|
ScrollAreaViewport,
|
|
15
15
|
} from "reka-ui"
|
|
16
|
+
import { prefersReducedMotion } from "../composables/easing"
|
|
16
17
|
|
|
17
18
|
// `fade` softens whichever of the top/bottom edges currently has content beyond
|
|
18
19
|
// it, so a clipped list reads as continuing rather than as cut off. On by
|
|
@@ -182,6 +183,29 @@ watch(
|
|
|
182
183
|
|
|
183
184
|
onBeforeUnmount(stopEdgeWatch)
|
|
184
185
|
|
|
186
|
+
/*
|
|
187
|
+
* A press on the bar beside the thumb. reka treats any press on the track as
|
|
188
|
+
* "put the thumb's middle under the pointer" and sets the scroll in one go,
|
|
189
|
+
* so a press that just missed the narrow thumb yanked the content by half a
|
|
190
|
+
* thumb, and a press further away teleported it. Like a native bar, a press
|
|
191
|
+
* off the thumb now pages one viewport towards the pointer, smoothly; the
|
|
192
|
+
* thumb itself has a hit area as wide as the bar (see its `before:`), so a
|
|
193
|
+
* press that meant to grab it does.
|
|
194
|
+
*/
|
|
195
|
+
function onTrackPointerDown(event: PointerEvent, axis: "x" | "y") {
|
|
196
|
+
const el = viewportEl.value
|
|
197
|
+
const bar = event.currentTarget as HTMLElement
|
|
198
|
+
const thumb = bar.querySelector<HTMLElement>("[data-lp-thumb]")
|
|
199
|
+
if (!el || !thumb || event.button !== 0 || thumb.contains(event.target as Node)) return
|
|
200
|
+
event.preventDefault()
|
|
201
|
+
event.stopImmediatePropagation()
|
|
202
|
+
const rect = thumb.getBoundingClientRect()
|
|
203
|
+
const before = axis === "y" ? event.clientY < rect.top : event.clientX < rect.left
|
|
204
|
+
const step = (axis === "y" ? el.clientHeight : el.clientWidth) * 0.85 * (before ? -1 : 1)
|
|
205
|
+
const behavior = prefersReducedMotion() ? "auto" : "smooth"
|
|
206
|
+
el.scrollBy(axis === "y" ? { top: step, behavior } : { left: step, behavior })
|
|
207
|
+
}
|
|
208
|
+
|
|
185
209
|
function onThumbPointerDown() {
|
|
186
210
|
dragging.value = true
|
|
187
211
|
const stop = () => {
|
|
@@ -230,6 +254,7 @@ function onThumbPointerDown() {
|
|
|
230
254
|
class="group z-1 flex w-3.5 touch-none select-none justify-end py-px pl-2.5 pr-px"
|
|
231
255
|
:class="barFade"
|
|
232
256
|
:style="barInsetTop ? { marginTop: barInsetTop } : undefined"
|
|
257
|
+
@pointerdown.capture="onTrackPointerDown($event, 'y')"
|
|
233
258
|
>
|
|
234
259
|
<!-- Sized with min-width, not width: reka sets an inline
|
|
235
260
|
`width: var(--reka-scroll-area-thumb-width)` on the thumb, and for a
|
|
@@ -238,7 +263,8 @@ function onThumbPointerDown() {
|
|
|
238
263
|
leaving an invisible thumb. A minimum wins over that without fighting
|
|
239
264
|
the positioning reka owns. -->
|
|
240
265
|
<ScrollAreaThumb
|
|
241
|
-
|
|
266
|
+
data-lp-thumb
|
|
267
|
+
class="relative min-w-1 rounded-l-pill bg-brand/40 before:absolute before:inset-y-0 before:right-0 before:-left-2.5 before:content-[''] transition-[min-width,background-color] duration-[var(--duration-fast)] group-hover:min-w-1.5 group-hover:bg-brand"
|
|
242
268
|
@pointerdown="onThumbPointerDown"
|
|
243
269
|
/>
|
|
244
270
|
</ScrollAreaScrollbar>
|
|
@@ -247,9 +273,11 @@ function onThumbPointerDown() {
|
|
|
247
273
|
orientation="horizontal"
|
|
248
274
|
class="group z-1 flex h-3.5 flex-col touch-none select-none justify-end px-px pb-px pt-2.5"
|
|
249
275
|
:class="barFade"
|
|
276
|
+
@pointerdown.capture="onTrackPointerDown($event, 'x')"
|
|
250
277
|
>
|
|
251
278
|
<ScrollAreaThumb
|
|
252
|
-
|
|
279
|
+
data-lp-thumb
|
|
280
|
+
class="relative min-h-1 rounded-t-pill bg-brand/40 before:absolute before:inset-x-0 before:bottom-0 before:-top-2.5 before:content-[''] transition-[min-height,background-color] duration-[var(--duration-fast)] group-hover:min-h-1.5 group-hover:bg-brand"
|
|
253
281
|
@pointerdown="onThumbPointerDown"
|
|
254
282
|
/>
|
|
255
283
|
</ScrollAreaScrollbar>
|
|
@@ -1098,7 +1098,7 @@ const barInsetTop = computed(() =>
|
|
|
1098
1098
|
<LpScrollArea
|
|
1099
1099
|
v-bind="scrollAttrs"
|
|
1100
1100
|
:fade="false"
|
|
1101
|
-
class="min-h-0 flex-1 rounded-card border border-line"
|
|
1101
|
+
class="min-h-0 flex-1 rounded-card border border-line bg-surface"
|
|
1102
1102
|
:style="load && floor ? { minHeight: `${floor + 2}px` } : undefined"
|
|
1103
1103
|
:bar-inset-top="barInsetTop"
|
|
1104
1104
|
>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* The heading row of a menu: what the menu acts on, so a right-click on one of
|
|
4
|
+
* many look-alike rows says which one it caught before anything is chosen.
|
|
5
|
+
* Not an item: it takes no focus or highlight, and long names truncate rather
|
|
6
|
+
* than widening the panel. Internal: LpContextMenu and LpDropdownMenu render it
|
|
7
|
+
* from their `title` props.
|
|
8
|
+
*/
|
|
9
|
+
import LpIcon from "./LpIcon.vue"
|
|
10
|
+
|
|
11
|
+
defineProps<{
|
|
12
|
+
title: string
|
|
13
|
+
subtitle?: string
|
|
14
|
+
icon?: string
|
|
15
|
+
image?: string
|
|
16
|
+
}>()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div class="flex max-w-72 min-w-0 items-center gap-2 px-2.5 pt-1.5 pb-2" aria-hidden="true">
|
|
21
|
+
<img v-if="image" :src="image" alt="" class="size-7 shrink-0 rounded-md object-cover" />
|
|
22
|
+
<span
|
|
23
|
+
v-else-if="icon"
|
|
24
|
+
class="grid size-7 shrink-0 place-items-center rounded-md bg-surface-soft text-muted"
|
|
25
|
+
>
|
|
26
|
+
<LpIcon :name="icon" :size="15" />
|
|
27
|
+
</span>
|
|
28
|
+
<span class="flex min-w-0 flex-col">
|
|
29
|
+
<span class="truncate text-sm font-medium text-ink">{{ title }}</span>
|
|
30
|
+
<span v-if="subtitle" class="truncate text-xs text-muted">{{ subtitle }}</span>
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="mb-1 h-px bg-line" role="presentation" />
|
|
34
|
+
</template>
|
|
@@ -13,7 +13,9 @@ import { useToast } from "./useToast"
|
|
|
13
13
|
*
|
|
14
14
|
* Why every call is allowed to fail: the Clipboard API needs a secure context
|
|
15
15
|
* and, in some browsers, a permission. It is absent over plain http, rejects
|
|
16
|
-
* outside a user gesture, and is missing entirely in a non-DOM test runner.
|
|
16
|
+
* outside a user gesture, and is missing entirely in a non-DOM test runner.
|
|
17
|
+
* Where it is missing or refuses, the old copy command is tried before
|
|
18
|
+
* giving up (see `writeLegacy`). A
|
|
17
19
|
* copy that cannot happen is a disappointment, not an exception, and a rejected
|
|
18
20
|
* promise reaching a component means an unhandled rejection in the console of
|
|
19
21
|
* an app that did nothing wrong.
|
|
@@ -23,6 +25,51 @@ import { useToast } from "./useToast"
|
|
|
23
25
|
* than any message, and the two are complementary rather than alternatives.
|
|
24
26
|
*/
|
|
25
27
|
|
|
28
|
+
/** The Clipboard API, where there is one and it agrees. */
|
|
29
|
+
async function writeAsync(text: string): Promise<boolean> {
|
|
30
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) return false
|
|
31
|
+
try {
|
|
32
|
+
await navigator.clipboard.writeText(text)
|
|
33
|
+
return true
|
|
34
|
+
} catch {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* The old copy command, for where the Clipboard API is missing or refuses.
|
|
41
|
+
* Embedded Chromium (CEF in a Tauri or Electron-like shell) rejects
|
|
42
|
+
* `writeText` when the host answers no permission requests, and plain http
|
|
43
|
+
* has no API at all, while a copy command run inside the click still works
|
|
44
|
+
* there. It copies the selection, so a throwaway text field holds the text
|
|
45
|
+
* and the page's own selection and focus are put back afterwards.
|
|
46
|
+
*/
|
|
47
|
+
function writeLegacy(text: string): boolean {
|
|
48
|
+
if (typeof document === "undefined") return false
|
|
49
|
+
const active = document.activeElement as HTMLElement | null
|
|
50
|
+
const selection = document.getSelection()
|
|
51
|
+
const ranges = selection ? Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i)) : []
|
|
52
|
+
const field = document.createElement("textarea")
|
|
53
|
+
field.value = text
|
|
54
|
+
field.setAttribute("readonly", "")
|
|
55
|
+
field.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none"
|
|
56
|
+
document.body.appendChild(field)
|
|
57
|
+
field.select()
|
|
58
|
+
let ok = false
|
|
59
|
+
try {
|
|
60
|
+
ok = document.execCommand("copy")
|
|
61
|
+
} catch {
|
|
62
|
+
ok = false
|
|
63
|
+
}
|
|
64
|
+
field.remove()
|
|
65
|
+
if (selection) {
|
|
66
|
+
selection.removeAllRanges()
|
|
67
|
+
for (const range of ranges) selection.addRange(range)
|
|
68
|
+
}
|
|
69
|
+
active?.focus?.({ preventScroll: true })
|
|
70
|
+
return ok
|
|
71
|
+
}
|
|
72
|
+
|
|
26
73
|
/** How long `copied` stays true — long enough to read, short enough to not linger. */
|
|
27
74
|
const COPIED_MS = 1400
|
|
28
75
|
|
|
@@ -81,12 +128,7 @@ export function useClipboard(): UseClipboard {
|
|
|
81
128
|
async function copy(text: string, options: CopyOptions = {}): Promise<boolean> {
|
|
82
129
|
const { toast = false, errorToast = false } = options
|
|
83
130
|
try {
|
|
84
|
-
|
|
85
|
-
// an insecure origin, and that should take the same path as a rejection.
|
|
86
|
-
await navigator.clipboard?.writeText(text)
|
|
87
|
-
// A missing API resolves `undefined` rather than rejecting, so confirm
|
|
88
|
-
// the write was actually possible before claiming it happened.
|
|
89
|
-
if (!navigator.clipboard) throw new Error("clipboard unavailable")
|
|
131
|
+
if (!(await writeAsync(text)) && !writeLegacy(text)) throw new Error("clipboard unavailable")
|
|
90
132
|
|
|
91
133
|
copied.value = true
|
|
92
134
|
clearTimeout(timer)
|