@leavepulse/ui 0.19.0 → 0.19.1

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.
@@ -6,7 +6,7 @@ import { _ as _sfc_main$3 } from "./LpButton-5VXXCjwF.js";
6
6
  import { _ as _sfc_main$1 } from "./LpFormField-Bn-ZwC6z.js";
7
7
  import { _ as _sfc_main$4 } from "./LpIcon-CCnX5_2j.js";
8
8
  import { _ as _sfc_main$5 } from "./LpInput-BauZRyzm.js";
9
- import { _ as _sfc_main$7 } from "./LpModal-BJTiTSDJ.js";
9
+ import { _ as _sfc_main$7 } from "./LpModal-vlrgcEY_.js";
10
10
  import { _ as _sfc_main$6 } from "./LpSelect-D5mU4cZO.js";
11
11
  const _hoisted_1 = { class: "flex flex-col gap-3" };
12
12
  const _hoisted_2 = { class: "flex items-stretch gap-2" };
@@ -1,6 +1,6 @@
1
1
  import { defineComponent, openBlock, createBlock, withCtx, renderSlot, createVNode, createTextVNode, toDisplayString } from "vue";
2
2
  import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
3
- import { _ as _sfc_main$1 } from "./LpModal-BJTiTSDJ.js";
3
+ import { _ as _sfc_main$1 } from "./LpModal-vlrgcEY_.js";
4
4
  const _sfc_main = /* @__PURE__ */ defineComponent({
5
5
  __name: "LpConfirmDialog",
6
6
  props: {
@@ -1,4 +1,4 @@
1
- import { defineComponent, computed, useSlots, openBlock, createBlock, unref, withCtx, createVNode, createElementVNode, mergeProps, createElementBlock, renderSlot, createTextVNode, toDisplayString, createCommentVNode, normalizeClass } from "vue";
1
+ import { defineComponent, computed, useSlots, ref, watch, onBeforeUnmount, openBlock, createBlock, unref, withCtx, createVNode, createElementVNode, mergeProps, createElementBlock, renderSlot, createTextVNode, toDisplayString, createCommentVNode, normalizeClass } from "vue";
2
2
  import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from "reka-ui";
3
3
  import { CLOSE_ICON } from "../components/dropdown.js";
4
4
  import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
@@ -13,6 +13,7 @@ const _hoisted_4 = {
13
13
  key: 3,
14
14
  class: "flex shrink-0 justify-end gap-2 p-5 pt-4"
15
15
  };
16
+ const TWEEN_MS = 160;
16
17
  const _sfc_main = /* @__PURE__ */ defineComponent({
17
18
  __name: "LpModal",
18
19
  props: {
@@ -49,6 +50,73 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
49
50
  slots.footer ? "" : "pb-5"
50
51
  ].filter(Boolean).join(" ")
51
52
  );
53
+ const panelRef = ref(null);
54
+ const tweening = ref(false);
55
+ const resizing = ref(false);
56
+ let ro;
57
+ let mo;
58
+ let raf = 0;
59
+ let settleTimer;
60
+ const retune = (el) => {
61
+ const from = el.offsetHeight;
62
+ el.style.height = "";
63
+ const to = el.offsetHeight;
64
+ if (to === 0) return;
65
+ if (to === from) {
66
+ el.style.height = `${to}px`;
67
+ return;
68
+ }
69
+ el.style.height = `${from}px`;
70
+ void el.offsetHeight;
71
+ el.style.height = `${to}px`;
72
+ resizing.value = true;
73
+ clearTimeout(settleTimer);
74
+ settleTimer = setTimeout(() => {
75
+ resizing.value = false;
76
+ }, TWEEN_MS);
77
+ };
78
+ watch(
79
+ () => props.open,
80
+ (isOpen) => {
81
+ ro?.disconnect();
82
+ mo?.disconnect();
83
+ ro = void 0;
84
+ mo = void 0;
85
+ cancelAnimationFrame(raf);
86
+ clearTimeout(settleTimer);
87
+ tweening.value = false;
88
+ resizing.value = false;
89
+ const el = panelRef.value?.$el;
90
+ if (!isOpen || !(el instanceof HTMLElement)) return;
91
+ el.style.height = `${el.offsetHeight}px`;
92
+ raf = requestAnimationFrame(() => {
93
+ tweening.value = true;
94
+ });
95
+ const watchChildren = () => {
96
+ if (!ro) return;
97
+ ro.disconnect();
98
+ for (const child of Array.from(el.children)) ro.observe(child);
99
+ };
100
+ if (typeof ResizeObserver !== "undefined") {
101
+ ro = new ResizeObserver(() => retune(el));
102
+ watchChildren();
103
+ }
104
+ if (typeof MutationObserver !== "undefined") {
105
+ mo = new MutationObserver(() => {
106
+ watchChildren();
107
+ retune(el);
108
+ });
109
+ mo.observe(el, { childList: true, subtree: true, characterData: true });
110
+ }
111
+ },
112
+ { flush: "post" }
113
+ );
114
+ onBeforeUnmount(() => {
115
+ ro?.disconnect();
116
+ mo?.disconnect();
117
+ cancelAnimationFrame(raf);
118
+ clearTimeout(settleTimer);
119
+ });
52
120
  return (_ctx, _cache) => {
53
121
  return openBlock(), createBlock(unref(DialogRoot), {
54
122
  open: __props.open,
@@ -60,7 +128,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
60
128
  createVNode(unref(DialogOverlay), { class: "lp-scrim fixed inset-0 z-(--z-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
61
129
  createElementVNode("div", _hoisted_1, [
62
130
  createVNode(unref(DialogContent), mergeProps({
63
- class: ["pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[rise-out_120ms_cubic-bezier(0.4,0,1,1)]", widthClass.value],
131
+ ref_key: "panelRef",
132
+ ref: panelRef,
133
+ class: ["pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[rise-out_120ms_cubic-bezier(0.4,0,1,1)]", [widthClass.value, tweening.value ? "transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none" : ""]],
64
134
  style: __props.width ? { width: __props.width } : void 0
65
135
  }, describedByAttrs.value), {
66
136
  default: withCtx(() => [
@@ -105,14 +175,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
105
175
  renderSlot(_ctx.$slots, "default")
106
176
  ], 2)) : (openBlock(), createBlock(_sfc_main$2, {
107
177
  key: 2,
108
- class: "min-h-0 flex-1 text-sm text-ink/90",
178
+ class: normalizeClass(["min-h-0 flex-1 text-sm text-ink/90", resizing.value ? "[&_[data-scrollbarimpl]]:invisible" : ""]),
109
179
  "content-class": bodyPad.value
110
180
  }, {
111
181
  default: withCtx(() => [
112
182
  renderSlot(_ctx.$slots, "default")
113
183
  ]),
114
184
  _: 3
115
- }, 8, ["content-class"])),
185
+ }, 8, ["class", "content-class"])),
116
186
  _ctx.$slots.footer ? (openBlock(), createElementBlock("footer", _hoisted_4, [
117
187
  renderSlot(_ctx.$slots, "footer")
118
188
  ])) : createCommentVNode("", true)
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpAddressInput-CWlfDyKz.js";
1
+ import { _ as _sfc_main } from "../chunks/LpAddressInput-N8O-LtCS.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpConfirmDialog-JNW4rpx3.js";
1
+ import { _ as _sfc_main } from "../chunks/LpConfirmDialog-B-amVJsM.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -16,15 +16,15 @@ type __VLS_Props = {
16
16
  */
17
17
  fillBody?: boolean;
18
18
  };
19
- declare var __VLS_33: {}, __VLS_52: {}, __VLS_60: {}, __VLS_62: {};
19
+ declare var __VLS_35: {}, __VLS_54: {}, __VLS_62: {}, __VLS_64: {};
20
20
  type __VLS_Slots = {} & {
21
- title?: (props: typeof __VLS_33) => any;
21
+ title?: (props: typeof __VLS_35) => any;
22
22
  } & {
23
- default?: (props: typeof __VLS_52) => any;
23
+ default?: (props: typeof __VLS_54) => any;
24
24
  } & {
25
- default?: (props: typeof __VLS_60) => any;
25
+ default?: (props: typeof __VLS_62) => any;
26
26
  } & {
27
- footer?: (props: typeof __VLS_62) => any;
27
+ footer?: (props: typeof __VLS_64) => any;
28
28
  };
29
29
  declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
30
30
  "update:open": (value: boolean) => any;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpModal-BJTiTSDJ.js";
1
+ import { _ as _sfc_main } from "../chunks/LpModal-vlrgcEY_.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { _ } from "./chunks/LayoutCanvas-Dd20boUG.js";
2
- import { _ as _2 } from "./chunks/LpAddressInput-CWlfDyKz.js";
2
+ import { _ as _2 } from "./chunks/LpAddressInput-N8O-LtCS.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";
@@ -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-JNW4rpx3.js";
17
+ import { _ as _15 } from "./chunks/LpConfirmDialog-B-amVJsM.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";
@@ -32,7 +32,7 @@ import { _ as _28 } from "./chunks/LpLightbox-CzhFe9wZ.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-tmKPNTuU.js";
35
- import { _ as _31 } from "./chunks/LpModal-BJTiTSDJ.js";
35
+ import { _ as _31 } from "./chunks/LpModal-vlrgcEY_.js";
36
36
  import { _ as _32 } from "./chunks/LpNotificationBell-DHze8wYN.js";
37
37
  import { _ as _33 } from "./chunks/LpNumberField-C5ar-OwE.js";
38
38
  import { _ as _34 } from "./chunks/LpOtpInput-DqaT0Z75.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -8,7 +8,8 @@ import {
8
8
  DialogRoot,
9
9
  DialogTitle,
10
10
  } from "reka-ui"
11
- import { computed, useSlots } from "vue"
11
+ import type { ComponentPublicInstance } from "vue"
12
+ import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
12
13
  import { CLOSE_ICON } from "./dropdown"
13
14
  import LpIcon from "./LpIcon.vue"
14
15
  import LpScrollArea from "./LpScrollArea.vue"
@@ -70,6 +71,118 @@ const bodyPad = computed(() =>
70
71
  .filter(Boolean)
71
72
  .join(" "),
72
73
  )
74
+
75
+ /*
76
+ * Height is content-derived, so a body that arrives late — a placeholder swapped
77
+ * for the loaded thing — resizes the panel in a single frame, right on top of
78
+ * the open animation. CSS alone can't smooth it: `height: auto` isn't
79
+ * interpolable, and the keyword-interpolation escape hatches
80
+ * (`interpolate-size`, `calc-size()`) are Chromium-only, so they would do
81
+ * nothing in the WebKitGTK launcher. Measuring and pinning a pixel height is
82
+ * what works on every engine.
83
+ *
84
+ * The pin is always the panel's own natural height, so `max-h` still clips and
85
+ * the body keeps `flex-1 min-h-0` — an over-tall panel hands its overflow to the
86
+ * scroll area exactly as before. A panel whose size never moves is pinned once
87
+ * and never transitions.
88
+ */
89
+ // reka renders a real element but exposes it as a component instance, so the
90
+ // panel is reached through $el.
91
+ const panelRef = ref<ComponentPublicInstance | null>(null)
92
+ const tweening = ref(false)
93
+ const resizing = ref(false)
94
+
95
+ /** Kept in step with the `duration-fast` utility on the panel. */
96
+ const TWEEN_MS = 160
97
+
98
+ let ro: ResizeObserver | undefined
99
+ let mo: MutationObserver | undefined
100
+ let raf = 0
101
+ let settleTimer: ReturnType<typeof setTimeout> | undefined
102
+
103
+ // The height is driven imperatively rather than through a style binding. By the
104
+ // time replacement content is in the DOM the panel has already reflowed to its
105
+ // new size, so a bound value would only ever be written once, with no earlier
106
+ // height for the transition to start from. Writing the OLD height, forcing a
107
+ // reflow, then writing the new one gives the transition both ends.
108
+ const retune = (el: HTMLElement) => {
109
+ const from = el.offsetHeight
110
+ el.style.height = ""
111
+ const to = el.offsetHeight
112
+ if (to === 0) return
113
+ if (to === from) {
114
+ el.style.height = `${to}px`
115
+ return
116
+ }
117
+ el.style.height = `${from}px`
118
+ void el.offsetHeight
119
+ el.style.height = `${to}px`
120
+
121
+ // While the panel is still catching up, the body is shorter than the content
122
+ // it already holds, so the scroll area would flash a bar for an overflow that
123
+ // resolves itself the moment the transition lands.
124
+ resizing.value = true
125
+ clearTimeout(settleTimer)
126
+ settleTimer = setTimeout(() => {
127
+ resizing.value = false
128
+ }, TWEEN_MS)
129
+ }
130
+
131
+ // Keyed off `open` rather than the ref: the panel is portalled and remounts on
132
+ // every open, and the teardown has to run on close, when the ref is already gone.
133
+ watch(
134
+ () => props.open,
135
+ (isOpen) => {
136
+ ro?.disconnect()
137
+ mo?.disconnect()
138
+ ro = undefined
139
+ mo = undefined
140
+ cancelAnimationFrame(raf)
141
+ clearTimeout(settleTimer)
142
+ tweening.value = false
143
+ resizing.value = false
144
+
145
+ const el = panelRef.value?.$el
146
+ if (!isOpen || !(el instanceof HTMLElement)) return
147
+
148
+ el.style.height = `${el.offsetHeight}px`
149
+ // The opening panel must not animate its height in from the pre-content box,
150
+ // so the transition only arms once that first pin has painted.
151
+ raf = requestAnimationFrame(() => {
152
+ tweening.value = true
153
+ })
154
+
155
+ // Two triggers, since either can move the height alone: the resize observer
156
+ // catches a child growing in place (an image decoding, a list filling in),
157
+ // the mutation observer catches one subtree swapped for another, where every
158
+ // observed box may keep its size. Children are observed, never the panel —
159
+ // that would feed the pin straight back in.
160
+ const watchChildren = () => {
161
+ if (!ro) return
162
+ ro.disconnect()
163
+ for (const child of Array.from(el.children)) ro.observe(child)
164
+ }
165
+ if (typeof ResizeObserver !== "undefined") {
166
+ ro = new ResizeObserver(() => retune(el))
167
+ watchChildren()
168
+ }
169
+ if (typeof MutationObserver !== "undefined") {
170
+ mo = new MutationObserver(() => {
171
+ watchChildren()
172
+ retune(el)
173
+ })
174
+ mo.observe(el, { childList: true, subtree: true, characterData: true })
175
+ }
176
+ },
177
+ { flush: "post" },
178
+ )
179
+
180
+ onBeforeUnmount(() => {
181
+ ro?.disconnect()
182
+ mo?.disconnect()
183
+ cancelAnimationFrame(raf)
184
+ clearTimeout(settleTimer)
185
+ })
73
186
  </script>
74
187
 
75
188
  <template>
@@ -100,8 +213,9 @@ const bodyPad = computed(() =>
100
213
  pass a description keeps the generated id and the link to
101
214
  DialogDescription. -->
102
215
  <DialogContent
103
- class="pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[rise-out_120ms_cubic-bezier(0.4,0,1,1)]"
104
- :class="widthClass"
216
+ ref="panelRef"
217
+ class="pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[rise-out_120ms_cubic-bezier(0.4,0,1,1)]"
218
+ :class="[widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
105
219
  :style="width ? { width } : undefined"
106
220
  v-bind="describedByAttrs"
107
221
  >
@@ -135,9 +249,14 @@ const bodyPad = computed(() =>
135
249
  >
136
250
  <slot />
137
251
  </div>
252
+ <!-- While the panel is still growing it is briefly shorter than the body
253
+ it already holds, so the scroll area raises a bar for an overflow
254
+ that resolves itself as the transition lands. Hiding just the bar
255
+ for the length of the tween leaves scrolling itself untouched. -->
138
256
  <LpScrollArea
139
257
  v-else
140
258
  class="min-h-0 flex-1 text-sm text-ink/90"
259
+ :class="resizing ? '[&_[data-scrollbarimpl]]:invisible' : ''"
141
260
  :content-class="bodyPad"
142
261
  >
143
262
  <slot />