@leavepulse/ui 0.95.1 → 0.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/assets/{LpTable-BJjNEvgJ.css → LpTable-DSTuJxRU.css} +37 -37
  2. package/dist/chunks/{DropzoneLinkField-Ckuh8zFC.js → DropzoneLinkField-D1gkwYOl.js} +2 -2
  3. package/dist/chunks/{FieldWrapper-CVTPnpoG.js → FieldWrapper-BaYhNT7r.js} +1 -1
  4. package/dist/chunks/{LpActionBar-43WHmTsM.js → LpActionBar-DuwBawji.js} +2 -2
  5. package/dist/chunks/{LpAddressInput-C5VIgRp0.js → LpAddressInput-D596mz6j.js} +6 -6
  6. package/dist/chunks/{LpAlert-Q0h7Gucu.js → LpAlert-CdzeGn72.js} +2 -2
  7. package/dist/chunks/{LpAppShell-C4b3ySa_.js → LpAppShell-iShI1QtP.js} +4 -4
  8. package/dist/chunks/{LpAutocomplete-YPxAPHEo.js → LpAutocomplete-C79ZI9mX.js} +1 -1
  9. package/dist/chunks/{LpBadge-Bh5roBr_.js → LpBadge-D_TB_Iuv.js} +1 -1
  10. package/dist/chunks/{LpBottomNav-9AqvZlRW.js → LpBottomNav-SMIiebhK.js} +1 -1
  11. package/dist/chunks/{LpBreadcrumbs-Zv1fn9e_.js → LpBreadcrumbs-CF-qfujt.js} +1 -1
  12. package/dist/chunks/{LpButton-BvVoF-PO.js → LpButton-78PtrlBs.js} +1 -1
  13. package/dist/chunks/{LpCard-xXBqLVGo.js → LpCard-C7LmpifA.js} +1 -1
  14. package/dist/chunks/{LpCheckbox-C9VnPIQf.js → LpCheckbox-BnQcOHbs.js} +2 -2
  15. package/dist/chunks/{LpCodeBlock-DTU40x8P.js → LpCodeBlock-DVPqlRQx.js} +1 -1
  16. package/dist/chunks/{LpCodeDisplay-F2hjLb0X.js → LpCodeDisplay-38kLrgH9.js} +4 -4
  17. package/dist/chunks/{LpColorPicker-KGMG8QEh.js → LpColorPicker-DuuaWRsT.js} +1 -1
  18. package/dist/chunks/{LpConfirmDialog-DrHcIxRn.js → LpConfirmDialog-CvC-Uxk5.js} +1 -1
  19. package/dist/chunks/{LpConfirmHost-D0Cd8FA_.js → LpConfirmHost-BAuEU5wp.js} +1 -1
  20. package/dist/chunks/{LpCopyButton-CiqUS4SX.js → LpCopyButton-CBk_uxSq.js} +1 -1
  21. package/dist/chunks/{LpCountdown-DVl7vXI1.js → LpCountdown-Db-uzYL_.js} +1 -1
  22. package/dist/chunks/{LpDataList-Cit9fOSp.js → LpDataList-DReLDsdm.js} +1 -1
  23. package/dist/chunks/{LpDatePicker-Dlyvgsan.js → LpDatePicker-DawYC7Pg.js} +2 -2
  24. package/dist/chunks/{LpDateRangePicker-DRBB8z6t.js → LpDateRangePicker-DNTj2SDa.js} +1 -1
  25. package/dist/chunks/{LpEditable-DfRUAGkA.js → LpEditable-CtulE7dP.js} +2 -2
  26. package/dist/chunks/{LpFileTree-gVjF_b6r.js → LpFileTree-CLpVoqzu.js} +3 -3
  27. package/dist/chunks/{LpFileTreeNode-CC48DbT5.js → LpFileTreeNode-DkjdwSni.js} +3 -3
  28. package/dist/chunks/{LpFormField-jbM7K1Hg.js → LpFormField-mIq_9UWb.js} +1 -1
  29. package/dist/chunks/{LpHintLayer-nuMqfzvy.js → LpHintLayer-BG4cgJNr.js} +2 -2
  30. package/dist/chunks/{LpHints-ByAd3a76.js → LpHints-DuisuI5J.js} +3 -3
  31. package/dist/chunks/{LpInfraNode-tD5x5YOy.js → LpInfraNode-CFSOir_N.js} +2 -2
  32. package/dist/chunks/{LpInput-DGMpxrcg.js → LpInput-BOI2XFq_.js} +1 -1
  33. package/dist/chunks/{LpInspector-DSL1lINx.js → LpInspector-f93FmxCP.js} +1 -1
  34. package/dist/chunks/{LpLightbox--EwvQGe-.js → LpLightbox-DAxV5Yvn.js} +3 -3
  35. package/dist/chunks/{LpLoader-Bf1yZwaU.js → LpLoader-2tO499NO.js} +1 -1
  36. package/dist/chunks/{LpLogInspector-BhJTJIst.js → LpLogInspector-DPZWN5Ev.js} +1 -1
  37. package/dist/chunks/{LpLogViewer-DSq9e8mT.js → LpLogViewer-Gftt0yf5.js} +4 -4
  38. package/dist/chunks/{LpMapPicker-V0Lybi10.js → LpMapPicker-DACiiuSY.js} +1 -1
  39. package/dist/chunks/{LpMediaGallery-DlYJf3DS.js → LpMediaGallery-BqEdUKhA.js} +1 -1
  40. package/dist/chunks/{LpModalNav-Cj02cfhL.js → LpModalNav-Ga6u3bbf.js} +2 -2
  41. package/dist/chunks/{LpNotificationBell-DNGijtmW.js → LpNotificationBell-CbBOWxEa.js} +3 -3
  42. package/dist/chunks/{LpNotificationFeed-BoovbtfL.js → LpNotificationFeed-DDWNr7vY.js} +2 -2
  43. package/dist/chunks/{LpNumberField-C0LSu-tl.js → LpNumberField-BtVJ0pT4.js} +1 -1
  44. package/dist/chunks/{LpOtpInput-C14XagVB.js → LpOtpInput-DTX1w62e.js} +1 -1
  45. package/dist/chunks/{LpPaletteTheme-_FwkJO2o.js → LpPaletteTheme-DJT8_7yH.js} +3 -3
  46. package/dist/chunks/{LpPasswordInput-BBDz2zyU.js → LpPasswordInput-B716mvni.js} +2 -2
  47. package/dist/chunks/{LpPhoneInput-BdglZsGH.js → LpPhoneInput-VtPE_Ipa.js} +1 -1
  48. package/dist/chunks/{LpPie-CtC4XdhC.js → LpPie-Ffg1F7As.js} +2 -2
  49. package/dist/chunks/{LpPipeline-aCALaUY8.js → LpPipeline-CyCgP9_y.js} +2 -2
  50. package/dist/chunks/{LpRadioGroup-Cr_fmrBY.js → LpRadioGroup-VJUwOA08.js} +1 -1
  51. package/dist/chunks/{LpRangeSlider-DbaITHtC.js → LpRangeSlider-CsAeqpK4.js} +1 -1
  52. package/dist/chunks/{LpRepeater-C0rns2xW.js → LpRepeater-BRuVEF2M.js} +3 -3
  53. package/dist/chunks/{LpScramble-DsE77iVp.js → LpScramble-DdP1aF0m.js} +1 -1
  54. package/dist/chunks/{LpSecretField-C9vwMFQ6.js → LpSecretField-B04cdcB8.js} +6 -6
  55. package/dist/chunks/{LpSegmented-DzFYP0ZF.js → LpSegmented-Dws26WdN.js} +1 -1
  56. package/dist/chunks/{LpSelect-Dffg5r4I.js → LpSelect-Crzu6LUQ.js} +1 -1
  57. package/dist/chunks/{LpShift-DAgS_d2x.js → LpShift-ta3RpuH8.js} +30 -13
  58. package/dist/chunks/{LpSidebar-DPri9uyz.js → LpSidebar-WQuItmR1.js} +2 -2
  59. package/dist/chunks/{LpSidebarNav-CV1rU6La.js → LpSidebarNav-C1glQiiz.js} +2 -2
  60. package/dist/chunks/{LpSidebarRow-C5jXpcRL.js → LpSidebarRow-D9WaZsrT.js} +1 -1
  61. package/dist/chunks/{LpSlider-DUd4gXnw.js → LpSlider-Cdfz-6mn.js} +1 -1
  62. package/dist/chunks/{LpStat-SyVb5JXm.js → LpStat-QhKwBDs9.js} +1 -1
  63. package/dist/chunks/{LpStatus-C6GqSHJj.js → LpStatus-CbPi3oxy.js} +2 -2
  64. package/dist/chunks/{LpStepMark-CPGVj1XG.js → LpStepMark-CUawCMEq.js} +1 -1
  65. package/dist/chunks/{LpStepper-DBNRA6Q2.js → LpStepper-Beyt0G17.js} +1 -1
  66. package/dist/chunks/{LpSwap-M7nFeW3C.js → LpSwap-qCOwgde2.js} +1 -1
  67. package/dist/chunks/{LpSwitch-D-mcaA6C.js → LpSwitch-CNOw_N5u.js} +2 -2
  68. package/dist/chunks/{LpTabFace-uoZgD_SN.js → LpTabFace-0ZdqEZMe.js} +1 -1
  69. package/dist/chunks/{LpTableFilter-CILHffC1.js → LpTableFilter-C6Wt5mUr.js} +3 -3
  70. package/dist/chunks/{LpTagsInput-DD8-xe3F.js → LpTagsInput-CJCyr2p3.js} +2 -2
  71. package/dist/chunks/{LpTextarea-CQg-dVxN.js → LpTextarea-B0U99u5R.js} +1 -1
  72. package/dist/chunks/{LpThemeStudio-yiEdFJQL.js → LpThemeStudio-B6ZDjqiv.js} +4 -4
  73. package/dist/chunks/{LpThemeSwitcher-DKkCFaFk.js → LpThemeSwitcher-FJLcLfjq.js} +2 -2
  74. package/dist/chunks/{LpTimeField-Dio7Tj9A.js → LpTimeField-BHM_MEPZ.js} +1 -1
  75. package/dist/chunks/{LpToaster-C7hHkGOA.js → LpToaster-DjkgZepp.js} +1 -1
  76. package/dist/chunks/{LpTopologyCanvas-B1YFfMaq.js → LpTopologyCanvas-DKn0Xcz4.js} +4 -4
  77. package/dist/chunks/{LpTour-BGUKUxSZ.js → LpTour-iZNl_vdy.js} +1 -1
  78. package/dist/chunks/{LpTruncate-DEHWL1OA.js → LpTruncate-BCbRf7G5.js} +1 -1
  79. package/dist/chunks/{LpUptimeBar-CXgzU5ym.js → LpUptimeBar-CDctaMWR.js} +2 -2
  80. package/dist/chunks/{LpVideoCompress-CvxNdyjp.js → LpVideoCompress-Cg0g2-S0.js} +2 -2
  81. package/dist/chunks/{LpVideoPlayer-FMuxTAU8.js → LpVideoPlayer-B624umtt.js} +3 -3
  82. package/dist/chunks/{LpWorkspaceSwitcher-CrUVSI7x.js → LpWorkspaceSwitcher-CyD9tXvM.js} +2 -2
  83. package/dist/chunks/{RevealToggle-D9Vs88kM.js → RevealToggle-O2lhG5Be.js} +1 -1
  84. package/dist/chunks/{TourCard-yrLqv4Oz.js → TourCard-CCHkCcWP.js} +3 -3
  85. package/dist/chunks/{VideoVolume-CzL5mZPE.js → VideoVolume-wsbvgXKK.js} +2 -2
  86. package/dist/components/DropzoneLinkField.vue.js +1 -1
  87. package/dist/components/FieldWrapper.vue.js +1 -1
  88. package/dist/components/LpActionBar.vue.js +1 -1
  89. package/dist/components/LpAddressInput.vue.js +1 -1
  90. package/dist/components/LpAlert.vue.js +1 -1
  91. package/dist/components/LpAppShell.vue.js +1 -1
  92. package/dist/components/LpAutocomplete.vue.js +1 -1
  93. package/dist/components/LpBadge.vue.js +1 -1
  94. package/dist/components/LpBottomNav.vue.js +1 -1
  95. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  96. package/dist/components/LpButton.vue.js +1 -1
  97. package/dist/components/LpCard.vue.js +1 -1
  98. package/dist/components/LpChart.vue.js +2 -2
  99. package/dist/components/LpCheckbox.vue.js +1 -1
  100. package/dist/components/LpCodeBlock.vue.js +1 -1
  101. package/dist/components/LpCodeDisplay.vue.js +1 -1
  102. package/dist/components/LpColorPicker.vue.js +1 -1
  103. package/dist/components/LpCommandPalette.vue.js +2 -2
  104. package/dist/components/LpConfirmDialog.vue.js +1 -1
  105. package/dist/components/LpConfirmHost.vue.js +1 -1
  106. package/dist/components/LpCopyButton.vue.js +1 -1
  107. package/dist/components/LpCountdown.vue.js +1 -1
  108. package/dist/components/LpDataList.vue.js +1 -1
  109. package/dist/components/LpDatePicker.vue.js +1 -1
  110. package/dist/components/LpDateRangePicker.vue.js +1 -1
  111. package/dist/components/LpDropzone.vue.js +1 -1
  112. package/dist/components/LpEditable.vue.js +1 -1
  113. package/dist/components/LpFileTree.vue.js +1 -1
  114. package/dist/components/LpFileTreeNode.vue.js +1 -1
  115. package/dist/components/LpFormField.vue.js +1 -1
  116. package/dist/components/LpHintLayer.vue.js +1 -1
  117. package/dist/components/LpHints.vue.js +1 -1
  118. package/dist/components/LpImageEditor.vue.js +1 -1
  119. package/dist/components/LpInfraNode.vue.js +1 -1
  120. package/dist/components/LpInput.vue.js +1 -1
  121. package/dist/components/LpInspector.vue.js +1 -1
  122. package/dist/components/LpLightbox.vue.js +1 -1
  123. package/dist/components/LpLoader.vue.js +1 -1
  124. package/dist/components/LpLogInspector.vue.js +1 -1
  125. package/dist/components/LpLogViewer.vue.js +1 -1
  126. package/dist/components/LpMapPicker.vue.js +1 -1
  127. package/dist/components/LpMediaGallery.vue.js +1 -1
  128. package/dist/components/LpModalNav.vue.js +1 -1
  129. package/dist/components/LpNotificationBell.vue.js +1 -1
  130. package/dist/components/LpNotificationFeed.vue.js +1 -1
  131. package/dist/components/LpNumberField.vue.js +1 -1
  132. package/dist/components/LpOtpInput.vue.js +1 -1
  133. package/dist/components/LpPaletteTheme.vue.js +1 -1
  134. package/dist/components/LpPasswordInput.vue.js +1 -1
  135. package/dist/components/LpPhoneInput.vue.js +1 -1
  136. package/dist/components/LpPie.vue.js +1 -1
  137. package/dist/components/LpPipeline.vue.js +1 -1
  138. package/dist/components/LpRadioGroup.vue.js +1 -1
  139. package/dist/components/LpRangeSlider.vue.js +1 -1
  140. package/dist/components/LpRepeater.vue.js +1 -1
  141. package/dist/components/LpScramble.vue.js +1 -1
  142. package/dist/components/LpSecretField.vue.js +1 -1
  143. package/dist/components/LpSegmented.vue.js +1 -1
  144. package/dist/components/LpSelect.vue.js +1 -1
  145. package/dist/components/LpShift.vue.d.ts +1 -3
  146. package/dist/components/LpShift.vue.js +1 -1
  147. package/dist/components/LpSidebar.vue.js +1 -1
  148. package/dist/components/LpSidebarNav.vue.js +1 -1
  149. package/dist/components/LpSidebarRow.vue.js +1 -1
  150. package/dist/components/LpSlider.vue.js +1 -1
  151. package/dist/components/LpStat.vue.js +1 -1
  152. package/dist/components/LpStatus.vue.js +1 -1
  153. package/dist/components/LpStepMark.vue.js +1 -1
  154. package/dist/components/LpStepper.vue.js +1 -1
  155. package/dist/components/LpSwap.vue.js +1 -1
  156. package/dist/components/LpSwitch.vue.js +1 -1
  157. package/dist/components/LpTabFace.vue.js +1 -1
  158. package/dist/components/LpTable.vue.js +14 -12
  159. package/dist/components/LpTableFilter.vue.js +1 -1
  160. package/dist/components/LpTabs.vue.js +1 -1
  161. package/dist/components/LpTagsInput.vue.js +1 -1
  162. package/dist/components/LpTextarea.vue.js +1 -1
  163. package/dist/components/LpThemeStudio.vue.js +1 -1
  164. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  165. package/dist/components/LpTimeField.vue.js +1 -1
  166. package/dist/components/LpToaster.vue.js +1 -1
  167. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  168. package/dist/components/LpTour.vue.js +1 -1
  169. package/dist/components/LpTruncate.vue.js +1 -1
  170. package/dist/components/LpUptimeBar.vue.js +1 -1
  171. package/dist/components/LpVideoCompress.vue.js +1 -1
  172. package/dist/components/LpVideoPlayer.vue.js +1 -1
  173. package/dist/components/LpVideoTrim.vue.js +3 -3
  174. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  175. package/dist/components/RevealToggle.vue.js +1 -1
  176. package/dist/components/TourCard.vue.js +1 -1
  177. package/dist/components/VideoVolume.vue.js +1 -1
  178. package/dist/composables/useAppUpdate.js +12 -1
  179. package/dist/index.js +74 -74
  180. package/package.json +1 -1
  181. package/src/components/LpShift.vue +52 -17
  182. package/src/components/LpTable.vue +13 -2
  183. package/src/composables/useAppUpdate.ts +16 -1
@@ -24,9 +24,14 @@
24
24
  *
25
25
  * The mechanism — measure the natural size, pin it in pixels, tween — lives in
26
26
  * `useShift`, and LpModal drives its panel with the same composable.
27
+ *
28
+ * Whether a tween is holding the box (`resizing`) is read through a ref to the
29
+ * component (`defineExpose`), not from the slot: a slot prop is read by this
30
+ * component's render, which would render the whole content again at both ends
31
+ * of every tween. See docs/migrations/lpshift-resizing-slot.md.
27
32
  */
28
33
  import { isElement, isHTMLElement, nodeWindow } from "../composables/useView"
29
- import { computed, onBeforeUnmount, watch } from "vue"
34
+ import { computed, onBeforeUnmount, onUpdated, watch } from "vue"
30
35
  import { prefersReducedMotion } from "../composables/easing"
31
36
  import { useShift, type ShiftAxis } from "../composables/useShift"
32
37
 
@@ -289,6 +294,48 @@ onBeforeUnmount(() => {
289
294
  clearTimeout(rememberTimer)
290
295
  })
291
296
 
297
+ /*
298
+ * The tween's classes go onto the box from here, not from the template. A
299
+ * render that read `tweening` or `resizing` would run again at the start and
300
+ * the end of every tween, and the slot runs inside that render: the whole
301
+ * content re-rendered twice per resize for two classes on one element. A
302
+ * table's sixty rows were each rendered again, and TransitionGroup measured
303
+ * every one of them again, for nothing in the rows having changed.
304
+ *
305
+ * Applied after every render too: a class bound by the parent replaces the
306
+ * element's whole class attribute when it changes, and these are put back.
307
+ */
308
+ const shiftClasses = computed(() => {
309
+ const on = [
310
+ tweening.value && !props.disabled
311
+ ? `${transitionClass.value} duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none`
312
+ : "",
313
+ resizing.value ? "overflow-hidden" : "",
314
+ // While the box holds a width its content has outgrown, a flex row would
315
+ // squeeze its children to fit (a button's keys folding into a column)
316
+ // instead of letting the easing edge clip them.
317
+ resizing.value && props.axis !== "height" ? "[&>*]:shrink-0" : "",
318
+ resizing.value && props.anchor === "end" ? "justify-end" : "",
319
+ ]
320
+ return on.join(" ").split(" ").filter(Boolean)
321
+ })
322
+ let applied: readonly string[] = []
323
+ function applyShiftClasses() {
324
+ const box = el.value
325
+ if (!box) return
326
+ const next = shiftClasses.value
327
+ for (const name of applied) if (!next.includes(name)) box.classList.remove(name)
328
+ for (const name of next) box.classList.add(name)
329
+ applied = next
330
+ }
331
+ watch(shiftClasses, applyShiftClasses, { flush: "post" })
332
+ onUpdated(() => {
333
+ // The parent's class binding may have rewritten the attribute: what was
334
+ // applied is gone from the element, so all of it goes back on.
335
+ applied = []
336
+ applyShiftClasses()
337
+ })
338
+
292
339
  defineExpose({ resizing })
293
340
  </script>
294
341
 
@@ -297,21 +344,9 @@ defineExpose({ resizing })
297
344
  smaller than the content it already holds, and without the clip that
298
345
  content spills past the easing edge. Dropped once settled, so nothing
299
346
  that legitimately overflows — a dropdown, a focus ring — is clipped at
300
- rest. -->
301
- <div
302
- ref="el"
303
- :class="[
304
- tweening && !disabled
305
- ? `${transitionClass} duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none`
306
- : '',
307
- resizing ? 'overflow-hidden' : '',
308
- // While the box holds a width its content has outgrown, a flex row would
309
- // squeeze its children to fit (a button's keys folding into a column)
310
- // instead of letting the easing edge clip them.
311
- resizing && axis !== 'height' ? '[&>*]:shrink-0' : '',
312
- resizing && anchor === 'end' ? 'justify-end' : '',
313
- ]"
314
- >
315
- <slot :resizing="resizing" />
347
+ rest. The classes are put on in script (`applyShiftClasses`), so a
348
+ tween starting or ending never renders the content again. -->
349
+ <div ref="el">
350
+ <slot />
316
351
  </div>
317
352
  </template>
@@ -1736,6 +1736,17 @@ const motionIds = computed(() => (headerless.value ? allEntries.value : viewEntr
1736
1736
  let shownIds: readonly string[] = []
1737
1737
  const bigSwap = computed(() => rowChanges(shownIds, motionIds.value) > ROW_MOTION_LIMIT)
1738
1738
  let settleFlip = false
1739
+ /*
1740
+ * Whether the rows carry their own enter, leave and move transitions, read by
1741
+ * the templates through these two computeds and never as `animating` itself:
1742
+ * the motion window closing flips `animating` 600 ms after every change, and
1743
+ * a template reading it re-renders the whole table for it. TransitionGroup then
1744
+ * measures every row again (it records positions on each render), sixty forced
1745
+ * reads for a switch that, after a big swap, was off already. A computed whose
1746
+ * value does not change does not re-render anything.
1747
+ */
1748
+ const rowMotion = computed(() => animating.value && !props.virtualRowHeight && !bigSwap.value)
1749
+ const cardMotion = computed(() => animating.value && !bigSwap.value)
1739
1750
  watch(
1740
1751
  motionIds,
1741
1752
  (ids) => {
@@ -2131,7 +2142,7 @@ usePaletteCommands(() => {
2131
2142
  <TransitionGroup
2132
2143
  tag="tbody"
2133
2144
  name="lp-row"
2134
- :css="animating && !virtualRowHeight && !bigSwap"
2145
+ :css="rowMotion"
2135
2146
  :move-class="bigSwap ? 'lp-row-still' : undefined"
2136
2147
  :class="busy && displayRows.length ? 'opacity-60 transition-opacity' : 'transition-opacity'"
2137
2148
  :aria-busy="busy || undefined"
@@ -2296,7 +2307,7 @@ usePaletteCommands(() => {
2296
2307
  :key="activeLayout"
2297
2308
  tag="div"
2298
2309
  name="lp-row"
2299
- :css="animating && !bigSwap"
2310
+ :css="cardMotion"
2300
2311
  :move-class="bigSwap ? 'lp-row-still' : undefined"
2301
2312
  v-bind="scrollAttrs"
2302
2313
  class="min-h-0 transition-opacity"
@@ -1,4 +1,5 @@
1
1
  import { onScopeDispose, readonly, ref, type Ref } from "vue"
2
+ import { prefersReducedMotion } from "./easing"
2
3
 
3
4
  /*
4
5
  * A new build is out and every open tab should run it, without anyone losing
@@ -20,6 +21,10 @@ import { onScopeDispose, readonly, ref, type Ref } from "vue"
20
21
  * window scroll position, and anything the app already keeps in storage, such
21
22
  * as form drafts. After the reload `restored` is true for the app to say
22
23
  * "updated" if it wants to.
24
+ *
25
+ * A visible reload does not blink: the page fades out before it and fades back
26
+ * in after it, so the swap reads as one change rather than a flash of blank.
27
+ * Under reduced motion, and in a hidden tab, it simply reloads.
23
28
  */
24
29
 
25
30
  export interface AppUpdateOptions {
@@ -51,6 +56,8 @@ export interface AppUpdate {
51
56
  const OVERLAY = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"], [data-state="open"][data-side]'
52
57
  const INPUT_EVENTS = ["pointerdown", "keydown", "wheel", "touchstart", "input"] as const
53
58
  const CARRY_MS = 60_000
59
+ const FADE_OUT_MS = 220
60
+ const FADE_IN_MS = 320
54
61
 
55
62
  interface Carried {
56
63
  href: string
@@ -94,6 +101,9 @@ export function useAppUpdate(options: AppUpdateOptions): AppUpdate {
94
101
  const carried = readCarried(key)
95
102
  if (carried && carried.href === location.href && Date.now() - carried.at < CARRY_MS) {
96
103
  restored.value = true
104
+ if (!prefersReducedMotion()) {
105
+ document.documentElement.animate([{ opacity: 0 }, { opacity: 1 }], { duration: FADE_IN_MS, easing: "cubic-bezier(0.2, 0, 0, 1)" })
106
+ }
97
107
  const top = carried.top
98
108
  const restore = () => window.scrollTo({ top, behavior: "instant" })
99
109
  requestAnimationFrame(() => requestAnimationFrame(restore))
@@ -106,7 +116,12 @@ export function useAppUpdate(options: AppUpdateOptions): AppUpdate {
106
116
  } catch {
107
117
  // Storage off: the reload still happens, only the scroll is lost.
108
118
  }
109
- location.reload()
119
+ if (document.hidden || prefersReducedMotion()) {
120
+ location.reload()
121
+ return
122
+ }
123
+ const fade = document.documentElement.animate([{ opacity: 1 }, { opacity: 0 }], { duration: FADE_OUT_MS, easing: "ease-in", fill: "forwards" })
124
+ fade.finished.then(() => location.reload(), () => location.reload())
110
125
  }
111
126
 
112
127
  function safe(): boolean {