@leavepulse/ui 0.22.21 → 0.22.22
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/{LpAddressInput-CxKwIG6T.js → LpAddressInput-peTpNSJK.js} +3 -3
- package/dist/chunks/{LpAppShell-D8CAP9oF.js → LpAppShell-CaQYQKZV.js} +25 -14
- package/dist/chunks/{LpConfirmDialog-CmgeAdCh.js → LpConfirmDialog-DOU_zaTI.js} +1 -1
- package/dist/chunks/{LpFormField-Bn-ZwC6z.js → LpFormField-CesaPWPO.js} +12 -6
- package/dist/chunks/{LpInput-BkomKdqQ.js → LpInput-BfLIWV-P.js} +1 -1
- package/dist/chunks/{LpModal-CNNSf5pv.js → LpModal-D_o-56To.js} +13 -76
- package/dist/chunks/{LpNotificationBell-DWDEkmdA.js → LpNotificationBell-a08cl0yY.js} +67 -61
- package/dist/chunks/{LpPasswordInput-DgUC2Ur7.js → LpPasswordInput-BfjDrvZq.js} +1 -1
- package/dist/chunks/LpShift-DXNsauQZ.js +40 -0
- package/dist/chunks/{LpTextarea-CRAkhemZ.js → LpTextarea-BDl7PR9U.js} +1 -1
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.d.ts +6 -6
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpShift.vue.d.ts +36 -0
- package/dist/components/LpShift.vue.js +4 -0
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/composables/useShift.d.ts +35 -0
- package/dist/composables/useShift.js +112 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +44 -40
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpAppShell.vue +27 -7
- package/src/components/LpFormField.vue +19 -2
- package/src/components/LpModal.vue +20 -128
- package/src/components/LpNotificationBell.vue +48 -42
- package/src/components/LpShift.vue +90 -0
- package/src/composables/useShift.ts +236 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { MaybeRefOrGetter, Ref } from 'vue';
|
|
2
|
+
export type ShiftAxis = "height" | "width" | "both";
|
|
3
|
+
export interface UseShiftOptions {
|
|
4
|
+
/** Which dimension is content-derived. Default `"height"`. */
|
|
5
|
+
axis?: MaybeRefOrGetter<ShiftAxis | undefined>;
|
|
6
|
+
/** Tween length. Defaults to the kit's `--duration-fast` (160ms). */
|
|
7
|
+
duration?: MaybeRefOrGetter<number | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* Gate: while falsy the element is left entirely alone — no observers, no
|
|
10
|
+
* pinning. Use it for a box that only exists while something is open, so a
|
|
11
|
+
* remounted element starts measuring from scratch rather than easing in from
|
|
12
|
+
* a size it held in a previous life. Default: always active.
|
|
13
|
+
*/
|
|
14
|
+
enabled?: MaybeRefOrGetter<unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface UseShift {
|
|
17
|
+
/** Attach to the element whose content-derived size should ease. */
|
|
18
|
+
el: Ref<HTMLElement | null>;
|
|
19
|
+
/**
|
|
20
|
+
* True while a tween owns the size. Bind the transition utilities off this,
|
|
21
|
+
* so the element is only transition-enabled once it has a size to ease FROM
|
|
22
|
+
* (otherwise the very first pin animates in from zero).
|
|
23
|
+
*/
|
|
24
|
+
tweening: Ref<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* True while the element is still catching up to its new size. During the
|
|
27
|
+
* tween the box is briefly smaller than the content it already holds, so any
|
|
28
|
+
* scroll region inside it would raise a bar for an overflow that resolves
|
|
29
|
+
* itself as the tween lands — hide the bar (not the scrolling) on this.
|
|
30
|
+
*/
|
|
31
|
+
resizing: Ref<boolean>;
|
|
32
|
+
/** Re-measure and ease to the current natural size. Rarely needed by hand. */
|
|
33
|
+
retune: () => void;
|
|
34
|
+
}
|
|
35
|
+
export declare function useShift(options?: UseShiftOptions): UseShift;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ref, watch, toValue, onBeforeUnmount } from "vue";
|
|
2
|
+
const TWEEN_MS = 160;
|
|
3
|
+
function useShift(options = {}) {
|
|
4
|
+
const el = ref(null);
|
|
5
|
+
const tweening = ref(false);
|
|
6
|
+
const resizing = ref(false);
|
|
7
|
+
let ro;
|
|
8
|
+
let mo;
|
|
9
|
+
let raf = 0;
|
|
10
|
+
let settleTimer;
|
|
11
|
+
let lastHeight = 0;
|
|
12
|
+
let lastWidth = 0;
|
|
13
|
+
let inFlight = false;
|
|
14
|
+
const axisOf = () => toValue(options.axis) ?? "height";
|
|
15
|
+
const wantsHeight = () => axisOf() !== "width";
|
|
16
|
+
const wantsWidth = () => axisOf() !== "height";
|
|
17
|
+
function reducedMotion() {
|
|
18
|
+
return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
19
|
+
}
|
|
20
|
+
function retune() {
|
|
21
|
+
const node = el.value;
|
|
22
|
+
if (!node) return;
|
|
23
|
+
if (inFlight) return;
|
|
24
|
+
const fromHeight = lastHeight;
|
|
25
|
+
const fromWidth = lastWidth;
|
|
26
|
+
if (wantsHeight()) {
|
|
27
|
+
node.style.height = "";
|
|
28
|
+
node.style.maxHeight = "none";
|
|
29
|
+
}
|
|
30
|
+
if (wantsWidth()) {
|
|
31
|
+
node.style.width = "";
|
|
32
|
+
node.style.maxWidth = "none";
|
|
33
|
+
}
|
|
34
|
+
const toHeight = wantsHeight() ? node.offsetHeight : 0;
|
|
35
|
+
const toWidth = wantsWidth() ? node.offsetWidth : 0;
|
|
36
|
+
if (wantsHeight()) node.style.maxHeight = "";
|
|
37
|
+
if (wantsWidth()) node.style.maxWidth = "";
|
|
38
|
+
lastHeight = toHeight;
|
|
39
|
+
lastWidth = toWidth;
|
|
40
|
+
const movesHeight = wantsHeight() && fromHeight !== 0 && toHeight !== fromHeight;
|
|
41
|
+
const movesWidth = wantsWidth() && fromWidth !== 0 && toWidth !== fromWidth;
|
|
42
|
+
if (!movesHeight && !movesWidth) return;
|
|
43
|
+
if (movesHeight) node.style.height = `${fromHeight}px`;
|
|
44
|
+
if (movesWidth) node.style.width = `${fromWidth}px`;
|
|
45
|
+
void node.offsetHeight;
|
|
46
|
+
if (movesHeight) node.style.height = `${toHeight}px`;
|
|
47
|
+
if (movesWidth) node.style.width = `${toWidth}px`;
|
|
48
|
+
inFlight = true;
|
|
49
|
+
resizing.value = true;
|
|
50
|
+
clearTimeout(settleTimer);
|
|
51
|
+
const wait = reducedMotion() ? 0 : toValue(options.duration) ?? TWEEN_MS;
|
|
52
|
+
settleTimer = setTimeout(() => {
|
|
53
|
+
inFlight = false;
|
|
54
|
+
resizing.value = false;
|
|
55
|
+
node.style.height = "";
|
|
56
|
+
node.style.width = "";
|
|
57
|
+
lastHeight = node.offsetHeight;
|
|
58
|
+
lastWidth = node.offsetWidth;
|
|
59
|
+
}, wait);
|
|
60
|
+
}
|
|
61
|
+
function teardown() {
|
|
62
|
+
ro?.disconnect();
|
|
63
|
+
mo?.disconnect();
|
|
64
|
+
ro = void 0;
|
|
65
|
+
mo = void 0;
|
|
66
|
+
cancelAnimationFrame(raf);
|
|
67
|
+
clearTimeout(settleTimer);
|
|
68
|
+
tweening.value = false;
|
|
69
|
+
resizing.value = false;
|
|
70
|
+
lastHeight = 0;
|
|
71
|
+
lastWidth = 0;
|
|
72
|
+
inFlight = false;
|
|
73
|
+
}
|
|
74
|
+
watch(
|
|
75
|
+
[el, () => toValue(options.enabled) ?? true],
|
|
76
|
+
([node, enabled]) => {
|
|
77
|
+
teardown();
|
|
78
|
+
if (!enabled || !node) return;
|
|
79
|
+
lastHeight = node.offsetHeight;
|
|
80
|
+
lastWidth = node.offsetWidth;
|
|
81
|
+
if (wantsHeight()) node.style.height = `${lastHeight}px`;
|
|
82
|
+
if (wantsWidth()) node.style.width = `${lastWidth}px`;
|
|
83
|
+
raf = requestAnimationFrame(() => {
|
|
84
|
+
tweening.value = true;
|
|
85
|
+
node.style.height = "";
|
|
86
|
+
node.style.width = "";
|
|
87
|
+
});
|
|
88
|
+
const watchChildren = () => {
|
|
89
|
+
if (!ro) return;
|
|
90
|
+
ro.disconnect();
|
|
91
|
+
for (const child of Array.from(node.children)) ro.observe(child);
|
|
92
|
+
};
|
|
93
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
94
|
+
ro = new ResizeObserver(() => retune());
|
|
95
|
+
watchChildren();
|
|
96
|
+
}
|
|
97
|
+
if (typeof MutationObserver !== "undefined") {
|
|
98
|
+
mo = new MutationObserver(() => {
|
|
99
|
+
watchChildren();
|
|
100
|
+
retune();
|
|
101
|
+
});
|
|
102
|
+
mo.observe(node, { childList: true, subtree: true, characterData: true });
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{ flush: "post" }
|
|
106
|
+
);
|
|
107
|
+
onBeforeUnmount(teardown);
|
|
108
|
+
return { el, tweening, resizing, retune };
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
useShift
|
|
112
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ 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 LpShift } from './components/LpShift.vue';
|
|
70
71
|
export { default as LpShortcutsDialog } from './components/LpShortcutsDialog.vue';
|
|
71
72
|
export type { ShortcutEntry, ShortcutGroup } from './components/LpShortcutsDialog.vue';
|
|
72
73
|
export { default as LpSidebar } from './components/LpSidebar.vue';
|
|
@@ -117,6 +118,8 @@ export { useRovingFocus } from './composables/useRovingFocus';
|
|
|
117
118
|
export type { RovingItem, UseRovingFocusOptions } from './composables/useRovingFocus';
|
|
118
119
|
export { useSettled } from './composables/useSettled';
|
|
119
120
|
export type { UseSettledOptions } from './composables/useSettled';
|
|
121
|
+
export { useShift } from './composables/useShift';
|
|
122
|
+
export type { ShiftAxis, UseShift, UseShiftOptions } from './composables/useShift';
|
|
120
123
|
export { useTilt } from './composables/useTilt';
|
|
121
124
|
export type { UseTilt, UseTiltOptions } from './composables/useTilt';
|
|
122
125
|
export { useZoomPan } from './composables/useZoomPan';
|
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-peTpNSJK.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-CaQYQKZV.js";
|
|
7
7
|
import { _ as _6 } from "./chunks/LpAutocomplete-DExlIZe_.js";
|
|
8
8
|
import { _ as _7 } from "./chunks/LpAvatar-B1Ez0ZAx.js";
|
|
9
9
|
import { _ as _8 } from "./chunks/LpBadge-BPodVcJa.js";
|
|
@@ -14,7 +14,7 @@ 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-DOU_zaTI.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";
|
|
@@ -24,20 +24,20 @@ import { _ as _21 } from "./chunks/LpDropdownMenu-DJpoqRCb.js";
|
|
|
24
24
|
import { _ as _22 } from "./chunks/LpEmptyState-CrfyRJUG.js";
|
|
25
25
|
import { _ as _23 } from "./chunks/LpFileTree-Cw50eG_y.js";
|
|
26
26
|
import { ancestorIds, checkStateOf, checkedCount, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
|
|
27
|
-
import { _ as _24 } from "./chunks/LpFormField-
|
|
27
|
+
import { _ as _24 } from "./chunks/LpFormField-CesaPWPO.js";
|
|
28
28
|
import { _ as _25 } from "./chunks/LpIcon-CCnX5_2j.js";
|
|
29
29
|
import { _ as _26 } from "./chunks/LpInfraNode-qIL3l7IO.js";
|
|
30
|
-
import { _ as _27 } from "./chunks/LpInput-
|
|
30
|
+
import { _ as _27 } from "./chunks/LpInput-BfLIWV-P.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
34
|
import { _ as _30 } from "./chunks/LpLogViewer-B1aLUVdz.js";
|
|
35
|
-
import { _ as _31 } from "./chunks/LpModal-
|
|
36
|
-
import { _ as _32 } from "./chunks/LpNotificationBell-
|
|
35
|
+
import { _ as _31 } from "./chunks/LpModal-D_o-56To.js";
|
|
36
|
+
import { _ as _32 } from "./chunks/LpNotificationBell-a08cl0yY.js";
|
|
37
37
|
import { _ as _33 } from "./chunks/LpNumberField-B3JuQFol.js";
|
|
38
38
|
import { _ as _34 } from "./chunks/LpOtpInput-DqaT0Z75.js";
|
|
39
39
|
import { _ as _35 } from "./chunks/LpPagination-CeGuNe_q.js";
|
|
40
|
-
import { _ as _36 } from "./chunks/LpPasswordInput-
|
|
40
|
+
import { _ as _36 } from "./chunks/LpPasswordInput-BfjDrvZq.js";
|
|
41
41
|
import { _ as _37 } from "./chunks/LpPhoneInput-NXXjciJJ.js";
|
|
42
42
|
import { flagEmoji, loadCountries, matchCountryByValue } from "./components/countries.js";
|
|
43
43
|
import { _ as _38 } from "./chunks/LpPopover-8268UmlC.js";
|
|
@@ -48,24 +48,25 @@ import { _ as _42 } from "./chunks/LpScrollArea-D8xyKXcF.js";
|
|
|
48
48
|
import { _ as _43 } from "./chunks/LpSegmented-BmpanyAo.js";
|
|
49
49
|
import { _ as _44 } from "./chunks/LpSelect-Cw8-gL5F.js";
|
|
50
50
|
import { _ as _45 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
|
|
51
|
+
import { _ as _46 } from "./chunks/LpShift-DXNsauQZ.js";
|
|
51
52
|
import { default as default3 } from "./components/LpShortcutsDialog.vue.js";
|
|
52
|
-
import { _ as
|
|
53
|
-
import { _ as
|
|
54
|
-
import { _ as
|
|
55
|
-
import { _ as
|
|
56
|
-
import { _ as
|
|
57
|
-
import { _ as
|
|
58
|
-
import { _ as
|
|
59
|
-
import { _ as
|
|
60
|
-
import { _ as
|
|
61
|
-
import { _ as
|
|
53
|
+
import { _ as _47 } from "./chunks/LpSidebar-D3GGLSE7.js";
|
|
54
|
+
import { _ as _48 } from "./chunks/LpSkeleton-eSFIo4kD.js";
|
|
55
|
+
import { _ as _49 } from "./chunks/LpSlider-B1SF3pRV.js";
|
|
56
|
+
import { _ as _50 } from "./chunks/LpStat-CdbEFOvn.js";
|
|
57
|
+
import { _ as _51 } from "./chunks/LpStepper-5dFTrW21.js";
|
|
58
|
+
import { _ as _52 } from "./chunks/LpSwitch-v6fnccSM.js";
|
|
59
|
+
import { _ as _53 } from "./chunks/LpTable-DTj52qlg.js";
|
|
60
|
+
import { _ as _54 } from "./chunks/LpTableOfContents-CjvxPMpU.js";
|
|
61
|
+
import { _ as _55 } from "./chunks/LpTabs-Cefe_lys.js";
|
|
62
|
+
import { _ as _56 } from "./chunks/LpTextarea-BDl7PR9U.js";
|
|
62
63
|
import { p as presets, a as parseTheme, s as serializeTheme } from "./chunks/LpThemeSwitcher-B_kj0T2b.js";
|
|
63
|
-
import { D, _ as
|
|
64
|
-
import { _ as
|
|
65
|
-
import { _ as
|
|
66
|
-
import { _ as
|
|
67
|
-
import { _ as
|
|
68
|
-
import { _ as
|
|
64
|
+
import { D, _ as _57, N, T, b, c, d, e, f, g, i, l, h, j, n, r, t, k, m, u, v } from "./chunks/LpThemeSwitcher-B_kj0T2b.js";
|
|
65
|
+
import { _ as _58 } from "./chunks/LpTilt-DuyKbpkA.js";
|
|
66
|
+
import { _ as _59 } from "./chunks/LpToaster-gDB1EaU3.js";
|
|
67
|
+
import { _ as _60 } from "./chunks/LpTooltip-D1hbjQWG.js";
|
|
68
|
+
import { _ as _61 } from "./chunks/LpTopologyCanvas-ebP67CLx.js";
|
|
69
|
+
import { _ as _62 } from "./chunks/LpUptimeBar-CB366k0O.js";
|
|
69
70
|
import { deserializeLayout, serializeLayout } from "./layout/tree.js";
|
|
70
71
|
import { addLeaf, addTab, countBlocks, countLeaves, makeLayout, moveLeaf, removeBlock, removeLeaf, reorderTab, resizeAt, setActiveTab } from "./layout/tree.js";
|
|
71
72
|
import { blockTitle, defineBlocks } from "./layout/registry.js";
|
|
@@ -74,6 +75,7 @@ import { appleModifierLabel, isAppleKeyboard, useHotkeys } from "./composables/u
|
|
|
74
75
|
import { useInputFilter } from "./composables/useInputFilter.js";
|
|
75
76
|
import { useRovingFocus } from "./composables/useRovingFocus.js";
|
|
76
77
|
import { useSettled } from "./composables/useSettled.js";
|
|
78
|
+
import { useShift } from "./composables/useShift.js";
|
|
77
79
|
import { useTilt } from "./composables/useTilt.js";
|
|
78
80
|
import { useZoomPan } from "./composables/useZoomPan.js";
|
|
79
81
|
import { useToast } from "./composables/useToast.js";
|
|
@@ -155,23 +157,24 @@ export {
|
|
|
155
157
|
_43 as LpSegmented,
|
|
156
158
|
_44 as LpSelect,
|
|
157
159
|
_45 as LpServiceNode,
|
|
160
|
+
_46 as LpShift,
|
|
158
161
|
default3 as LpShortcutsDialog,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
162
|
+
_47 as LpSidebar,
|
|
163
|
+
_48 as LpSkeleton,
|
|
164
|
+
_49 as LpSlider,
|
|
165
|
+
_50 as LpStat,
|
|
166
|
+
_51 as LpStepper,
|
|
167
|
+
_52 as LpSwitch,
|
|
168
|
+
_53 as LpTable,
|
|
169
|
+
_54 as LpTableOfContents,
|
|
170
|
+
_55 as LpTabs,
|
|
171
|
+
_56 as LpTextarea,
|
|
172
|
+
_57 as LpThemeSwitcher,
|
|
173
|
+
_58 as LpTilt,
|
|
174
|
+
_59 as LpToaster,
|
|
175
|
+
_60 as LpTooltip,
|
|
176
|
+
_61 as LpTopologyCanvas,
|
|
177
|
+
_62 as LpUptimeBar,
|
|
175
178
|
N as NO_BACKDROP_ATTR,
|
|
176
179
|
OSM_TILES,
|
|
177
180
|
T as THEME_COOKIE,
|
|
@@ -239,6 +242,7 @@ export {
|
|
|
239
242
|
useLayout,
|
|
240
243
|
useRovingFocus,
|
|
241
244
|
useSettled,
|
|
245
|
+
useShift,
|
|
242
246
|
u as useTheme,
|
|
243
247
|
useTilt,
|
|
244
248
|
useToast,
|
package/package.json
CHANGED
package/src/component-names.ts
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* - a default `isActive` (path prefix-match) and a default page title derived
|
|
12
12
|
* from the nav items, so the header title and the active pill never drift.
|
|
13
13
|
*
|
|
14
|
+
* The header bar's two content-driven regions — the title block and the actions
|
|
15
|
+
* cluster — ease between sizes rather than snapping (LpShift). Those regions
|
|
16
|
+
* change constantly in normal use: actions teleport in per route or per section
|
|
17
|
+
* tab, and every label is translated, so their measurements move under the
|
|
18
|
+
* user. Without the easing each change re-lays the bar in a single frame.
|
|
19
|
+
*
|
|
14
20
|
* Everything product-specific goes through slots: #logo (brand in the rail
|
|
15
21
|
* header), #sidebar-actions / #sidebar-footer (pass-through to LpSidebar),
|
|
16
22
|
* #header-actions (right side of the top bar — bell, search, theme switch),
|
|
@@ -18,10 +24,11 @@
|
|
|
18
24
|
* Overlays that aren't part of the frame (command palette, toaster) live in the
|
|
19
25
|
* host next to <LpAppShell>.
|
|
20
26
|
*/
|
|
21
|
-
import { computed,
|
|
27
|
+
import { computed, watch } from "vue"
|
|
22
28
|
import LpButton from "./LpButton.vue"
|
|
23
29
|
import LpIcon from "./LpIcon.vue"
|
|
24
30
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
31
|
+
import LpShift from "./LpShift.vue"
|
|
25
32
|
import LpSidebar from "./LpSidebar.vue"
|
|
26
33
|
import type { SidebarItem, SidebarSection } from "./sidebar"
|
|
27
34
|
|
|
@@ -195,13 +202,26 @@ defineExpose({ openDrawer: _openDrawer })
|
|
|
195
202
|
<LpIcon name="lucide:menu" :size="18" />
|
|
196
203
|
</LpButton>
|
|
197
204
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
<
|
|
205
|
+
<!-- The title block eases its height: a page that adds a subtitle, or a
|
|
206
|
+
translation that wraps a long title onto a second line, otherwise
|
|
207
|
+
re-lays the whole bar in one frame. Width is left free so the
|
|
208
|
+
truncation still resolves against the space actually available. -->
|
|
209
|
+
<LpShift axis="height" class="flex min-w-0 flex-1 items-center">
|
|
210
|
+
<slot name="header-title">
|
|
211
|
+
<h1 class="truncate text-lg font-semibold">{{ pageTitle }}</h1>
|
|
212
|
+
</slot>
|
|
213
|
+
</LpShift>
|
|
214
|
+
|
|
215
|
+
<!-- Actions ease their WIDTH. This is the bar's worst offender: page
|
|
216
|
+
actions arrive by teleport, so a button appears or disappears with
|
|
217
|
+
the route or the section tab, and a translation swaps a short label
|
|
218
|
+
for a long one — each snapping every control to its left sideways.
|
|
219
|
+
Kept mounted regardless of the slot, so a page that only teleports
|
|
220
|
+
into it still gets the easing (an empty box measures zero wide and
|
|
221
|
+
costs nothing). -->
|
|
222
|
+
<LpShift axis="width" class="ml-auto flex shrink-0 items-center gap-2">
|
|
203
223
|
<slot name="header-actions" />
|
|
204
|
-
</
|
|
224
|
+
</LpShift>
|
|
205
225
|
</header>
|
|
206
226
|
|
|
207
227
|
<!-- full-bleed: page owns the whole region (canvas/map); otherwise the
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* Form-control wrapper: label + the control (default slot) + hint/error.
|
|
4
4
|
* Error takes priority over hint. Uses reka Label for proper association.
|
|
5
|
+
*
|
|
6
|
+
* The message line eases its height (LpShift): validation messages arrive
|
|
7
|
+
* and clear as the user types, and each one appearing under the control shoved
|
|
8
|
+
* every field below it down a line. The control itself is left outside the
|
|
9
|
+
* eased box, so the thing being typed into never moves.
|
|
5
10
|
*/
|
|
6
11
|
import { Label } from "reka-ui"
|
|
7
12
|
import { useId } from "vue"
|
|
13
|
+
import LpShift from "./LpShift.vue"
|
|
8
14
|
|
|
9
15
|
defineProps<{
|
|
10
16
|
label?: string
|
|
@@ -17,6 +23,7 @@ const id = useId()
|
|
|
17
23
|
</script>
|
|
18
24
|
|
|
19
25
|
<template>
|
|
26
|
+
<!-- `gap` covers label→control only; the message carries its own spacing. -->
|
|
20
27
|
<div class="flex flex-col gap-1.5">
|
|
21
28
|
<Label v-if="label" :for="id" class="text-sm font-medium text-ink">
|
|
22
29
|
{{ label }}
|
|
@@ -25,7 +32,17 @@ const id = useId()
|
|
|
25
32
|
|
|
26
33
|
<slot :id="id" />
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
<!-- Swapping hint→error changes the text but not the height, so the eased
|
|
36
|
+
box only ever moves when a message appears or clears.
|
|
37
|
+
|
|
38
|
+
The gap that used to separate the message from the control is carried
|
|
39
|
+
INSIDE the eased box as padding, not by the parent's `gap`: a flex gap
|
|
40
|
+
applies to the wrapper whether or not it holds a message, which would
|
|
41
|
+
leave every message-less field a row taller than before — and would be
|
|
42
|
+
a step the easing could not cover, since it exists even at height 0. -->
|
|
43
|
+
<LpShift axis="height">
|
|
44
|
+
<p v-if="error" class="pt-1.5 text-xs text-danger">{{ error }}</p>
|
|
45
|
+
<p v-else-if="hint" class="pt-1.5 text-xs text-muted">{{ hint }}</p>
|
|
46
|
+
</LpShift>
|
|
30
47
|
</div>
|
|
31
48
|
</template>
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
DialogTitle,
|
|
10
10
|
} from "reka-ui"
|
|
11
11
|
import type { ComponentPublicInstance } from "vue"
|
|
12
|
-
import { computed,
|
|
12
|
+
import { computed, ref, useSlots, watch } from "vue"
|
|
13
|
+
import { useShift } from "../composables/useShift"
|
|
13
14
|
import { CLOSE_ICON } from "./dropdown"
|
|
14
15
|
import LpIcon from "./LpIcon.vue"
|
|
15
16
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
@@ -86,20 +87,14 @@ const bodyPad = computed(() =>
|
|
|
86
87
|
|
|
87
88
|
/*
|
|
88
89
|
* Height is content-derived, so a body that arrives late — a placeholder swapped
|
|
89
|
-
* for the loaded thing —
|
|
90
|
-
* the open animation.
|
|
91
|
-
* interpolable, and the keyword-interpolation escape hatches
|
|
92
|
-
* (`interpolate-size`, `calc-size()`) are Chromium-only, so they would do
|
|
93
|
-
* nothing in the WebKitGTK launcher. Measuring and pinning a pixel height is
|
|
94
|
-
* what works on every engine.
|
|
90
|
+
* for the loaded thing — would resize the panel in a single frame, right on top
|
|
91
|
+
* of the open animation. The easing is set up further down.
|
|
95
92
|
*
|
|
96
93
|
* The pin is always the panel's own natural height, so `max-h` still clips and
|
|
97
94
|
* the body keeps `flex-1 min-h-0` — an over-tall panel hands its overflow to the
|
|
98
95
|
* scroll area exactly as before. A panel whose size never moves is pinned once
|
|
99
96
|
* and never transitions.
|
|
100
97
|
*/
|
|
101
|
-
// reka renders a real element but exposes it as a component instance, so the
|
|
102
|
-
// panel is reached through $el.
|
|
103
98
|
const panelRef = ref<ComponentPublicInstance | null>(null)
|
|
104
99
|
|
|
105
100
|
/**
|
|
@@ -156,130 +151,27 @@ function onOpenAutoFocus(event: Event) {
|
|
|
156
151
|
return
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
/** Where the panel last settled, so a retune has a height to ease FROM. */
|
|
170
|
-
let lastHeight = 0
|
|
171
|
-
/** True while a tween owns the height, so observers don't cancel it midway. */
|
|
172
|
-
let inFlight = false
|
|
173
|
-
|
|
174
|
-
// The height is driven imperatively rather than through a style binding. By the
|
|
175
|
-
// time replacement content is in the DOM the panel has already reflowed to its
|
|
176
|
-
// new size, so a bound value would only ever be written once, with no earlier
|
|
177
|
-
// height for the transition to start from. Writing the OLD height, forcing a
|
|
178
|
-
// reflow, then writing the new one gives the transition both ends.
|
|
179
|
-
const retune = (el: HTMLElement) => {
|
|
180
|
-
// One swap wakes both observers — the mutation as content lands, the resize as
|
|
181
|
-
// it reflows — and the second call would clear the pin the first just wrote.
|
|
182
|
-
if (inFlight) return
|
|
183
|
-
// Remembered, not measured: by the time either observer fires the panel has
|
|
184
|
-
// already reflowed, so the element can only report where it is going.
|
|
185
|
-
const from = lastHeight
|
|
186
|
-
el.style.height = ""
|
|
187
|
-
// `max-h` comes from a class, so the panel already reports the clipped figure
|
|
188
|
-
// once the cap bites. Pinning that would leave a short panel sitting over
|
|
189
|
-
// content it can no longer grow to fit, so the natural height is measured with
|
|
190
|
-
// the cap lifted and the clamp is left to CSS afterwards.
|
|
191
|
-
el.style.maxHeight = "none"
|
|
192
|
-
const to = el.offsetHeight
|
|
193
|
-
el.style.maxHeight = ""
|
|
194
|
-
lastHeight = to
|
|
195
|
-
// Nothing moved, so there is nothing to ease — and leaving the height unset
|
|
196
|
-
// keeps the panel free to size itself.
|
|
197
|
-
if (to === 0 || from === 0 || to === from) return
|
|
198
|
-
el.style.height = `${from}px`
|
|
199
|
-
void el.offsetHeight
|
|
200
|
-
el.style.height = `${to}px`
|
|
201
|
-
|
|
202
|
-
inFlight = true
|
|
203
|
-
// While the panel is still catching up, the body is shorter than the content
|
|
204
|
-
// it already holds, so the scroll area would flash a bar for an overflow that
|
|
205
|
-
// resolves itself the moment the transition lands.
|
|
206
|
-
resizing.value = true
|
|
207
|
-
clearTimeout(settleTimer)
|
|
208
|
-
settleTimer = setTimeout(() => {
|
|
209
|
-
inFlight = false
|
|
210
|
-
resizing.value = false
|
|
211
|
-
// Released once the panel has arrived. A height left pinned in pixels stops
|
|
212
|
-
// being a starting point and becomes a cap: content that grows afterwards
|
|
213
|
-
// is clipped, and `max-h` can no longer size the panel itself.
|
|
214
|
-
el.style.height = ""
|
|
215
|
-
// Re-read rather than trusting `to`: `max-h` may have clamped the panel
|
|
216
|
-
// short of it, and the next tween has to start from where it really sits.
|
|
217
|
-
lastHeight = el.offsetHeight
|
|
218
|
-
}, TWEEN_MS)
|
|
219
|
-
}
|
|
154
|
+
// The panel's height eases between content-derived sizes — see
|
|
155
|
+
// `useShift`, which owns the measure/pin/tween mechanism and is shared
|
|
156
|
+
// with LpShift. Gated on `open`, because the panel is portalled and remounts
|
|
157
|
+
// on every open: a remount must measure afresh rather than ease in from the
|
|
158
|
+
// size the previous incarnation happened to hold.
|
|
159
|
+
const {
|
|
160
|
+
el: sizedEl,
|
|
161
|
+
tweening,
|
|
162
|
+
resizing,
|
|
163
|
+
} = useShift({ axis: "height", enabled: () => props.open })
|
|
220
164
|
|
|
221
|
-
//
|
|
222
|
-
//
|
|
165
|
+
// reka renders a real element but exposes it as a component instance, so the
|
|
166
|
+
// sized element is handed over through $el once the panel has mounted.
|
|
223
167
|
watch(
|
|
224
|
-
() => props.open,
|
|
225
|
-
(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
ro = undefined
|
|
229
|
-
mo = undefined
|
|
230
|
-
cancelAnimationFrame(raf)
|
|
231
|
-
clearTimeout(settleTimer)
|
|
232
|
-
tweening.value = false
|
|
233
|
-
resizing.value = false
|
|
234
|
-
|
|
235
|
-
lastHeight = 0
|
|
236
|
-
inFlight = false
|
|
237
|
-
|
|
238
|
-
const el = panelRef.value?.$el
|
|
239
|
-
if (!isOpen || !(el instanceof HTMLElement)) return
|
|
240
|
-
|
|
241
|
-
lastHeight = el.offsetHeight
|
|
242
|
-
el.style.height = `${el.offsetHeight}px`
|
|
243
|
-
// The opening panel must not animate its height in from the pre-content box,
|
|
244
|
-
// so the transition only arms once that first pin has painted. The pin is
|
|
245
|
-
// dropped in the same frame: it exists to give the first `retune` something
|
|
246
|
-
// to start from, and holding it would cap the panel at its opening size.
|
|
247
|
-
raf = requestAnimationFrame(() => {
|
|
248
|
-
tweening.value = true
|
|
249
|
-
el.style.height = ""
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
// Two triggers, since either can move the height alone: the resize observer
|
|
253
|
-
// catches a child growing in place (an image decoding, a list filling in),
|
|
254
|
-
// the mutation observer catches one subtree swapped for another, where every
|
|
255
|
-
// observed box may keep its size. Children are observed, never the panel —
|
|
256
|
-
// that would feed the pin straight back in.
|
|
257
|
-
const watchChildren = () => {
|
|
258
|
-
if (!ro) return
|
|
259
|
-
ro.disconnect()
|
|
260
|
-
for (const child of Array.from(el.children)) ro.observe(child)
|
|
261
|
-
}
|
|
262
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
263
|
-
ro = new ResizeObserver(() => retune(el))
|
|
264
|
-
watchChildren()
|
|
265
|
-
}
|
|
266
|
-
if (typeof MutationObserver !== "undefined") {
|
|
267
|
-
mo = new MutationObserver(() => {
|
|
268
|
-
watchChildren()
|
|
269
|
-
retune(el)
|
|
270
|
-
})
|
|
271
|
-
mo.observe(el, { childList: true, subtree: true, characterData: true })
|
|
272
|
-
}
|
|
168
|
+
() => (props.open ? panelRef.value : null),
|
|
169
|
+
(panel) => {
|
|
170
|
+
const el = panel?.$el
|
|
171
|
+
sizedEl.value = el instanceof HTMLElement ? el : null
|
|
273
172
|
},
|
|
274
173
|
{ flush: "post" },
|
|
275
174
|
)
|
|
276
|
-
|
|
277
|
-
onBeforeUnmount(() => {
|
|
278
|
-
ro?.disconnect()
|
|
279
|
-
mo?.disconnect()
|
|
280
|
-
cancelAnimationFrame(raf)
|
|
281
|
-
clearTimeout(settleTimer)
|
|
282
|
-
})
|
|
283
175
|
</script>
|
|
284
176
|
|
|
285
177
|
<template>
|