@leavepulse/ui 0.78.2 → 0.78.3

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.
@@ -1,6 +1,7 @@
1
1
  import { defineComponent, computed, useModel, ref, useId, onMounted, watch, nextTick, onBeforeUnmount, useSlots, openBlock, createElementBlock, mergeProps, unref, createElementVNode, withModifiers, toDisplayString, normalizeClass, createVNode, createSlots, withCtx, renderSlot, normalizeProps, guardReactiveProps, normalizeStyle, createCommentVNode, withDirectives, vShow, createBlock, Teleport, mergeModels } from "vue";
2
2
  import { prefersReducedMotion, easingToken, durationToken } from "../composables/easing.js";
3
3
  import { useLabels } from "../composables/useLpConfig.js";
4
+ import { useResizeObserver } from "../composables/useResizeObserver.js";
4
5
  import { useScrollMemory } from "../composables/scrollMemory.js";
5
6
  import { useView, viewWindow } from "../composables/useView.js";
6
7
  import { _ as _sfc_main$6 } from "./AppShellContextNav-C6Ho2mof.js";
@@ -14,7 +15,7 @@ import { _ as _sfc_main$7 } from "./LpScrollArea-CqvwfZs2.js";
14
15
  import { _ as _sfc_main$5 } from "./LpShift-DAgS_d2x.js";
15
16
  import { _ as _sfc_main$1 } from "./LpSidebar-CvRvVBTt.js";
16
17
  import { mergeClass, useMergedAttrs } from "../composables/useMergedClass.js";
17
- import { nodeWindow } from "../composables/realm.js";
18
+ import { nodeWindow, isHTMLElement } from "../composables/realm.js";
18
19
  const _hoisted_1 = ["href"];
19
20
  const _hoisted_2 = { class: "truncate text-lg font-semibold" };
20
21
  const _hoisted_3 = ["data-lp-density"];
@@ -132,8 +133,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
132
133
  () => props.path,
133
134
  () => props.hash,
134
135
  "",
135
- // The sticky header covers the top of the window; a region starts below it.
136
- () => documentScroll && !bleeding.value ? headerEl.value?.offsetHeight ?? 0 : 0
136
+ // The sticky header and sub-page row cover the top of the window; a region starts below them.
137
+ () => documentScroll && !bleeding.value ? (headerEl.value?.offsetHeight ?? 0) + contextRowHeight.value : 0
137
138
  );
138
139
  useScrollMemory(
139
140
  () => asideArea.value?.viewportEl,
@@ -180,14 +181,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
180
181
  });
181
182
  const bottomBarEl = ref(null);
182
183
  const bottomBarHeight = ref(0);
183
- let bottomBarSize;
184
- onMounted(() => {
185
- const el = bottomBarEl.value?.$el;
186
- if (!el || el.nodeType !== 1) return;
187
- bottomBarSize = new (nodeWindow(el)).ResizeObserver(() => bottomBarHeight.value = el.offsetHeight);
188
- bottomBarSize.observe(el);
189
- });
190
- onBeforeUnmount(() => bottomBarSize?.disconnect());
184
+ useResizeObserver(
185
+ () => bottomBarEl.value?.$el?.nodeType === 1 ? bottomBarEl.value.$el : null,
186
+ (_, el) => bottomBarHeight.value = el.offsetHeight,
187
+ { immediate: true }
188
+ );
189
+ const contextRowEl = ref(null);
190
+ const contextRowHeight = ref(0);
191
191
  let releaseDocument;
192
192
  onMounted(() => {
193
193
  if (!documentScroll || !pageEl.value) return;
@@ -250,7 +250,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
250
250
  const columnClass = computed(() => hasAside.value ? "grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)_auto]" : "flex min-w-0 flex-1 flex-col");
251
251
  const columnStyle = computed(() => ({
252
252
  ...hasAside.value ? { gridTemplateRows: `auto ${contextVisible.value ? "auto " : ""}minmax(0,1fr) auto` } : {},
253
- ...windowScrolls.value ? { "--lp-shell-top": shellTop.value } : {}
253
+ ...shellVars.value
254
254
  }));
255
255
  const spanClass = computed(() => hasAside.value ? "col-span-2" : void 0);
256
256
  const view = useView();
@@ -280,6 +280,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
280
280
  const contextVisible = computed(
281
281
  () => (navMode.value === "bottom" || navMode.value === "drawer") && (contextItems.value.length > 0 || !!slots["context-nav"] && !!props.adaptiveNav?.contextNav)
282
282
  );
283
+ watch(
284
+ [contextVisible, mounted],
285
+ () => {
286
+ const next = headerEl.value?.nextElementSibling;
287
+ contextRowEl.value = contextVisible.value && isHTMLElement(next) && next !== mainEl.value ? next : null;
288
+ if (!contextRowEl.value) contextRowHeight.value = 0;
289
+ },
290
+ { flush: "post" }
291
+ );
292
+ useResizeObserver(contextRowEl, (_, el) => contextRowHeight.value = el.offsetHeight, { immediate: true });
283
293
  function onSelect(item) {
284
294
  emit("select", item);
285
295
  }
@@ -315,7 +325,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
315
325
  );
316
326
  const STICKY = "sticky z-(--z-sticky)";
317
327
  const railClass = computed(() => windowScrolls.value ? "sticky top-0 flex h-dvh shrink-0 self-start" : "contents");
318
- const shellTop = computed(() => headerHidden.value ? "0px" : headerHeight.value);
328
+ const shellHeader = computed(() => headerHidden.value ? "0px" : headerHeight.value);
329
+ const shellVars = computed(
330
+ () => windowScrolls.value ? {
331
+ "--lp-shell-header": shellHeader.value,
332
+ "--lp-shell-top": `calc(${shellHeader.value} + ${contextRowHeight.value}px)`,
333
+ "--lp-shell-bottom": `${bottomBarHeight.value}px`
334
+ } : { "--lp-shell-header": "0px", "--lp-shell-top": "0px", "--lp-shell-bottom": "0px" }
335
+ );
319
336
  const BACK_TO_TOP_ABOVE_BAR = {
320
337
  sm: "bottom-[calc(4.5rem+env(safe-area-inset-bottom))] sm:bottom-[calc(1rem+env(safe-area-inset-bottom))]",
321
338
  md: "bottom-[calc(4.5rem+env(safe-area-inset-bottom))] md:bottom-[calc(1rem+env(safe-area-inset-bottom))]",
@@ -478,7 +495,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
478
495
  mode: navMode.value
479
496
  }, () => [
480
497
  createVNode(_sfc_main$6, {
481
- class: normalizeClass([spanClass.value, windowScrolls.value && `${STICKY} top-(--lp-shell-top) transition-[top] duration-[var(--duration-medium)] ease-[var(--ease-settle)]`]),
498
+ class: normalizeClass([spanClass.value, windowScrolls.value && `${STICKY} top-(--lp-shell-header) transition-[top] duration-[var(--duration-medium)] ease-[var(--ease-settle)]`]),
482
499
  items: contextItems.value,
483
500
  "is-active": activeTest.value,
484
501
  label: unref(text).contextNav,
@@ -575,8 +592,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
575
592
  windowScrolls.value && __props.pageScrollbar ? (openBlock(), createBlock(_sfc_main$8, {
576
593
  key: 2,
577
594
  top: "var(--lp-shell-top)",
578
- bottom: `${bottomBarHeight.value}px`
579
- }, null, 8, ["bottom"])) : createCommentVNode("", true),
595
+ bottom: "var(--lp-shell-bottom)"
596
+ })) : createCommentVNode("", true),
580
597
  __props.bottomNav ? (openBlock(), createBlock(_sfc_main$9, {
581
598
  key: 3,
582
599
  ref_key: "bottomBarEl",
@@ -54,6 +54,15 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
54
54
  * `min-h-[calc(100dvh-4rem)]` or `fullBleed`. `"region"`: the shell is
55
55
  * the viewport and the page scrolls inside main. Read once: changing it
56
56
  * remounts the page.
57
+ *
58
+ * A page's own sticky parts keep clear of the shell's with two variables
59
+ * set inside it: `--lp-shell-top`, how much of the window's top the shell
60
+ * covers (the header, 0 while `headerCollapse` has it away, plus the
61
+ * sub-page row), and `--lp-shell-bottom`, how much of its foot (the bottom
62
+ * bar). Both change as those do, and are 0px in `"region"` and on a
63
+ * `fullBleed` page, where nothing of the shell's lies over the page:
64
+ * `position: sticky; top: var(--lp-shell-top)`. Keep the page's sticky
65
+ * layers under `--z-sticky`, the shell's.
57
66
  */
58
67
  scroll?: "document" | "region";
59
68
  /**
@@ -210,6 +219,15 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
210
219
  * `min-h-[calc(100dvh-4rem)]` or `fullBleed`. `"region"`: the shell is
211
220
  * the viewport and the page scrolls inside main. Read once: changing it
212
221
  * remounts the page.
222
+ *
223
+ * A page's own sticky parts keep clear of the shell's with two variables
224
+ * set inside it: `--lp-shell-top`, how much of the window's top the shell
225
+ * covers (the header, 0 while `headerCollapse` has it away, plus the
226
+ * sub-page row), and `--lp-shell-bottom`, how much of its foot (the bottom
227
+ * bar). Both change as those do, and are 0px in `"region"` and on a
228
+ * `fullBleed` page, where nothing of the shell's lies over the page:
229
+ * `position: sticky; top: var(--lp-shell-top)`. Keep the page's sticky
230
+ * layers under `--z-sticky`, the shell's.
213
231
  */
214
232
  scroll?: "document" | "region";
215
233
  /**
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpAppShell-DkvU6IBB.js";
1
+ import { _ as _sfc_main } from "../chunks/LpAppShell-DRHoQtr-.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { _ as _3 } from "./chunks/LpMapPicker-D5_YLLBC.js";
4
4
  import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
5
5
  import { _ as _4 } from "./chunks/LpActionBar-BGMk5KoG.js";
6
6
  import { _ as _5 } from "./chunks/LpAlert-CjvTZIKF.js";
7
- import { _ as _6 } from "./chunks/LpAppShell-DkvU6IBB.js";
7
+ import { _ as _6 } from "./chunks/LpAppShell-DRHoQtr-.js";
8
8
  import { _ as _7 } from "./chunks/LpAutocomplete-CrZbd0uv.js";
9
9
  import { _ as _8 } from "./chunks/LpAvatar-cQVsNk6F.js";
10
10
  import { _ as _9 } from "./chunks/LpBadge-D5QQdDQ_.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.78.2",
3
+ "version": "0.78.3",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -47,8 +47,9 @@ export default { inheritAttrs: false }
47
47
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots, watch } from "vue"
48
48
  import { easingToken, durationToken, prefersReducedMotion } from "../composables/easing"
49
49
  import { useLabels } from "../composables/useLpConfig"
50
+ import { useResizeObserver } from "../composables/useResizeObserver"
50
51
  import { useScrollMemory } from "../composables/scrollMemory"
51
- import { nodeWindow, useView, viewWindow } from "../composables/useView"
52
+ import { isHTMLElement, nodeWindow, useView, viewWindow } from "../composables/useView"
52
53
  import AppShellContextNav from "./AppShellContextNav.vue"
53
54
  import {
54
55
  contextNavItems,
@@ -129,6 +130,15 @@ const props = withDefaults(
129
130
  * `min-h-[calc(100dvh-4rem)]` or `fullBleed`. `"region"`: the shell is
130
131
  * the viewport and the page scrolls inside main. Read once: changing it
131
132
  * remounts the page.
133
+ *
134
+ * A page's own sticky parts keep clear of the shell's with two variables
135
+ * set inside it: `--lp-shell-top`, how much of the window's top the shell
136
+ * covers (the header, 0 while `headerCollapse` has it away, plus the
137
+ * sub-page row), and `--lp-shell-bottom`, how much of its foot (the bottom
138
+ * bar). Both change as those do, and are 0px in `"region"` and on a
139
+ * `fullBleed` page, where nothing of the shell's lies over the page:
140
+ * `position: sticky; top: var(--lp-shell-top)`. Keep the page's sticky
141
+ * layers under `--z-sticky`, the shell's.
132
142
  */
133
143
  scroll?: "document" | "region"
134
144
  /**
@@ -329,8 +339,8 @@ useScrollMemory(
329
339
  () => props.path,
330
340
  () => props.hash,
331
341
  "",
332
- // The sticky header covers the top of the window; a region starts below it.
333
- () => (documentScroll && !bleeding.value ? (headerEl.value?.offsetHeight ?? 0) : 0),
342
+ // The sticky header and sub-page row cover the top of the window; a region starts below them.
343
+ () => (documentScroll && !bleeding.value ? (headerEl.value?.offsetHeight ?? 0) + contextRowHeight.value : 0),
334
344
  )
335
345
  // The side region keeps its own place on the same address.
336
346
  useScrollMemory(
@@ -389,17 +399,19 @@ watch(bleeding, (bleed) => {
389
399
  void nextTick(() => view.scrollTo({ top, behavior: "instant" }))
390
400
  }
391
401
  })
392
- // The bottom bar's height, for the page's scrollbar to stop above it (0 where it is hidden).
402
+ /*
403
+ * What the shell's sticky bars cover of the window, for the page: the sub-page
404
+ * row's height (it sits under the header) and the bottom bar's (0 where hidden).
405
+ */
393
406
  const bottomBarEl = ref<{ $el: HTMLElement } | null>(null)
394
407
  const bottomBarHeight = ref(0)
395
- let bottomBarSize: ResizeObserver | undefined
396
- onMounted(() => {
397
- const el = bottomBarEl.value?.$el
398
- if (!el || el.nodeType !== 1) return
399
- bottomBarSize = new (nodeWindow(el).ResizeObserver)(() => (bottomBarHeight.value = el.offsetHeight))
400
- bottomBarSize.observe(el)
401
- })
402
- onBeforeUnmount(() => bottomBarSize?.disconnect())
408
+ useResizeObserver(
409
+ () => (bottomBarEl.value?.$el?.nodeType === 1 ? bottomBarEl.value.$el : null),
410
+ (_, el) => (bottomBarHeight.value = (el as HTMLElement).offsetHeight),
411
+ { immediate: true },
412
+ )
413
+ const contextRowEl = ref<HTMLElement | null>(null)
414
+ const contextRowHeight = ref(0)
403
415
 
404
416
  let releaseDocument: (() => void) | undefined
405
417
  onMounted(() => {
@@ -480,7 +492,7 @@ const asideClasses = computed(() =>
480
492
  const columnClass = computed(() => (hasAside.value ? "grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)_auto]" : "flex min-w-0 flex-1 flex-col"))
481
493
  const columnStyle = computed(() => ({
482
494
  ...(hasAside.value ? { gridTemplateRows: `auto ${contextVisible.value ? "auto " : ""}minmax(0,1fr) auto` } : {}),
483
- ...(windowScrolls.value ? { "--lp-shell-top": shellTop.value } : {}),
495
+ ...shellVars.value,
484
496
  }))
485
497
  const spanClass = computed(() => (hasAside.value ? "col-span-2" : undefined))
486
498
 
@@ -520,6 +532,17 @@ const contextItems = computed(() => (props.adaptiveNav?.contextNav ? contextNavI
520
532
  const contextVisible = computed(
521
533
  () => (navMode.value === "bottom" || navMode.value === "drawer") && (contextItems.value.length > 0 || (!!slots["context-nav"] && !!props.adaptiveNav?.contextNav)),
522
534
  )
535
+ // The row is the app's own slot or the shell's, so it is found where it renders: right after the header.
536
+ watch(
537
+ [contextVisible, mounted],
538
+ () => {
539
+ const next = headerEl.value?.nextElementSibling
540
+ contextRowEl.value = contextVisible.value && isHTMLElement(next) && next !== mainEl.value ? next : null
541
+ if (!contextRowEl.value) contextRowHeight.value = 0
542
+ },
543
+ { flush: "post" },
544
+ )
545
+ useResizeObserver(contextRowEl, (_, el) => (contextRowHeight.value = (el as HTMLElement).offsetHeight), { immediate: true })
523
546
 
524
547
  function onSelect(item: SidebarItem) {
525
548
  emit("select", item)
@@ -564,12 +587,27 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
564
587
 
565
588
  /*
566
589
  * What stays in view while the window scrolls the page. The header sits on
567
- * top; the row of sub-pages and the aside sit under it, following it out
568
- * when it collapses (--lp-shell-top is how much of the header is showing).
590
+ * top; the row of sub-pages sits under it and follows it out when it
591
+ * collapses (--lp-shell-header is how much of the header is showing).
592
+ *
593
+ * Two variables are the page's to use (documented on `scroll`), and set in
594
+ * every mode, so `var(--lp-shell-top)` never needs a fallback:
595
+ * --lp-shell-top is how much of the window's top the shell covers (header
596
+ * plus sub-page row), --lp-shell-bottom how much of its foot (the bottom
597
+ * bar). Both are 0px where the page scrolls in its own region or is full bleed.
569
598
  */
570
599
  const STICKY = "sticky z-(--z-sticky)"
571
600
  const railClass = computed(() => (windowScrolls.value ? "sticky top-0 flex h-dvh shrink-0 self-start" : "contents"))
572
- const shellTop = computed(() => (headerHidden.value ? "0px" : headerHeight.value))
601
+ const shellHeader = computed(() => (headerHidden.value ? "0px" : headerHeight.value))
602
+ const shellVars = computed(() =>
603
+ windowScrolls.value
604
+ ? {
605
+ "--lp-shell-header": shellHeader.value,
606
+ "--lp-shell-top": `calc(${shellHeader.value} + ${contextRowHeight.value}px)`,
607
+ "--lp-shell-bottom": `${bottomBarHeight.value}px`,
608
+ }
609
+ : { "--lp-shell-header": "0px", "--lp-shell-top": "0px", "--lp-shell-bottom": "0px" },
610
+ )
573
611
  const BACK_TO_TOP_ABOVE_BAR: Record<NavBreakpoint, string> = {
574
612
  sm: "bottom-[calc(4.5rem+env(safe-area-inset-bottom))] sm:bottom-[calc(1rem+env(safe-area-inset-bottom))]",
575
613
  md: "bottom-[calc(4.5rem+env(safe-area-inset-bottom))] md:bottom-[calc(1rem+env(safe-area-inset-bottom))]",
@@ -725,7 +763,7 @@ const backToTopClass = computed(() =>
725
763
  <template v-if="contextVisible">
726
764
  <slot name="context-nav" :items="contextItems" :mode="navMode">
727
765
  <AppShellContextNav
728
- :class="[spanClass, windowScrolls && `${STICKY} top-(--lp-shell-top) transition-[top] duration-[var(--duration-medium)] ease-[var(--ease-settle)]`]"
766
+ :class="[spanClass, windowScrolls && `${STICKY} top-(--lp-shell-header) transition-[top] duration-[var(--duration-medium)] ease-[var(--ease-settle)]`]"
729
767
  :items="contextItems"
730
768
  :is-active="activeTest"
731
769
  :label="text.contextNav"
@@ -769,7 +807,7 @@ const backToTopClass = computed(() =>
769
807
  <slot name="aside" />
770
808
  </LpScrollArea>
771
809
  </aside>
772
- <LpPageScrollbar v-if="windowScrolls && pageScrollbar" top="var(--lp-shell-top)" :bottom="`${bottomBarHeight}px`" />
810
+ <LpPageScrollbar v-if="windowScrolls && pageScrollbar" top="var(--lp-shell-top)" bottom="var(--lp-shell-bottom)" />
773
811
  <LpBottomNav
774
812
  v-if="bottomNav"
775
813
  ref="bottomBarEl"