@leavepulse/ui 0.37.0 → 0.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{LpAppShell-lVvpENMH.js → LpAppShell-Cs9jDoqJ.js} +1 -1
- package/dist/chunks/{LpDatePicker-6JFXllj5.js → LpDatePicker-BPbvu3cZ.js} +49 -8
- package/dist/chunks/{LpDrawer-DAJN5rGK.js → LpDrawer-CslZtgUm.js} +10 -3
- package/dist/chunks/{LpModalNav-K6N5YpQk.js → LpModalNav-Dkogc3O_.js} +1 -1
- package/dist/chunks/{LpSidebar-DcYavim2.js → LpSidebar-DvGfofv6.js} +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.d.ts +8 -0
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/modalDepth.js +1 -1
- package/dist/composables/useShift.js +19 -0
- package/dist/index.js +5 -5
- package/package.json +1 -1
- package/src/components/LpDatePicker.vue +61 -5
- package/src/components/LpDrawer.vue +14 -2
- package/src/components/modalDepth.ts +8 -1
- package/src/composables/useShift.ts +28 -0
|
@@ -3,7 +3,7 @@ import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
|
|
|
3
3
|
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
4
4
|
import { _ as _sfc_main$5 } from "./LpScrollArea-D8xyKXcF.js";
|
|
5
5
|
import { _ as _sfc_main$4 } from "./LpShift-DXNsauQZ.js";
|
|
6
|
-
import { _ as _sfc_main$1 } from "./LpSidebar-
|
|
6
|
+
import { _ as _sfc_main$1 } from "./LpSidebar-DvGfofv6.js";
|
|
7
7
|
import { useMergedAttrs } from "../composables/useMergedClass.js";
|
|
8
8
|
const _hoisted_1 = { class: "flex min-w-0 flex-1 flex-col" };
|
|
9
9
|
const _hoisted_2 = { class: "relative flex h-16 shrink-0 items-center gap-3 border-b border-line bg-surface px-4 md:px-6" };
|
|
@@ -5,6 +5,11 @@ import { _ as _sfc_main$2 } from "./LpCalendar-B7Ko5RdR.js";
|
|
|
5
5
|
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
6
6
|
const _hoisted_1 = ["disabled"];
|
|
7
7
|
const _hoisted_2 = ["onKeydown"];
|
|
8
|
+
const _hoisted_3 = {
|
|
9
|
+
key: 0,
|
|
10
|
+
class: "flex items-center gap-2 border-t border-line px-3 py-2"
|
|
11
|
+
};
|
|
12
|
+
const _hoisted_4 = ["value", "disabled"];
|
|
8
13
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
9
14
|
__name: "LpDatePicker",
|
|
10
15
|
props: {
|
|
@@ -17,18 +22,27 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
17
22
|
disabled: { type: Boolean },
|
|
18
23
|
invalid: { type: Boolean },
|
|
19
24
|
size: { default: "md" },
|
|
20
|
-
format: { default: () => ({ year: "numeric", month: "short", day: "numeric" }) }
|
|
25
|
+
format: { default: () => ({ year: "numeric", month: "short", day: "numeric" }) },
|
|
26
|
+
time: { type: Boolean },
|
|
27
|
+
defaultTime: { default: "00:00" },
|
|
28
|
+
doneLabel: { default: "Done" }
|
|
21
29
|
},
|
|
22
30
|
emits: ["update:modelValue"],
|
|
23
31
|
setup(__props, { emit: __emit }) {
|
|
24
32
|
const props = __props;
|
|
25
33
|
const emit = __emit;
|
|
26
34
|
const open = ref(false);
|
|
35
|
+
const day = computed(() => (props.modelValue || "").slice(0, 10));
|
|
36
|
+
const minute = computed(() => {
|
|
37
|
+
const at = (props.modelValue || "").slice(11, 16);
|
|
38
|
+
return /^\d{2}:\d{2}$/.test(at) ? at : "";
|
|
39
|
+
});
|
|
27
40
|
const display = computed(() => {
|
|
28
41
|
if (!props.modelValue) return "";
|
|
29
|
-
const [y, m, d] =
|
|
42
|
+
const [y, m, d] = day.value.split("-").map(Number);
|
|
30
43
|
if (!y || !m || !d) return props.modelValue;
|
|
31
|
-
|
|
44
|
+
const shown = new Intl.DateTimeFormat(void 0, props.format).format(new Date(y, m - 1, d));
|
|
45
|
+
return props.time && minute.value ? `${shown}, ${minute.value}` : shown;
|
|
32
46
|
});
|
|
33
47
|
const shellSize = {
|
|
34
48
|
sm: "h-(--size-control-sm) text-xs",
|
|
@@ -36,8 +50,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
36
50
|
lg: "h-(--size-control-lg) text-sm"
|
|
37
51
|
};
|
|
38
52
|
function onPick(v) {
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
if (!props.time) {
|
|
54
|
+
emit("update:modelValue", v);
|
|
55
|
+
open.value = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
emit("update:modelValue", v ? `${v}T${minute.value || props.defaultTime}` : void 0);
|
|
59
|
+
}
|
|
60
|
+
function onTime(at) {
|
|
61
|
+
if (!day.value) return;
|
|
62
|
+
emit("update:modelValue", `${day.value}T${at || props.defaultTime}`);
|
|
41
63
|
}
|
|
42
64
|
function clear() {
|
|
43
65
|
emit("update:modelValue", void 0);
|
|
@@ -45,7 +67,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
45
67
|
return (_ctx, _cache) => {
|
|
46
68
|
return openBlock(), createBlock(unref(PopoverRoot), {
|
|
47
69
|
open: open.value,
|
|
48
|
-
"onUpdate:open": _cache[
|
|
70
|
+
"onUpdate:open": _cache[2] || (_cache[2] = ($event) => open.value = $event)
|
|
49
71
|
}, {
|
|
50
72
|
default: withCtx(() => [
|
|
51
73
|
createVNode(unref(PopoverTrigger), { "as-child": "" }, {
|
|
@@ -98,13 +120,32 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
98
120
|
}, {
|
|
99
121
|
default: withCtx(() => [
|
|
100
122
|
createVNode(_sfc_main$2, {
|
|
101
|
-
"model-value":
|
|
123
|
+
"model-value": day.value,
|
|
102
124
|
min: __props.min,
|
|
103
125
|
max: __props.max,
|
|
104
126
|
"is-disabled": __props.isDisabled,
|
|
105
127
|
class: "border-0 bg-transparent",
|
|
106
128
|
"onUpdate:modelValue": onPick
|
|
107
|
-
}, null, 8, ["model-value", "min", "max", "is-disabled"])
|
|
129
|
+
}, null, 8, ["model-value", "min", "max", "is-disabled"]),
|
|
130
|
+
__props.time ? (openBlock(), createElementBlock("div", _hoisted_3, [
|
|
131
|
+
createVNode(_sfc_main$1, {
|
|
132
|
+
name: "lucide:clock",
|
|
133
|
+
size: 15,
|
|
134
|
+
class: "shrink-0 text-muted"
|
|
135
|
+
}),
|
|
136
|
+
createElementVNode("input", {
|
|
137
|
+
type: "time",
|
|
138
|
+
value: minute.value,
|
|
139
|
+
disabled: !day.value,
|
|
140
|
+
class: "h-(--size-control-sm) flex-1 rounded-control border border-line bg-surface-soft px-2 text-sm text-ink outline-none transition-colors focus-visible:border-brand focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-55",
|
|
141
|
+
onInput: _cache[0] || (_cache[0] = ($event) => onTime($event.target.value))
|
|
142
|
+
}, null, 40, _hoisted_4),
|
|
143
|
+
createElementVNode("button", {
|
|
144
|
+
type: "button",
|
|
145
|
+
class: "shrink-0 rounded-control px-2 py-1 text-xs text-muted transition-colors hover:text-ink",
|
|
146
|
+
onClick: _cache[1] || (_cache[1] = ($event) => open.value = false)
|
|
147
|
+
}, toDisplayString(__props.doneLabel), 1)
|
|
148
|
+
])) : createCommentVNode("", true)
|
|
108
149
|
]),
|
|
109
150
|
_: 1
|
|
110
151
|
}, 8, ["class"])
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { defineComponent, computed, ref, useSlots, openBlock, createElementBlock, Fragment, createElementVNode, normalizeStyle, normalizeClass, withDirectives, vShow, toDisplayString, createCommentVNode, createVNode, unref, withCtx, createBlock, renderSlot, createTextVNode } from "vue";
|
|
1
|
+
import { defineComponent, computed, watch, onBeforeUnmount, ref, useSlots, openBlock, createElementBlock, Fragment, createElementVNode, normalizeStyle, normalizeClass, withDirectives, vShow, toDisplayString, createCommentVNode, createVNode, unref, withCtx, createBlock, renderSlot, createTextVNode } from "vue";
|
|
2
2
|
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
6
|
import { _ as _sfc_main$2 } from "./LpScrollArea-D8xyKXcF.js";
|
|
7
|
+
import { useModalLayer } from "../components/modalDepth.js";
|
|
7
8
|
const _hoisted_1 = { class: "text-base font-semibold text-ink" };
|
|
8
9
|
const _hoisted_2 = { class: "flex flex-col gap-1" };
|
|
9
10
|
const _hoisted_3 = ["data-vaul-no-drag"];
|
|
@@ -47,6 +48,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
47
48
|
const isSheet = computed(() => dir.value === "top" || dir.value === "bottom");
|
|
48
49
|
const showHandle = computed(() => props.handle ?? isSheet.value);
|
|
49
50
|
const SIZES = { sm: 22, md: 28, lg: 36, xl: 48 };
|
|
51
|
+
const { claim, release, layer } = useModalLayer();
|
|
52
|
+
watch(() => props.open, (open) => open ? claim() : release(), { immediate: true });
|
|
53
|
+
onBeforeUnmount(release);
|
|
50
54
|
const contentClass = computed(() => {
|
|
51
55
|
const base = "fixed z-(--z-modal) flex flex-col border-line bg-surface-raised shadow-panel outline-none";
|
|
52
56
|
const map = {
|
|
@@ -215,10 +219,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
215
219
|
default: withCtx(() => [
|
|
216
220
|
createVNode(unref(DrawerPortal), null, {
|
|
217
221
|
default: withCtx(() => [
|
|
218
|
-
createVNode(unref(DrawerOverlay), {
|
|
222
|
+
createVNode(unref(DrawerOverlay), {
|
|
223
|
+
style: normalizeStyle({ zIndex: unref(layer).scrim }),
|
|
224
|
+
class: "lp-scrim fixed inset-0 z-(--z-overlay) data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
|
|
225
|
+
}, null, 8, ["style"]),
|
|
219
226
|
createVNode(unref(DrawerContent), {
|
|
220
227
|
class: normalizeClass(contentClass.value),
|
|
221
|
-
style: normalizeStyle(sizeStyle.value),
|
|
228
|
+
style: normalizeStyle([sizeStyle.value, { zIndex: unref(layer).panel }]),
|
|
222
229
|
"data-vaul-animate": handingOff.value ? "false" : void 0
|
|
223
230
|
}, {
|
|
224
231
|
default: withCtx(() => [
|
|
@@ -2,7 +2,7 @@ import { defineComponent, ref, computed, openBlock, createBlock, createSlots, wi
|
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
|
|
3
3
|
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
4
4
|
import { _ as _sfc_main$1 } from "./LpModal-D3QxgWaT.js";
|
|
5
|
-
import { _ as _sfc_main$4 } from "./LpSidebar-
|
|
5
|
+
import { _ as _sfc_main$4 } from "./LpSidebar-DvGfofv6.js";
|
|
6
6
|
const _hoisted_1 = { class: "flex items-center gap-2" };
|
|
7
7
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
8
8
|
__name: "LpModalNav",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent, computed, ref, useId, openBlock, createElementBlock, Fragment, createElementVNode, mergeProps, normalizeClass, renderSlot, createCommentVNode, createBlock, createVNode, createSlots, withCtx, normalizeProps, guardReactiveProps } from "vue";
|
|
2
|
-
import { _ as _sfc_main$3 } from "./LpDrawer-
|
|
2
|
+
import { _ as _sfc_main$3 } from "./LpDrawer-CslZtgUm.js";
|
|
3
3
|
import { _ as _sfc_main$1 } from "./LpInput-9EXWUM9B.js";
|
|
4
4
|
import { _ as _sfc_main$2 } from "./LpSidebarNav-CqwvT4H8.js";
|
|
5
5
|
const _hoisted_1 = {
|
|
@@ -11,6 +11,12 @@ type __VLS_Props = {
|
|
|
11
11
|
size?: "sm" | "md" | "lg";
|
|
12
12
|
/** Intl format for the displayed date. */
|
|
13
13
|
format?: Intl.DateTimeFormatOptions;
|
|
14
|
+
/** Also pick a minute; the model becomes "YYYY-MM-DDTHH:mm". */
|
|
15
|
+
time?: boolean;
|
|
16
|
+
/** Time a freshly picked day starts at, "HH:mm". */
|
|
17
|
+
defaultTime?: string;
|
|
18
|
+
/** Label of the button that closes the popover in `time` mode. */
|
|
19
|
+
doneLabel?: string;
|
|
14
20
|
};
|
|
15
21
|
declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
|
|
16
22
|
"update:modelValue": (value: string | undefined) => any;
|
|
@@ -20,6 +26,8 @@ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {
|
|
|
20
26
|
size: "sm" | "md" | "lg";
|
|
21
27
|
format: Intl.DateTimeFormatOptions;
|
|
22
28
|
placeholder: string;
|
|
29
|
+
defaultTime: string;
|
|
30
|
+
doneLabel: string;
|
|
23
31
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
24
32
|
declare const _default: typeof __VLS_export;
|
|
25
33
|
export default _default;
|
|
@@ -14,6 +14,17 @@ function useShift(options = {}) {
|
|
|
14
14
|
let lastWidth = 0;
|
|
15
15
|
let inFlight = false;
|
|
16
16
|
let tweenStart = 0;
|
|
17
|
+
const scrolled = /* @__PURE__ */ new Map();
|
|
18
|
+
const rememberScroll = (event) => {
|
|
19
|
+
const target = event.target;
|
|
20
|
+
if (target instanceof Element) scrolled.set(target, target.scrollTop);
|
|
21
|
+
};
|
|
22
|
+
function restoreScroll() {
|
|
23
|
+
for (const [node, top] of scrolled) {
|
|
24
|
+
if (!node.isConnected) scrolled.delete(node);
|
|
25
|
+
else if (node.scrollTop !== top) node.scrollTop = top;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
17
28
|
const axisOf = () => toValue(options.axis) ?? "height";
|
|
18
29
|
const wantsHeight = () => axisOf() !== "width";
|
|
19
30
|
const wantsWidth = () => axisOf() !== "height";
|
|
@@ -40,6 +51,7 @@ function useShift(options = {}) {
|
|
|
40
51
|
const toWidth = wantsWidth() ? node.offsetWidth : 0;
|
|
41
52
|
if (wantsHeight()) node.style.maxHeight = "";
|
|
42
53
|
if (wantsWidth()) node.style.maxWidth = "";
|
|
54
|
+
restoreScroll();
|
|
43
55
|
lastHeight = toHeight;
|
|
44
56
|
lastWidth = toWidth;
|
|
45
57
|
const movesHeight = wantsHeight() && fromHeight !== 0 && toHeight !== fromHeight;
|
|
@@ -50,6 +62,7 @@ function useShift(options = {}) {
|
|
|
50
62
|
void node.offsetHeight;
|
|
51
63
|
if (movesHeight) node.style.height = `${toHeight}px`;
|
|
52
64
|
if (movesWidth) node.style.width = `${toWidth}px`;
|
|
65
|
+
restoreScroll();
|
|
53
66
|
inFlight = true;
|
|
54
67
|
tweenStart = performance.now();
|
|
55
68
|
resizing.value = true;
|
|
@@ -64,7 +77,11 @@ function useShift(options = {}) {
|
|
|
64
77
|
lastWidth = node.offsetWidth;
|
|
65
78
|
}, wait);
|
|
66
79
|
}
|
|
80
|
+
let scrollHost = null;
|
|
67
81
|
function teardown() {
|
|
82
|
+
scrollHost?.removeEventListener("scroll", rememberScroll, true);
|
|
83
|
+
scrollHost = null;
|
|
84
|
+
scrolled.clear();
|
|
68
85
|
ro?.disconnect();
|
|
69
86
|
mo?.disconnect();
|
|
70
87
|
ro = void 0;
|
|
@@ -84,6 +101,8 @@ function useShift(options = {}) {
|
|
|
84
101
|
if (!enabled || !node) return;
|
|
85
102
|
lastHeight = node.offsetHeight;
|
|
86
103
|
lastWidth = node.offsetWidth;
|
|
104
|
+
scrollHost = node;
|
|
105
|
+
node.addEventListener("scroll", rememberScroll, { capture: true, passive: true });
|
|
87
106
|
if (wantsHeight()) node.style.height = `${lastHeight}px`;
|
|
88
107
|
if (wantsWidth()) node.style.width = `${lastWidth}px`;
|
|
89
108
|
raf = requestAnimationFrame(() => {
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { _ as _3 } from "./chunks/LpMapPicker-ul4oYno6.js";
|
|
|
4
4
|
import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
|
|
5
5
|
import { _ as _4 } from "./chunks/LpActionBar-BlvtGB2P.js";
|
|
6
6
|
import { _ as _5 } from "./chunks/LpAlert-BAYHDSS1.js";
|
|
7
|
-
import { _ as _6 } from "./chunks/LpAppShell-
|
|
7
|
+
import { _ as _6 } from "./chunks/LpAppShell-Cs9jDoqJ.js";
|
|
8
8
|
import { _ as _7 } from "./chunks/LpAutocomplete-DExlIZe_.js";
|
|
9
9
|
import { _ as _8 } from "./chunks/LpAvatar-D3l4nabg.js";
|
|
10
10
|
import { _ as _9 } from "./chunks/LpBadge-Fe0D1bYx.js";
|
|
@@ -21,10 +21,10 @@ import { DEFAULT_SWATCHES, clampHsv, formatColor, hsvToHsl, hsvToRgb, isLight, n
|
|
|
21
21
|
import { default as default3 } from "./components/LpCommandPalette.vue.js";
|
|
22
22
|
import { _ as _17 } from "./chunks/LpConfirmDialog-B6RAvDES.js";
|
|
23
23
|
import { _ as _18 } from "./chunks/LpContextMenu-B6WBLleS.js";
|
|
24
|
-
import { _ as _19 } from "./chunks/LpDatePicker-
|
|
24
|
+
import { _ as _19 } from "./chunks/LpDatePicker-BPbvu3cZ.js";
|
|
25
25
|
import { _ as _20 } from "./chunks/LpDisclosure-ChQYmjlR.js";
|
|
26
26
|
import { _ as _21 } from "./chunks/LpDivider-CUX46KR0.js";
|
|
27
|
-
import { _ as _22 } from "./chunks/LpDrawer-
|
|
27
|
+
import { _ as _22 } from "./chunks/LpDrawer-CslZtgUm.js";
|
|
28
28
|
import { _ as _23 } from "./chunks/LpDropdownMenu-DJpoqRCb.js";
|
|
29
29
|
import { default as default4 } from "./components/LpDropzone.vue.js";
|
|
30
30
|
import { _ as _24 } from "./chunks/LpEmptyState-Dsdqz1Wj.js";
|
|
@@ -41,7 +41,7 @@ import { _ as _31 } from "./chunks/LpLink-DCND-zyu.js";
|
|
|
41
41
|
import { _ as _32 } from "./chunks/LpLogViewer-y53ZqSVq.js";
|
|
42
42
|
import { _ as _33 } from "./chunks/LpMediaGallery-Cid7PGYx.js";
|
|
43
43
|
import { _ as _34 } from "./chunks/LpModal-D3QxgWaT.js";
|
|
44
|
-
import { _ as _35 } from "./chunks/LpModalNav-
|
|
44
|
+
import { _ as _35 } from "./chunks/LpModalNav-Dkogc3O_.js";
|
|
45
45
|
import { _ as _36 } from "./chunks/LpNotificationBell-lHePf89W.js";
|
|
46
46
|
import { _ as _37 } from "./chunks/LpNumberField-CWuX08iX.js";
|
|
47
47
|
import { _ as _38 } from "./chunks/LpNumberFlow-amWYaeuA.js";
|
|
@@ -63,7 +63,7 @@ import { _ as _52 } from "./chunks/LpSelect-ANIVtrpM.js";
|
|
|
63
63
|
import { _ as _53 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
|
|
64
64
|
import { _ as _54 } from "./chunks/LpShift-DXNsauQZ.js";
|
|
65
65
|
import { default as default6 } from "./components/LpShortcutsDialog.vue.js";
|
|
66
|
-
import { _ as _55 } from "./chunks/LpSidebar-
|
|
66
|
+
import { _ as _55 } from "./chunks/LpSidebar-DvGfofv6.js";
|
|
67
67
|
import { _ as _56 } from "./chunks/LpSkeleton-CTI-vBgW.js";
|
|
68
68
|
import { _ as _57 } from "./chunks/LpSlider-BwNIBkAK.js";
|
|
69
69
|
import { _ as _58 } from "./chunks/LpSparkline-C6xQ2dwv.js";
|
package/package.json
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* v-model), same as LpCalendar (which it embeds). The trigger shows a formatted
|
|
5
5
|
* date; picking a day fills the field and closes the popover. Clearable, with
|
|
6
6
|
* min/max + isDisabled forwarded to the calendar. Themed like the other inputs.
|
|
7
|
+
*
|
|
8
|
+
* With `time`, the model carries a minute too ("YYYY-MM-DDTHH:mm") and the
|
|
9
|
+
* popover grows a time row under the calendar. Anything that needs a moment
|
|
10
|
+
* rather than a day — an expiry, a scheduled start — would otherwise fall back
|
|
11
|
+
* to a native `datetime-local`, which the browser paints in its own locale and
|
|
12
|
+
* chrome, breaking the form it sits in. Picking a day keeps the time already
|
|
13
|
+
* chosen (default `defaultTime`), so the popover closes on the first click for
|
|
14
|
+
* the common case.
|
|
7
15
|
*/
|
|
8
16
|
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from "reka-ui"
|
|
9
17
|
import { computed, ref } from "vue"
|
|
@@ -25,11 +33,19 @@ const props = withDefaults(
|
|
|
25
33
|
size?: "sm" | "md" | "lg"
|
|
26
34
|
/** Intl format for the displayed date. */
|
|
27
35
|
format?: Intl.DateTimeFormatOptions
|
|
36
|
+
/** Also pick a minute; the model becomes "YYYY-MM-DDTHH:mm". */
|
|
37
|
+
time?: boolean
|
|
38
|
+
/** Time a freshly picked day starts at, "HH:mm". */
|
|
39
|
+
defaultTime?: string
|
|
40
|
+
/** Label of the button that closes the popover in `time` mode. */
|
|
41
|
+
doneLabel?: string
|
|
28
42
|
}>(),
|
|
29
43
|
{
|
|
30
44
|
placeholder: "Pick a date",
|
|
31
45
|
size: "md",
|
|
32
46
|
format: () => ({ year: "numeric", month: "short", day: "numeric" }),
|
|
47
|
+
defaultTime: "00:00",
|
|
48
|
+
doneLabel: "Done",
|
|
33
49
|
},
|
|
34
50
|
)
|
|
35
51
|
|
|
@@ -39,12 +55,22 @@ const emit = defineEmits<{
|
|
|
39
55
|
|
|
40
56
|
const open = ref(false)
|
|
41
57
|
|
|
58
|
+
/** Day half of the model; the calendar never sees the time. */
|
|
59
|
+
const day = computed(() => (props.modelValue || "").slice(0, 10))
|
|
60
|
+
|
|
61
|
+
/** Minute half, "HH:mm" — empty when the model carries a date only. */
|
|
62
|
+
const minute = computed(() => {
|
|
63
|
+
const at = (props.modelValue || "").slice(11, 16)
|
|
64
|
+
return /^\d{2}:\d{2}$/.test(at) ? at : ""
|
|
65
|
+
})
|
|
66
|
+
|
|
42
67
|
const display = computed(() => {
|
|
43
68
|
if (!props.modelValue) return ""
|
|
44
69
|
// Parse as local midnight so the formatted day can't drift across a timezone.
|
|
45
|
-
const [y, m, d] =
|
|
70
|
+
const [y, m, d] = day.value.split("-").map(Number)
|
|
46
71
|
if (!y || !m || !d) return props.modelValue
|
|
47
|
-
|
|
72
|
+
const shown = new Intl.DateTimeFormat(undefined, props.format).format(new Date(y, m - 1, d))
|
|
73
|
+
return props.time && minute.value ? `${shown}, ${minute.value}` : shown
|
|
48
74
|
})
|
|
49
75
|
|
|
50
76
|
const shellSize = {
|
|
@@ -54,8 +80,21 @@ const shellSize = {
|
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
function onPick(v: string | undefined) {
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
if (!props.time) {
|
|
84
|
+
emit("update:modelValue", v)
|
|
85
|
+
open.value = false
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
// The popover stays open: the time row below is part of the same choice, and
|
|
89
|
+
// closing on the day would hide it right as it becomes reachable.
|
|
90
|
+
emit("update:modelValue", v ? `${v}T${minute.value || props.defaultTime}` : undefined)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function onTime(at: string) {
|
|
94
|
+
// A time without a day would be a model no consumer can use; until a day is
|
|
95
|
+
// picked the row is disabled, so this only guards a stray event.
|
|
96
|
+
if (!day.value) return
|
|
97
|
+
emit("update:modelValue", `${day.value}T${at || props.defaultTime}`)
|
|
59
98
|
}
|
|
60
99
|
|
|
61
100
|
function clear() {
|
|
@@ -104,13 +143,30 @@ function clear() {
|
|
|
104
143
|
:class="[POPOVER_PANEL, 'z-(--z-popover) rounded-card p-0 outline-none']"
|
|
105
144
|
>
|
|
106
145
|
<LpCalendar
|
|
107
|
-
:model-value="
|
|
146
|
+
:model-value="day"
|
|
108
147
|
:min="min"
|
|
109
148
|
:max="max"
|
|
110
149
|
:is-disabled="isDisabled"
|
|
111
150
|
class="border-0 bg-transparent"
|
|
112
151
|
@update:model-value="onPick"
|
|
113
152
|
/>
|
|
153
|
+
<div v-if="time" class="flex items-center gap-2 border-t border-line px-3 py-2">
|
|
154
|
+
<LpIcon name="lucide:clock" :size="15" class="shrink-0 text-muted" />
|
|
155
|
+
<input
|
|
156
|
+
type="time"
|
|
157
|
+
:value="minute"
|
|
158
|
+
:disabled="!day"
|
|
159
|
+
class="h-(--size-control-sm) flex-1 rounded-control border border-line bg-surface-soft px-2 text-sm text-ink outline-none transition-colors focus-visible:border-brand focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-55"
|
|
160
|
+
@input="onTime(($event.target as HTMLInputElement).value)"
|
|
161
|
+
/>
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
class="shrink-0 rounded-control px-2 py-1 text-xs text-muted transition-colors hover:text-ink"
|
|
165
|
+
@click="open = false"
|
|
166
|
+
>
|
|
167
|
+
{{ doneLabel }}
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
114
170
|
</PopoverContent>
|
|
115
171
|
</PopoverPortal>
|
|
116
172
|
</PopoverRoot>
|
|
@@ -23,10 +23,11 @@ import {
|
|
|
23
23
|
DrawerRoot,
|
|
24
24
|
DrawerTitle,
|
|
25
25
|
} from "vaul-vue"
|
|
26
|
-
import { computed, ref, useSlots } from "vue"
|
|
26
|
+
import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
|
|
27
27
|
import { CLOSE_ICON } from "./dropdown"
|
|
28
28
|
import LpIcon from "./LpIcon.vue"
|
|
29
29
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
30
|
+
import { useModalLayer } from "./modalDepth"
|
|
30
31
|
|
|
31
32
|
type Direction = "top" | "bottom" | "left" | "right"
|
|
32
33
|
|
|
@@ -125,6 +126,16 @@ const SIZES = { sm: 22, md: 28, lg: 36, xl: 48 } // rem along the main axis
|
|
|
125
126
|
// Geometry per edge. The cross-axis is pinned to the viewport; the main axis
|
|
126
127
|
// takes the size preset. vaul drives the open/close transform itself, so we
|
|
127
128
|
// don't add slide keyframes — only the resting box + rounding toward the centre.
|
|
129
|
+
/*
|
|
130
|
+
* The drawer joins the same stack as LpModal. It painted at the fixed
|
|
131
|
+
* --z-overlay/--z-modal rungs, so a dialog opened from it (a picker inside a
|
|
132
|
+
* settings drawer) claimed those same rungs and the tie went to DOM order —
|
|
133
|
+
* the drawer, portalled later on reopen, covered the dialog it had opened.
|
|
134
|
+
*/
|
|
135
|
+
const { claim, release, layer } = useModalLayer()
|
|
136
|
+
watch(() => props.open, (open) => (open ? claim() : release()), { immediate: true })
|
|
137
|
+
onBeforeUnmount(release)
|
|
138
|
+
|
|
128
139
|
const contentClass = computed(() => {
|
|
129
140
|
const base = "fixed z-(--z-modal) flex flex-col border-line bg-surface-raised shadow-panel outline-none"
|
|
130
141
|
const map: Record<Direction, string> = {
|
|
@@ -345,11 +356,12 @@ const edgeStripClass = computed(() =>
|
|
|
345
356
|
>
|
|
346
357
|
<DrawerPortal>
|
|
347
358
|
<DrawerOverlay
|
|
359
|
+
:style="{ zIndex: layer.scrim }"
|
|
348
360
|
class="lp-scrim fixed inset-0 z-(--z-overlay) data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
|
|
349
361
|
/>
|
|
350
362
|
<DrawerContent
|
|
351
363
|
:class="contentClass"
|
|
352
|
-
:style="sizeStyle"
|
|
364
|
+
:style="[sizeStyle, { zIndex: layer.panel }]"
|
|
353
365
|
:data-vaul-animate="handingOff ? 'false' : undefined"
|
|
354
366
|
>
|
|
355
367
|
<!-- Resize grip on the inner edge (horizontal drawers). data-vaul-no-drag
|
|
@@ -26,8 +26,15 @@ import { computed, ref, type ComputedRef } from "vue"
|
|
|
26
26
|
* is 100 and --z-modal 110, so a gap of 2 left the upper scrim under the lower
|
|
27
27
|
* PANEL — it darkened that panel and could not blur it, a backdrop-filter
|
|
28
28
|
* having nothing of it underneath to work on.
|
|
29
|
+
*
|
|
30
|
+
* Clearing it means strictly more than the 10 between the two rungs. A step of
|
|
31
|
+
* exactly 10 put the lower panel at 100, level with the upper scrim, and a tie
|
|
32
|
+
* in z-index is settled by DOM order. Siblings in one component happened to
|
|
33
|
+
* come out right, but a dialog mounted once at the app root — opened from a
|
|
34
|
+
* page dialog that teleported in later — lost the tie, and its scrim darkened
|
|
35
|
+
* the page behind the first dialog instead of the first dialog itself.
|
|
29
36
|
*/
|
|
30
|
-
const STEP =
|
|
37
|
+
const STEP = 20
|
|
31
38
|
|
|
32
39
|
/** Levels currently claimed, oldest first. */
|
|
33
40
|
const stack = ref<number[]>([])
|
|
@@ -91,6 +91,25 @@ export function useShift(
|
|
|
91
91
|
let inFlight = false
|
|
92
92
|
/** When the in-flight tween started, to tell a duplicate from a follow-up. */
|
|
93
93
|
let tweenStart = 0
|
|
94
|
+
/**
|
|
95
|
+
* Scroll positions inside the box, kept from the scroll events themselves.
|
|
96
|
+
* Measuring lifts the height cap, and for that one reflow a scrolling list
|
|
97
|
+
* inside (a picker, a span tree) is as tall as its content: it no longer
|
|
98
|
+
* overflows, the browser clamps its scrollTop to 0, and putting the cap back
|
|
99
|
+
* does not bring the position back. Every click that changed the content —
|
|
100
|
+
* ticking an item, picking a span — threw the list back to the top.
|
|
101
|
+
*/
|
|
102
|
+
const scrolled = new Map<Element, number>()
|
|
103
|
+
const rememberScroll = (event: Event) => {
|
|
104
|
+
const target = event.target
|
|
105
|
+
if (target instanceof Element) scrolled.set(target, target.scrollTop)
|
|
106
|
+
}
|
|
107
|
+
function restoreScroll() {
|
|
108
|
+
for (const [node, top] of scrolled) {
|
|
109
|
+
if (!node.isConnected) scrolled.delete(node)
|
|
110
|
+
else if (node.scrollTop !== top) node.scrollTop = top
|
|
111
|
+
}
|
|
112
|
+
}
|
|
94
113
|
|
|
95
114
|
const axisOf = () => toValue(options.axis) ?? "height"
|
|
96
115
|
const wantsHeight = () => axisOf() !== "width"
|
|
@@ -151,6 +170,7 @@ export function useShift(
|
|
|
151
170
|
const toWidth = wantsWidth() ? node.offsetWidth : 0
|
|
152
171
|
if (wantsHeight()) node.style.maxHeight = ""
|
|
153
172
|
if (wantsWidth()) node.style.maxWidth = ""
|
|
173
|
+
restoreScroll()
|
|
154
174
|
lastHeight = toHeight
|
|
155
175
|
lastWidth = toWidth
|
|
156
176
|
|
|
@@ -173,6 +193,7 @@ export function useShift(
|
|
|
173
193
|
void node.offsetHeight
|
|
174
194
|
if (movesHeight) node.style.height = `${toHeight}px`
|
|
175
195
|
if (movesWidth) node.style.width = `${toWidth}px`
|
|
196
|
+
restoreScroll()
|
|
176
197
|
|
|
177
198
|
inFlight = true
|
|
178
199
|
tweenStart = performance.now()
|
|
@@ -194,7 +215,12 @@ export function useShift(
|
|
|
194
215
|
}, wait)
|
|
195
216
|
}
|
|
196
217
|
|
|
218
|
+
let scrollHost: HTMLElement | null = null
|
|
219
|
+
|
|
197
220
|
function teardown() {
|
|
221
|
+
scrollHost?.removeEventListener("scroll", rememberScroll, true)
|
|
222
|
+
scrollHost = null
|
|
223
|
+
scrolled.clear()
|
|
198
224
|
ro?.disconnect()
|
|
199
225
|
mo?.disconnect()
|
|
200
226
|
ro = undefined
|
|
@@ -218,6 +244,8 @@ export function useShift(
|
|
|
218
244
|
|
|
219
245
|
lastHeight = node.offsetHeight
|
|
220
246
|
lastWidth = node.offsetWidth
|
|
247
|
+
scrollHost = node
|
|
248
|
+
node.addEventListener("scroll", rememberScroll, { capture: true, passive: true })
|
|
221
249
|
if (wantsHeight()) node.style.height = `${lastHeight}px`
|
|
222
250
|
if (wantsWidth()) node.style.width = `${lastWidth}px`
|
|
223
251
|
// A box must not animate in from its pre-content size, so the transition
|