@leavepulse/ui 0.87.0 → 0.88.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 (243) hide show
  1. package/dist/assets/{LpCommandPalette-DCZB4EAf.css → LpCommandPalette-CO_5TFOQ.css} +38 -19
  2. package/dist/assets/{LpDropzone-CfL3Vrx4.css → LpDropzone-CVGUQqgY.css} +10 -10
  3. package/dist/assets/{LpImageEditor-y3B-aW3H.css → LpImageEditor-D_ECuegW.css} +6 -6
  4. package/dist/assets/{LpTable-ClBFQeTd.css → LpTable-BqZNpWgC.css} +25 -25
  5. package/dist/chunks/{ContextMenuItems-D0jObgzN.js → ContextMenuItems-LujPZSWM.js} +2 -1
  6. package/dist/chunks/{DropzoneLinkField-DhwYRnvd.js → DropzoneLinkField-Ckuh8zFC.js} +2 -2
  7. package/dist/chunks/{IconCloseButton-CmWOJAXG.js → IconCloseButton-C_V7qngw.js} +1 -1
  8. package/dist/chunks/{LpActionBar-Cx_MSZl9.js → LpActionBar-43WHmTsM.js} +1 -1
  9. package/dist/chunks/{LpAddressInput-CfqQR5jE.js → LpAddressInput-BCqR0zmv.js} +5 -5
  10. package/dist/chunks/{LpAlert-CYtbpRYW.js → LpAlert-Q0h7Gucu.js} +1 -1
  11. package/dist/chunks/{LpAppShell-BQG98odS.js → LpAppShell-BWaVRsDB.js} +3 -3
  12. package/dist/chunks/{LpAutocomplete-VNID5zdy.js → LpAutocomplete-YPxAPHEo.js} +5 -5
  13. package/dist/chunks/{LpButton-DSHioIfD.js → LpButton-BvVoF-PO.js} +1 -1
  14. package/dist/chunks/{LpCheckbox-P7tEw8GO.js → LpCheckbox-C9VnPIQf.js} +1 -1
  15. package/dist/chunks/{LpCodeBlock-BNThgUdQ.js → LpCodeBlock-BgWcg2mk.js} +3 -3
  16. package/dist/chunks/{LpCodeDisplay-DsiE-Qy9.js → LpCodeDisplay-DBi1YPLp.js} +3 -3
  17. package/dist/chunks/{LpColorPicker-EXpwcX-t.js → LpColorPicker-KGMG8QEh.js} +4 -4
  18. package/dist/chunks/{LpConfirmDialog-DPiPVRn_.js → LpConfirmDialog-y6k0ryWR.js} +2 -2
  19. package/dist/chunks/{LpConfirmHost-Ddiljlog.js → LpConfirmHost-D-X0tDym.js} +1 -1
  20. package/dist/chunks/{LpContextMenu-DYpDm6iI.js → LpContextMenu-DEFirphS.js} +3 -3
  21. package/dist/chunks/{LpContextSwitcher-BPDSkKyA.js → LpContextSwitcher-COw6GHPW.js} +1 -1
  22. package/dist/chunks/{LpCopyButton-CxX682t4.js → LpCopyButton-BGzw95Xc.js} +2 -2
  23. package/dist/chunks/{LpDataList-DCtF7krm.js → LpDataList-B56Gu6Yt.js} +3 -3
  24. package/dist/chunks/{LpDatePicker-BElEHVm6.js → LpDatePicker-Dlyvgsan.js} +2 -2
  25. package/dist/chunks/{LpDateRangePicker-vLt-drEu.js → LpDateRangePicker-CAn2yGbn.js} +1 -1
  26. package/dist/chunks/{LpDrawer-C97k-3L2.js → LpDrawer-iRHwJOGn.js} +4 -4
  27. package/dist/chunks/{LpDropdownMenu-BPEsqZCN.js → LpDropdownMenu-CdCe7D2d.js} +3 -3
  28. package/dist/chunks/{LpEditable-CeRAzYMv.js → LpEditable-DfRUAGkA.js} +2 -2
  29. package/dist/chunks/{LpEmptyState-DrHBR3yh.js → LpEmptyState-DLOm07KH.js} +1 -1
  30. package/dist/chunks/{LpFileTree-BcQgsw4e.js → LpFileTree-BN_zybBN.js} +3 -3
  31. package/dist/chunks/{LpFileTreeNode-Dk2WqkPF.js → LpFileTreeNode-CC48DbT5.js} +3 -3
  32. package/dist/chunks/{LpHintLayer-CUSRLtLu.js → LpHintLayer-nuMqfzvy.js} +3 -3
  33. package/dist/chunks/{LpHints-BF40CP4_.js → LpHints-ByAd3a76.js} +3 -3
  34. package/dist/chunks/{LpHoverCard-CHE9jAJG.js → LpHoverCard-DYguY2AP.js} +10 -61
  35. package/dist/chunks/{LpInput-BU5IsOlE.js → LpInput-DGMpxrcg.js} +1 -1
  36. package/dist/chunks/{LpInspector-CEko6-pS.js → LpInspector-oTfkOdYd.js} +2 -2
  37. package/dist/chunks/{LpLightbox-REei-y5e.js → LpLightbox-a24V_0Wk.js} +19 -7
  38. package/dist/chunks/{LpLogInspector-Bwz351LY.js → LpLogInspector-BhJTJIst.js} +1 -1
  39. package/dist/chunks/{LpLogViewer-BCkqdH66.js → LpLogViewer-BuIzr_6h.js} +4 -4
  40. package/dist/chunks/{LpMediaGallery-BF1Bch3v.js → LpMediaGallery-B7DOV_aL.js} +2 -2
  41. package/dist/chunks/{LpModal-BK2T5uIG.js → LpModal-D1E7rQzw.js} +5 -5
  42. package/dist/chunks/{LpModalNav-Dk4eHpg6.js → LpModalNav-DivqT1NV.js} +5 -4
  43. package/dist/chunks/{LpNotificationBell-CMwUXhFg.js → LpNotificationBell-KRgdRZs-.js} +4 -4
  44. package/dist/chunks/{LpNotificationFeed-BXsRp9jm.js → LpNotificationFeed-CMGYU8c-.js} +5 -5
  45. package/dist/chunks/{LpNumberField-QUMXvpqx.js → LpNumberField-C0LSu-tl.js} +1 -1
  46. package/dist/chunks/{LpPaletteTheme-wiJtP5hV.js → LpPaletteTheme-CLwO-SDf.js} +3 -3
  47. package/dist/chunks/{LpPasswordInput-W9AZfzaL.js → LpPasswordInput-LPVI7D3v.js} +2 -2
  48. package/dist/chunks/{LpPhoneInput-DsRv-Qla.js → LpPhoneInput-BdglZsGH.js} +5 -5
  49. package/dist/chunks/{LpPie-pbrNFbRP.js → LpPie-CsiBXe-e.js} +3 -3
  50. package/dist/chunks/{LpPipeline-BHWmvOiv.js → LpPipeline-DVcoSs--.js} +1 -1
  51. package/dist/chunks/{LpPopover-BQSuxtai.js → LpPopover-w2rr4x7n.js} +2 -2
  52. package/dist/chunks/{LpRadio-CWA5Eb3M.js → LpRadio-4KyxeSz4.js} +1 -1
  53. package/dist/chunks/{LpRadioGroup-D94KMStE.js → LpRadioGroup-Cr_fmrBY.js} +1 -1
  54. package/dist/chunks/{LpRangeSlider-CkP9hYUC.js → LpRangeSlider-DbaITHtC.js} +1 -1
  55. package/dist/chunks/{LpRelativeTime-0zI-44ve.js → LpRelativeTime-CN5bWD73.js} +1 -1
  56. package/dist/chunks/{LpRepeater-DLLcbQ6J.js → LpRepeater-C0rns2xW.js} +1 -1
  57. package/dist/chunks/{LpSecretField-Au7ZQTtm.js → LpSecretField-BwrZ4TfT.js} +5 -5
  58. package/dist/chunks/{LpSection-RKCwB0xN.js → LpSection-CwsfNR7N.js} +1 -1
  59. package/dist/chunks/{LpSegmented-DD8MPj4j.js → LpSegmented-DzFYP0ZF.js} +1 -1
  60. package/dist/chunks/{LpSelect-BeXz1TWh.js → LpSelect-Dffg5r4I.js} +5 -5
  61. package/dist/chunks/{LpSidebar-DUFlf6Uu.js → LpSidebar-DQtCYlT4.js} +4 -4
  62. package/dist/chunks/{LpSidebarNav-C34YEZIR.js → LpSidebarNav-CFcI7--7.js} +1 -1
  63. package/dist/chunks/{LpSidebarRow-BeEGgsZi.js → LpSidebarRow-C5jXpcRL.js} +2 -2
  64. package/dist/chunks/{LpSlider-BCy5rjwK.js → LpSlider-DUd4gXnw.js} +1 -1
  65. package/dist/chunks/{LpStat-eVz7iUfD.js → LpStat-SyVb5JXm.js} +1 -1
  66. package/dist/chunks/{LpSwitch-D7hmVsif.js → LpSwitch-D-mcaA6C.js} +1 -1
  67. package/dist/chunks/{LpTableFilter-BGFNKyUX.js → LpTableFilter-CILHffC1.js} +3 -3
  68. package/dist/chunks/{LpTagsInput-D9uOSlvf.js → LpTagsInput-DD8-xe3F.js} +2 -2
  69. package/dist/chunks/{LpTextarea-DwE-lQt4.js → LpTextarea-CQg-dVxN.js} +1 -1
  70. package/dist/chunks/{LpThemeStudio-COn2H76d.js → LpThemeStudio-BPPctXgd.js} +4 -4
  71. package/dist/chunks/{LpThemeSwitcher-B3R3nNid.js → LpThemeSwitcher-B3c44xFH.js} +4 -4
  72. package/dist/chunks/{LpTimeField-Mi8H1g_9.js → LpTimeField-Dio7Tj9A.js} +1 -1
  73. package/dist/chunks/{LpToaster-DOJn6Vey.js → LpToaster-Cdx1tnHB.js} +2 -2
  74. package/dist/chunks/{LpTooltip-Bz_rrq5S.js → LpTooltip-BKde8Xjv.js} +43 -7
  75. package/dist/chunks/{LpTopologyCanvas-5RFMP4gR.js → LpTopologyCanvas-CQ0l6A5h.js} +3 -3
  76. package/dist/chunks/{LpTour-DUI_hoPv.js → LpTour-BGUKUxSZ.js} +1 -1
  77. package/dist/chunks/{LpTruncate-CynJhsEw.js → LpTruncate-C_C8eiIE.js} +2 -2
  78. package/dist/chunks/{LpUptimeBar-G7PfNJUH.js → LpUptimeBar-Dt6SY_-m.js} +1 -1
  79. package/dist/chunks/{LpVideoCompress-CPUxXh-c.js → LpVideoCompress-CvxNdyjp.js} +2 -2
  80. package/dist/chunks/{LpVideoPlayer-BbCjEQAx.js → LpVideoPlayer-FMuxTAU8.js} +4 -4
  81. package/dist/chunks/{LpWindow-Dp_0nNqg.js → LpWindow-DsPeiDCZ.js} +1 -1
  82. package/dist/chunks/{LpWorkspaceSwitcher-bjkncjSl.js → LpWorkspaceSwitcher-DCIiOQt_.js} +4 -4
  83. package/dist/chunks/{PickerField-CjOTUXDK.js → PickerField-DzF3UVnX.js} +1 -1
  84. package/dist/chunks/{RevealToggle-Bl1NcfxB.js → RevealToggle-BsYxY6Oz.js} +2 -2
  85. package/dist/chunks/{TourCard-BovXtMbO.js → TourCard-yrLqv4Oz.js} +1 -1
  86. package/dist/chunks/{VideoVolume-DE3i1ree.js → VideoVolume-CzL5mZPE.js} +2 -2
  87. package/dist/components/ContextMenuItems.vue.js +1 -1
  88. package/dist/components/DropzoneLinkField.vue.js +1 -1
  89. package/dist/components/IconCloseButton.vue.js +1 -1
  90. package/dist/components/LpActionBar.vue.js +1 -1
  91. package/dist/components/LpAddressInput.vue.js +1 -1
  92. package/dist/components/LpAlert.vue.js +1 -1
  93. package/dist/components/LpAppShell.vue.js +1 -1
  94. package/dist/components/LpAutocomplete.vue.js +1 -1
  95. package/dist/components/LpButton.vue.js +1 -1
  96. package/dist/components/LpChart.vue.js +2 -2
  97. package/dist/components/LpCheckbox.vue.js +1 -1
  98. package/dist/components/LpCodeBlock.vue.js +1 -1
  99. package/dist/components/LpCodeDisplay.vue.js +1 -1
  100. package/dist/components/LpColorPicker.vue.js +1 -1
  101. package/dist/components/LpCommandPalette.vue.d.ts +2 -0
  102. package/dist/components/LpCommandPalette.vue.js +69 -33
  103. package/dist/components/LpConfirmDialog.vue.js +1 -1
  104. package/dist/components/LpConfirmHost.vue.js +1 -1
  105. package/dist/components/LpContextMenu.vue.js +1 -1
  106. package/dist/components/LpContextSwitcher.vue.js +1 -1
  107. package/dist/components/LpCopyButton.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/LpDock.vue.js +1 -1
  112. package/dist/components/LpDrawer.vue.js +1 -1
  113. package/dist/components/LpDropdownMenu.vue.js +1 -1
  114. package/dist/components/LpDropzone.vue.js +5 -5
  115. package/dist/components/LpEditable.vue.js +1 -1
  116. package/dist/components/LpEmptyState.vue.js +1 -1
  117. package/dist/components/LpFileTree.vue.js +1 -1
  118. package/dist/components/LpFileTreeNode.vue.js +1 -1
  119. package/dist/components/LpHintLayer.vue.js +1 -1
  120. package/dist/components/LpHints.vue.js +1 -1
  121. package/dist/components/LpHoverCard.vue.d.ts +3 -3
  122. package/dist/components/LpHoverCard.vue.js +1 -1
  123. package/dist/components/LpImageEditor.vue.js +4 -4
  124. package/dist/components/LpInput.vue.js +1 -1
  125. package/dist/components/LpInspector.vue.js +1 -1
  126. package/dist/components/LpLightbox.vue.js +1 -1
  127. package/dist/components/LpLogInspector.vue.js +1 -1
  128. package/dist/components/LpLogViewer.vue.js +1 -1
  129. package/dist/components/LpMediaGallery.vue.js +1 -1
  130. package/dist/components/LpModal.vue.js +1 -1
  131. package/dist/components/LpModalNav.vue.js +1 -1
  132. package/dist/components/LpNotificationBell.vue.js +1 -1
  133. package/dist/components/LpNotificationFeed.vue.js +1 -1
  134. package/dist/components/LpNumberField.vue.js +1 -1
  135. package/dist/components/LpPaletteTheme.vue.js +1 -1
  136. package/dist/components/LpPasswordInput.vue.js +1 -1
  137. package/dist/components/LpPhoneInput.vue.js +1 -1
  138. package/dist/components/LpPie.vue.js +1 -1
  139. package/dist/components/LpPipeline.vue.js +1 -1
  140. package/dist/components/LpPopover.vue.js +1 -1
  141. package/dist/components/LpRadio.vue.js +1 -1
  142. package/dist/components/LpRadioGroup.vue.js +1 -1
  143. package/dist/components/LpRangeSlider.vue.js +1 -1
  144. package/dist/components/LpRelativeTime.vue.js +1 -1
  145. package/dist/components/LpRepeater.vue.js +1 -1
  146. package/dist/components/LpSecretField.vue.js +1 -1
  147. package/dist/components/LpSection.vue.js +1 -1
  148. package/dist/components/LpSegmented.vue.js +1 -1
  149. package/dist/components/LpSelect.vue.js +1 -1
  150. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  151. package/dist/components/LpSidebar.vue.js +1 -1
  152. package/dist/components/LpSidebarNav.vue.js +1 -1
  153. package/dist/components/LpSidebarRow.vue.js +1 -1
  154. package/dist/components/LpSlider.vue.js +1 -1
  155. package/dist/components/LpStat.vue.js +1 -1
  156. package/dist/components/LpSwitch.vue.js +1 -1
  157. package/dist/components/LpTable.vue.js +16 -13
  158. package/dist/components/LpTableFilter.vue.js +1 -1
  159. package/dist/components/LpTabs.vue.js +1 -1
  160. package/dist/components/LpTagsInput.vue.js +1 -1
  161. package/dist/components/LpTextarea.vue.js +1 -1
  162. package/dist/components/LpThemeStudio.vue.js +1 -1
  163. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  164. package/dist/components/LpTimeField.vue.js +1 -1
  165. package/dist/components/LpToaster.vue.js +1 -1
  166. package/dist/components/LpTooltip.vue.d.ts +9 -0
  167. package/dist/components/LpTooltip.vue.js +1 -1
  168. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  169. package/dist/components/LpTour.vue.js +1 -1
  170. package/dist/components/LpTruncate.vue.js +1 -1
  171. package/dist/components/LpUptimeBar.vue.js +1 -1
  172. package/dist/components/LpVideoCompress.vue.js +1 -1
  173. package/dist/components/LpVideoPlayer.vue.js +1 -1
  174. package/dist/components/LpVideoTrim.vue.js +4 -4
  175. package/dist/components/LpWindow.vue.js +1 -1
  176. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  177. package/dist/components/PickerField.vue.js +1 -1
  178. package/dist/components/RevealToggle.vue.js +1 -1
  179. package/dist/components/TourCard.vue.js +1 -1
  180. package/dist/components/VideoVolume.vue.js +1 -1
  181. package/dist/components/badge.d.ts +1 -1
  182. package/dist/components/badge.js +1 -1
  183. package/dist/components/dropdown.d.ts +14 -0
  184. package/dist/components/dropdown.js +7 -1
  185. package/dist/components/menuWhen.js +1 -1
  186. package/dist/components/modalDepth.d.ts +1 -0
  187. package/dist/components/modalDepth.js +2 -0
  188. package/dist/composables/useReveal.js +3 -3
  189. package/dist/composables/useTouchHold.d.ts +19 -0
  190. package/dist/composables/useTouchHold.js +60 -0
  191. package/dist/index.js +72 -71
  192. package/package.json +1 -1
  193. package/src/components/ContextMenuItems.vue +2 -1
  194. package/src/components/IconCloseButton.vue +1 -1
  195. package/src/components/LpAlert.vue +1 -1
  196. package/src/components/LpAutocomplete.vue +4 -4
  197. package/src/components/LpButton.vue +1 -1
  198. package/src/components/LpCheckbox.vue +1 -1
  199. package/src/components/LpCodeBlock.vue +2 -2
  200. package/src/components/LpColorPicker.vue +3 -3
  201. package/src/components/LpCommandPalette.vue +89 -7
  202. package/src/components/LpContextMenu.vue +2 -2
  203. package/src/components/LpDataList.vue +1 -1
  204. package/src/components/LpDrawer.vue +7 -4
  205. package/src/components/LpDropdownMenu.vue +2 -2
  206. package/src/components/LpDropzone.vue +2 -2
  207. package/src/components/LpEditable.vue +2 -2
  208. package/src/components/LpEmptyState.vue +1 -1
  209. package/src/components/LpFileTreeNode.vue +1 -1
  210. package/src/components/LpHintLayer.vue +2 -2
  211. package/src/components/LpHoverCard.vue +11 -80
  212. package/src/components/LpImageEditor.vue +1 -1
  213. package/src/components/LpInput.vue +1 -1
  214. package/src/components/LpLightbox.vue +20 -3
  215. package/src/components/LpModal.vue +4 -4
  216. package/src/components/LpModalNav.vue +2 -1
  217. package/src/components/LpNumberField.vue +1 -1
  218. package/src/components/LpPhoneInput.vue +5 -5
  219. package/src/components/LpPopover.vue +2 -2
  220. package/src/components/LpRadio.vue +1 -1
  221. package/src/components/LpRadioGroup.vue +1 -1
  222. package/src/components/LpRangeSlider.vue +1 -1
  223. package/src/components/LpSection.vue +9 -6
  224. package/src/components/LpSegmented.vue +1 -1
  225. package/src/components/LpSelect.vue +4 -4
  226. package/src/components/LpSlider.vue +1 -1
  227. package/src/components/LpStat.vue +1 -1
  228. package/src/components/LpSwitch.vue +1 -1
  229. package/src/components/LpTable.vue +6 -1
  230. package/src/components/LpTableFilter.vue +1 -1
  231. package/src/components/LpTagsInput.vue +2 -2
  232. package/src/components/LpTextarea.vue +1 -1
  233. package/src/components/LpThemeSwitcher.vue +2 -2
  234. package/src/components/LpTimeField.vue +1 -1
  235. package/src/components/LpToaster.vue +1 -1
  236. package/src/components/LpTooltip.vue +59 -5
  237. package/src/components/badge.ts +1 -1
  238. package/src/components/dropdown.ts +24 -0
  239. package/src/components/modalDepth.ts +6 -0
  240. package/src/composables/useReveal.ts +7 -4
  241. package/src/composables/useTouchHold.ts +92 -0
  242. package/src/locales/uk.ts +1 -0
  243. package/src/tokens/utilities.css +34 -0
@@ -320,6 +320,8 @@ export type PaletteLabels = Partial<Record<PaletteMode, string>> & {
320
320
  pinned?: string
321
321
  select: string
322
322
  close: string
323
+ /** The close button that stands in for Esc on a touch screen. */
324
+ dismiss: string
323
325
  /** Hint for leaving a mode, shown only while inside one. */
324
326
  back: string
325
327
  searching: string
@@ -527,6 +529,7 @@ const DEFAULT_LABELS: Required<PaletteLabels> = {
527
529
  running: "Running…",
528
530
  select: "select",
529
531
  close: "close",
532
+ dismiss: "Close",
530
533
  back: "back",
531
534
  searching: "Searching…",
532
535
  searchFailed: "Search is unavailable right now.",
@@ -649,6 +652,43 @@ function actionsOf(c: Command | undefined): Command[] {
649
652
 
650
653
  const selectedActions = computed(() => actionsOf(flat.value[selectedIndex.value]))
651
654
 
655
+ /*
656
+ * On a phone the palette is a sheet over the whole screen, and the on-screen
657
+ * keyboard takes the bottom of it without resizing the layout viewport: the
658
+ * last rows would sit under the keys. The visible part is tracked instead and
659
+ * handed to the CSS, so the list ends where the keyboard begins.
660
+ */
661
+ const viewport = ref<Record<string, string>>({})
662
+ function trackViewport() {
663
+ const vv = typeof window === "undefined" ? undefined : window.visualViewport
664
+ // The whole visible rectangle, not only its height: a page wider than the
665
+ // phone is zoomed out to fit, and a fixed sheet then measures the wider
666
+ // layout instead of the screen the person is looking at.
667
+ viewport.value = vv
668
+ ? {
669
+ "--lp-cmd-vh": `${vv.height}px`,
670
+ "--lp-cmd-vw": `${vv.width}px`,
671
+ "--lp-cmd-vx": `${vv.offsetLeft}px`,
672
+ "--lp-cmd-vy": `${vv.offsetTop}px`,
673
+ }
674
+ : {}
675
+ }
676
+ watch(
677
+ () => props.open,
678
+ (open, _, onCleanup) => {
679
+ const vv = typeof window === "undefined" ? undefined : window.visualViewport
680
+ if (!open || !vv) return
681
+ trackViewport()
682
+ vv.addEventListener("resize", trackViewport)
683
+ vv.addEventListener("scroll", trackViewport)
684
+ onCleanup(() => {
685
+ vv.removeEventListener("resize", trackViewport)
686
+ vv.removeEventListener("scroll", trackViewport)
687
+ })
688
+ },
689
+ { immediate: true },
690
+ )
691
+
652
692
  /*
653
693
  * Tab is free for this: the rows are out of the tab order and the box is the
654
694
  * only field, so Tab otherwise just walks off to the dialog's edge. Without
@@ -666,6 +706,18 @@ function onTab(event: KeyboardEvent) {
666
706
  event.stopPropagation()
667
707
  openActions(c)
668
708
  }
709
+ /*
710
+ * A long press on a phone, a right click elsewhere: the row's actions. On a
711
+ * touch screen there is no hover to make a row the active one, so this is the
712
+ * only way to the actions of any row but the first. A row without actions
713
+ * keeps the browser's own menu.
714
+ */
715
+ function onRowContextMenu(event: MouseEvent, c: Command) {
716
+ if (!actionsOf(c).length) return
717
+ event.preventDefault()
718
+ openActions(c)
719
+ }
720
+
669
721
  function openActions(c: Command) {
670
722
  const actions = actionsOf(c)
671
723
  if (!actions.length) return
@@ -1573,7 +1625,7 @@ const guardOutside = useOutsideGuard()
1573
1625
  ref="contentRef"
1574
1626
  class="lp-cmd-dialog fixed left-1/2 top-[12vh] flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]"
1575
1627
  :class="[frosted ? (layer.nested ? 'lp-glass-overlay lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', previewPane && 'lp-cmd-split']"
1576
- :style="{ zIndex: layer.panel }"
1628
+ :style="{ zIndex: layer.panel, ...viewport }"
1577
1629
  :data-pane="paneOpen ? 'open' : undefined"
1578
1630
  :data-editing="editing ? '' : undefined"
1579
1631
  @open-auto-focus.prevent
@@ -1640,13 +1692,22 @@ const guardOutside = useOutsideGuard()
1640
1692
  :aria-controls="listShown ? listId : undefined"
1641
1693
  :aria-activedescendant="activeId"
1642
1694
  :placeholder="promptCommand?.prompt?.placeholder ?? mode?.childPlaceholder ?? placeholder"
1643
- class="h-12 min-w-0 flex-1 bg-transparent text-sm text-ink outline-none placeholder:text-muted"
1695
+ class="lp-entry h-12 min-w-0 flex-1 bg-transparent text-sm text-ink outline-none placeholder:text-muted"
1644
1696
  @keydown.enter.prevent="runSelected"
1645
1697
  @keydown.backspace="onBackspace"
1646
1698
  @keydown.esc="onEscape"
1647
1699
  @keydown.tab="onTab"
1648
1700
  />
1649
- <kbd class="shrink-0 rounded-small border border-line px-1.5 py-0.5 text-[10px] text-muted">{{ text.esc }}</kbd>
1701
+ <kbd class="shrink-0 rounded-small border border-line px-1.5 py-0.5 text-[10px] text-muted [@media(hover:none)_and_(pointer:coarse)]:hidden">{{ text.esc }}</kbd>
1702
+ <!-- A finger has no Esc, and tapping the dim edge is not there when
1703
+ the palette fills the screen: the way out is a button. -->
1704
+ <button
1705
+ type="button"
1706
+ class="hidden shrink-0 rounded-inner px-2 py-1.5 text-sm font-medium text-brand outline-none focus-visible:ring-2 focus-visible:ring-ring [@media(hover:none)_and_(pointer:coarse)]:inline-flex"
1707
+ @click="emit('update:open', false)"
1708
+ >
1709
+ {{ text.dismiss }}
1710
+ </button>
1650
1711
  </div>
1651
1712
 
1652
1713
  <div v-if="promptCommand" class="flex flex-col gap-3 p-4">
@@ -1659,8 +1720,8 @@ const guardOutside = useOutsideGuard()
1659
1720
  the only way to the row's actions and pin. On a pointer the same
1660
1721
  buttons live in the footer, so the list never shifts under this strip. -->
1661
1722
  <div v-if="active && (canPin || selectedActions.length) && !promptCommand" class="hidden items-center justify-end gap-2 border-b border-line px-3 py-1 [@media(hover:none)_and_(pointer:coarse)]:flex">
1662
- <LpButton v-if="selectedActions.length" size="xs" variant="ghost" @click="openActions(active)">{{ text.actions }} <span class="text-muted">Tab</span></LpButton>
1663
- <LpButton v-if="canPin" size="xs" variant="ghost" :aria-label="isPinned(active) ? text.unpin : text.pin" :aria-pressed="isPinned(active)" @click="togglePin(active)"><LpIcon name="lucide:star" :size="14" />{{ isPinned(active) ? text.unpin : text.pin }} <span class="text-muted">Alt+P</span></LpButton>
1723
+ <LpButton v-if="selectedActions.length" size="xs" variant="ghost" @click="openActions(active)">{{ text.actions }}</LpButton>
1724
+ <LpButton v-if="canPin" size="xs" variant="ghost" :aria-label="isPinned(active) ? text.unpin : text.pin" :aria-pressed="isPinned(active)" @click="togglePin(active)"><LpIcon name="lucide:star" :size="14" />{{ isPinned(active) ? text.unpin : text.pin }}</LpButton>
1664
1725
  </div>
1665
1726
  <!-- Results -->
1666
1727
  <div class="flex min-h-0 flex-1" @pointermove="onPointerMove">
@@ -1711,6 +1772,7 @@ const guardOutside = useOutsideGuard()
1711
1772
  : 'text-ink hover:bg-surface-soft/60',
1712
1773
  ]"
1713
1774
  @click="onRowClick($event, c)"
1775
+ @contextmenu="onRowContextMenu($event, c)"
1714
1776
  @pointerenter="onPointerEnter(c)"
1715
1777
  @pointerleave="onPointerLeave"
1716
1778
  >
@@ -1761,12 +1823,13 @@ const guardOutside = useOutsideGuard()
1761
1823
  </template>
1762
1824
  </span>
1763
1825
  </span>
1764
- <MenuShortcut v-if="c.shortcut" :combo="c.shortcut" />
1826
+ <!-- Keys mean nothing to a finger: hidden on a touch screen. -->
1827
+ <MenuShortcut v-if="c.shortcut" :combo="c.shortcut" class="[@media(hover:none)_and_(pointer:coarse)]:hidden" />
1765
1828
  <!-- The prefix that opens this mode from the box, so the
1766
1829
  shortcut is learned by seeing it rather than reading docs. -->
1767
1830
  <kbd
1768
1831
  v-else-if="c.prefix && c.children"
1769
- class="shrink-0 rounded-small border border-line px-1.5 font-mono text-[11px] whitespace-pre text-muted"
1832
+ class="shrink-0 rounded-small border border-line px-1.5 font-mono text-[11px] whitespace-pre text-muted [@media(hover:none)_and_(pointer:coarse)]:hidden"
1770
1833
  >{{ c.prefix }}</kbd>
1771
1834
  <!-- Drills in rather than runs: worth saying before the click,
1772
1835
  since the two look identical otherwise. -->
@@ -1943,6 +2006,25 @@ const guardOutside = useOutsideGuard()
1943
2006
  .lp-cmd-pane {
1944
2007
  width: var(--lp-cmd-pane);
1945
2008
  }
2009
+ /*
2010
+ * A phone: a sheet over the whole visible screen rather than a card floating
2011
+ * a tenth of the way down, which left a band of nothing above it and let the
2012
+ * keyboard cover its lower half. Keyed to touch as well as width, so a narrow
2013
+ * desktop window keeps the card.
2014
+ */
2015
+ @media (hover: none) and (pointer: coarse) and (max-width: 639px) {
2016
+ .lp-cmd-dialog {
2017
+ inset: var(--lp-cmd-vy, 0) auto auto var(--lp-cmd-vx, 0);
2018
+ translate: none;
2019
+ width: var(--lp-cmd-vw, 100%);
2020
+ height: var(--lp-cmd-vh, 100dvh);
2021
+ max-height: var(--lp-cmd-vh, 100dvh);
2022
+ border-width: 0;
2023
+ border-radius: 0;
2024
+ padding-top: env(safe-area-inset-top);
2025
+ padding-bottom: env(safe-area-inset-bottom);
2026
+ }
2027
+ }
1946
2028
  .lp-cmd-dialog[data-editing] .lp-cmd-pane {
1947
2029
  width: 100%;
1948
2030
  }
@@ -14,7 +14,7 @@ import { hints } from "../composables/useHints"
14
14
  import { HINT_SIGNALS, noteContextTarget } from "../composables/useHintSystem"
15
15
  import ContextMenuItems from "./ContextMenuItems.vue"
16
16
  import MenuTitle from "./MenuTitle.vue"
17
- import { POPOVER_PANEL } from "./dropdown"
17
+ import { PANEL_FIT, POPOVER_PANEL } from "./dropdown"
18
18
  import { useOutsideGuard, usePortalTarget, useView, viewDocument } from "../composables/useView"
19
19
 
20
20
  export interface ContextMenuItemDef {
@@ -82,7 +82,7 @@ const props = withDefaults(defineProps<{
82
82
  const enabled = computed(() => props.always || props.items.length > 0)
83
83
  const hinted = computed(() => enabled.value && !props.noHint)
84
84
 
85
- const PANEL = `${POPOVER_PANEL} z-(--z-popover) min-w-48 p-1 outline-none`
85
+ const PANEL = `${POPOVER_PANEL} ${PANEL_FIT} z-(--z-popover) min-w-48 p-1 outline-none`
86
86
 
87
87
  // A menu opened over a text field takes focus, and on close reka hands it to the
88
88
  // trigger, so the caret and the selection the user right-clicked are gone — the
@@ -125,7 +125,7 @@ const pad = computed(() => (props.size === "sm" ? "py-1.5 text-xs" : "py-2.5 tex
125
125
  class="group/row grid items-baseline"
126
126
  :class="[ROW[layout], pad, divided && i > 0 ? 'border-t border-line' : '']"
127
127
  >
128
- <dt class="flex min-w-0 items-center gap-1 text-muted">
128
+ <dt class="flex min-w-0 items-center gap-1 text-muted [overflow-wrap:anywhere]">
129
129
  <slot :name="`label-${keyOf(item)}`" :item="item">
130
130
  <slot name="label" :item="item">{{ item.label }}</slot>
131
131
  </slot>
@@ -197,11 +197,14 @@ onBeforeUnmount(release)
197
197
  const contentClass = computed(() => {
198
198
  const surface = frosted.value ? (layer.value.nested ? "lp-glass-overlay lp-glass-overlay--nested" : "lp-glass-overlay") : "bg-surface-raised"
199
199
  const base = `fixed z-(--z-modal) flex flex-col border-line ${surface} shadow-panel outline-none`
200
+ // The panel pads itself by the safe area of each screen edge it touches, so
201
+ // on a phone with a notch or a home indicator its header, close button and
202
+ // last row stay clear of them while its surface still reaches the edge.
200
203
  const map: Record<Direction, string> = {
201
- left: "inset-y-0 left-0 border-r rounded-r-card",
202
- right: "inset-y-0 right-0 border-l rounded-l-card",
203
- top: "inset-x-0 top-0 border-b rounded-b-card",
204
- bottom: "inset-x-0 bottom-0 border-t rounded-t-card",
204
+ left: "inset-y-0 left-0 border-r rounded-r-card pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)]",
205
+ right: "inset-y-0 right-0 border-l rounded-l-card pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] pr-[env(safe-area-inset-right)]",
206
+ top: "inset-x-0 top-0 border-b rounded-b-card pt-[env(safe-area-inset-top)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]",
207
+ bottom: "inset-x-0 bottom-0 border-t rounded-t-card pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]",
205
208
  }
206
209
  return [base, map[dir.value]]
207
210
  })
@@ -11,7 +11,7 @@ import { HINT_SIGNALS } from "../composables/useHintSystem"
11
11
  import { DropdownMenuContent, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuTrigger } from "reka-ui"
12
12
  import ContextMenuItems from "./ContextMenuItems.vue"
13
13
  import type { ContextMenuItemDef } from "./LpContextMenu.vue"
14
- import { POPOVER_PANEL } from "./dropdown"
14
+ import { PANEL_FIT, POPOVER_PANEL } from "./dropdown"
15
15
  import MenuTitle from "./MenuTitle.vue"
16
16
  import { useOutsideGuard, usePortalTarget } from "../composables/useView"
17
17
 
@@ -33,7 +33,7 @@ const props = withDefaults(defineProps<{
33
33
  rightClickToo?: boolean
34
34
  }>(), { palette: undefined })
35
35
 
36
- const PANEL = `${POPOVER_PANEL} z-(--z-popover) min-w-44 p-1 outline-none`
36
+ const PANEL = `${POPOVER_PANEL} ${PANEL_FIT} z-(--z-popover) min-w-44 p-1 outline-none`
37
37
  // reka names the menu after its trigger, and aria-labelledby beats aria-label:
38
38
  // a `title` only names the menu with that link taken off. MenuTitle itself is
39
39
  // hidden from assistive tech, so nothing is announced twice.
@@ -42,7 +42,7 @@ import { useMergedAttrs } from "../composables/useMergedClass"
42
42
  import { useReveal } from "../composables/useReveal"
43
43
  import { PopoverAnchor, PopoverContent, PopoverPortal, PopoverRoot } from "reka-ui"
44
44
  import DropzoneLinkField from "./DropzoneLinkField.vue"
45
- import { POPOVER_PANEL } from "./dropdown"
45
+ import { PANEL_FIT, POPOVER_PANEL } from "./dropdown"
46
46
  import LpIcon from "./LpIcon.vue"
47
47
  import { inTextField } from "../composables/textEntry"
48
48
  import { useOutsideGuard, usePortalTarget, useView, viewWindow } from "../composables/useView"
@@ -568,7 +568,7 @@ const guardOutside = useOutsideGuard()
568
568
  side="bottom"
569
569
  align="start"
570
570
  :side-offset="6"
571
- :class="POPOVER_PANEL"
571
+ :class="[POPOVER_PANEL, PANEL_FIT]"
572
572
  class="z-(--z-popover) w-80 rounded-card p-3 outline-none"
573
573
  @open-auto-focus.prevent
574
574
  >
@@ -264,7 +264,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() => "relative inline-
264
264
  >
265
265
  <span
266
266
  aria-hidden="true"
267
- class="invisible col-start-1 row-start-1 min-w-[4ch]"
267
+ class="lp-entry invisible col-start-1 row-start-1 min-w-[4ch]"
268
268
  :class="[PAD[size], multiline ? 'whitespace-pre-wrap break-words' : 'whitespace-pre']"
269
269
  >{{ (draft || placeholder || " ") + (multiline ? "\n" : "\u200a") }}</span>
270
270
  <component
@@ -280,7 +280,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() => "relative inline-
280
280
  :disabled="disabled"
281
281
  :readonly="readonly"
282
282
  :rows="multiline ? 1 : undefined"
283
- class="col-start-1 row-start-1 w-full min-w-0 resize-none overflow-hidden bg-transparent text-ink outline-none placeholder:text-muted"
283
+ class="lp-entry col-start-1 row-start-1 w-full min-w-0 resize-none overflow-hidden bg-transparent text-ink outline-none placeholder:text-muted"
284
284
  :class="PAD[size]"
285
285
  @input="draft = ($event.target as HTMLInputElement).value"
286
286
  @keydown="onFieldKey"
@@ -43,7 +43,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
43
43
  >
44
44
  <LpIcon :name="icon" :size="compact ? 16 : 22" />
45
45
  </div>
46
- <div class="flex flex-col gap-1">
46
+ <div class="flex max-w-full flex-col gap-1 [overflow-wrap:anywhere]">
47
47
  <p class="font-semibold text-ink" :class="compact ? 'text-sm' : ''">{{ title }}</p>
48
48
  <!-- The slot is for a line a string cannot carry: a command, a link. -->
49
49
  <p v-if="description || $slots.description" class="max-w-sm text-sm text-muted">
@@ -302,7 +302,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
302
302
  <input
303
303
  v-if="isRenaming"
304
304
  ref="renameField"
305
- class="w-full min-w-0 rounded-sm border bg-surface-soft px-1 text-sm text-ink outline-none"
305
+ class="lp-entry w-full min-w-0 rounded-sm border bg-surface-soft px-1 text-sm text-ink outline-none"
306
306
  :class="renameInvalid ? 'border-danger' : 'border-line-strong'"
307
307
  :value="renameValue"
308
308
  :aria-label="renameLabel"
@@ -19,7 +19,7 @@ import { PopoverAnchor, PopoverContent, PopoverPortal, PopoverRoot } from "reka-
19
19
  import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue"
20
20
  import { hints } from "../composables/useHints"
21
21
  import { useLabels } from "../composables/useLpConfig"
22
- import { PANEL_SURFACE, POPOVER_ANIM } from "./dropdown"
22
+ import { PANEL_FIT, PANEL_SURFACE, POPOVER_ANIM } from "./dropdown"
23
23
  import LpBadge from "./LpBadge.vue"
24
24
  import LpButton from "./LpButton.vue"
25
25
  import { useOutsideGuard, usePortalTarget } from "../composables/useView"
@@ -147,7 +147,7 @@ const guardOutside = useOutsideGuard()
147
147
  side="bottom"
148
148
  :side-offset="8"
149
149
  :collision-padding="8"
150
- :class="[PANEL_SURFACE, POPOVER_ANIM, 'z-(--z-popover) w-72 rounded-card p-3 text-sm text-ink outline-none']"
150
+ :class="[PANEL_SURFACE, POPOVER_ANIM, PANEL_FIT, 'z-(--z-popover) w-72 rounded-card p-3 text-sm text-ink outline-none']"
151
151
  >
152
152
  <p class="flex items-center gap-2 font-medium">
153
153
  <LpBadge tone="brand" size="sm">{{ labels.badge }}</LpBadge>
@@ -34,10 +34,11 @@ import {
34
34
  HoverCardRoot,
35
35
  HoverCardTrigger,
36
36
  } from "reka-ui"
37
- import { computed, onBeforeUnmount, watch } from "vue"
37
+ import { computed, mergeProps, useAttrs, watch } from "vue"
38
38
  import { mergeClass } from "../composables/useMergedClass"
39
+ import { useTouchHold } from "../composables/useTouchHold"
39
40
  import { useOutsideGuard, usePortalTarget } from "../composables/useView"
40
- import { POPOVER_PANEL } from "./dropdown"
41
+ import { PANEL_FIT, POPOVER_PANEL } from "./dropdown"
41
42
 
42
43
  const props = withDefaults(
43
44
  defineProps<{
@@ -81,96 +82,32 @@ const props = withDefaults(
81
82
  const open = defineModel<boolean>("open", { default: false })
82
83
 
83
84
  const panel = computed(() =>
84
- mergeClass(POPOVER_PANEL, "z-(--z-popover) w-72 rounded-card p-4 text-sm text-ink outline-none", props.panelClass),
85
+ mergeClass(POPOVER_PANEL, PANEL_FIT, "z-(--z-popover) w-72 rounded-card p-4 text-sm text-ink outline-none", props.panelClass),
85
86
  )
86
87
 
87
88
  const portalTo = usePortalTarget()
88
89
  const guardOutside = useOutsideGuard()
89
90
 
90
- /*
91
- * Press-and-hold. A hold that drifts is a scroll, not a hold, so moving past
92
- * a few pixels cancels it. Once it has opened the card, the click the finger
93
- * produces on lifting is swallowed: the reader asked for the preview, not for
94
- * the link under it.
95
- */
96
- const SLOP = 8
97
- /* A long press may produce no click at all; past this the next click is the user's own. */
98
- const CLICK_AFTER_RELEASE_MS = 350
99
- let holdTimer = 0
100
- /* The finger the hold belongs to: a second one must not move or end it. */
101
- let holdPointer: number | null = null
102
- let start = { x: 0, y: 0 }
103
- let held = false
104
-
105
- function cancelHold() {
106
- window.clearTimeout(holdTimer)
107
- holdTimer = 0
108
- }
109
-
110
- function onPointerUp(event: PointerEvent) {
111
- if (holdPointer !== null && event.pointerId !== holdPointer) return
112
- holdPointer = null
113
- cancelHold()
114
- if (held) window.setTimeout(() => (held = false), CLICK_AFTER_RELEASE_MS)
115
- }
116
-
117
- // A cancelled press produces no click, so there is nothing left to swallow.
118
- function onPointerCancel(event?: PointerEvent) {
119
- if (event && holdPointer !== null && event.pointerId !== holdPointer) return
120
- holdPointer = null
121
- cancelHold()
122
- held = false
123
- }
124
-
125
- function onPointerDown(event: PointerEvent) {
126
- // Any new press starts over: on a hybrid laptop the mouse click after a hold
127
- // must not be taken for that hold's own click.
128
- if (holdTimer && event.pointerId !== holdPointer) return
129
- held = false
130
- if (event.pointerType !== "touch" || props.touch !== "hold" || props.disabled) return
131
- holdPointer = event.pointerId
132
- start = { x: event.clientX, y: event.clientY }
133
- cancelHold()
134
- holdTimer = window.setTimeout(() => {
135
- holdTimer = 0
136
- held = true
137
- open.value = true
138
- }, props.holdDelay)
139
- }
140
-
141
- function onPointerMove(event: PointerEvent) {
142
- if (!holdTimer || event.pointerId !== holdPointer) return
143
- if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > SLOP) cancelHold()
144
- }
145
-
146
- function onClickCapture(event: MouseEvent) {
147
- if (!held) return
148
- held = false
149
- event.preventDefault()
150
- event.stopPropagation()
151
- }
152
-
153
- // A long press would otherwise also bring up the system's own menu for the link.
154
- function onContextMenu(event: Event) {
155
- if (holdTimer || held) event.preventDefault()
156
- }
91
+ // Press-and-hold opens the card on a touch screen, where there is no hover.
92
+ const hold = useTouchHold({ enabled: () => props.touch === "hold" && !props.disabled, delay: () => props.holdDelay, onHold: () => (open.value = true) })
157
93
 
158
94
  watch(
159
95
  () => props.disabled,
160
96
  (off) => {
161
97
  if (!off) return
162
- onPointerCancel()
98
+ hold.cancel()
163
99
  open.value = false
164
100
  },
165
101
  )
166
102
 
167
- onBeforeUnmount(cancelHold)
168
-
169
103
  // Several roots (trigger and portal), so Vue cannot hand attributes on by
170
104
  // itself and dropped them: a trigger wrapped around this one from outside (an
171
105
  // LpPopover's, as-child) lost its id, ARIA state and click, and its popover
172
106
  // never opened. They go to the trigger, which merges them onto the slot's element.
173
107
  defineOptions({ inheritAttrs: false })
108
+ // The caller's own listeners on the trigger run beside the hold's, not instead of them.
109
+ const attrs = useAttrs()
110
+ const triggerAttrs = computed(() => mergeProps(attrs, hold.handlers))
174
111
  </script>
175
112
 
176
113
  <template>
@@ -178,14 +115,8 @@ defineOptions({ inheritAttrs: false })
178
115
  <HoverCardRoot v-else v-model:open="open" :open-delay="openDelay" :close-delay="closeDelay">
179
116
  <HoverCardTrigger
180
117
  as-child
181
- v-bind="$attrs"
118
+ v-bind="triggerAttrs"
182
119
  :class="touch === 'hold' ? '[-webkit-touch-callout:none]' : undefined"
183
- @pointerdown="onPointerDown"
184
- @pointermove="onPointerMove"
185
- @pointerup="onPointerUp"
186
- @pointercancel="onPointerCancel"
187
- @click.capture="onClickCapture"
188
- @contextmenu="onContextMenu"
189
120
  >
190
121
  <slot name="trigger" />
191
122
  </HoverCardTrigger>
@@ -558,7 +558,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
558
558
  tabindex="0"
559
559
  role="application"
560
560
  :aria-label="text.canvas"
561
- class="relative w-full overflow-hidden rounded-card border border-line bg-surface-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
561
+ class="relative w-full touch-none overflow-hidden rounded-card border border-line bg-surface-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
562
562
  :style="frameStyle"
563
563
  @wheel.prevent="onWheel"
564
564
  @pointerdown="zoom.onPointerDown"
@@ -209,7 +209,7 @@ usePaletteControl({ label: () => props.label, disabled: () => field.value.disabl
209
209
  :aria-invalid="isInvalidState || undefined"
210
210
  :aria-describedby="mergeDescribedBy(attrs['aria-describedby'], field.describedBy)"
211
211
  :class="[
212
- 'lp-autofill min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted',
212
+ 'lp-autofill lp-entry min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted',
213
213
  clearable && '[&::-webkit-search-cancel-button]:hidden',
214
214
  hasLeading ? 'pl-2' : padX[size ?? 'md'],
215
215
  hasTrailing ? '' : padX[size ?? 'md'],
@@ -364,8 +364,18 @@ function rotate() {
364
364
  // Swipe to page through, but only while the image is at fit scale — once zoomed
365
365
  // a horizontal drag is a pan, and stealing it would make the image unreachable.
366
366
  let swipeStart: { x: number; y: number } | null = null
367
+ // Fingers on the stage. A second one makes the gesture a pinch, and a pinch
368
+ // whose fingers part sideways is not a swipe: it zoomed and then paged away
369
+ // from what it zoomed into.
370
+ const fingers = new Set<number>()
367
371
 
368
372
  function onPointerDown(e: PointerEvent) {
373
+ fingers.add(e.pointerId)
374
+ if (fingers.size > 1) {
375
+ swipeStart = null
376
+ if (!isVideo.value) zoom.onPointerDown(e)
377
+ return
378
+ }
369
379
  if (isVideo.value) {
370
380
  // A drag on the player's bar is a seek or a volume change, never a page.
371
381
  swipeStart = (e.target as Element).closest?.("[data-lp-player-controls]") ? null : { x: e.clientX, y: e.clientY }
@@ -375,7 +385,14 @@ function onPointerDown(e: PointerEvent) {
375
385
  swipeStart = zoom.zoomed.value ? null : { x: e.clientX, y: e.clientY }
376
386
  }
377
387
 
388
+ function onPointerCancel(e: PointerEvent) {
389
+ fingers.delete(e.pointerId)
390
+ swipeStart = null
391
+ if (!isVideo.value) zoom.onPointerUp(e)
392
+ }
393
+
378
394
  function onPointerUp(e: PointerEvent) {
395
+ fingers.delete(e.pointerId)
379
396
  if (!isVideo.value) zoom.onPointerUp(e)
380
397
  if (!swipeStart) return
381
398
  const dx = e.clientX - swipeStart.x
@@ -534,7 +551,7 @@ const guardOutside = useOutsideGuard()
534
551
  @interact-outside="guardOutside"
535
552
  @escape-key-down="guardOutside"
536
553
  ref="contentRef"
537
- class="fixed inset-0 flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none"
554
+ class="fixed inset-0 flex flex-col pt-[env(safe-area-inset-top)] pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none"
538
555
  :style="{ zIndex: layer.panel }"
539
556
  @keydown="onKeydown"
540
557
  >
@@ -599,13 +616,13 @@ const guardOutside = useOutsideGuard()
599
616
  <!-- Stage -->
600
617
  <div
601
618
  ref="viewport"
602
- class="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden"
619
+ class="relative flex min-h-0 flex-1 touch-none items-center justify-center overflow-hidden"
603
620
  :class="isPanning ? 'cursor-grabbing' : isZoomed ? 'cursor-grab' : ''"
604
621
  @wheel="onStageWheel"
605
622
  @pointerdown="onPointerDown"
606
623
  @pointermove="zoom.onPointerMove"
607
624
  @pointerup="onPointerUp"
608
- @pointercancel="zoom.onPointerUp"
625
+ @pointercancel="onPointerCancel"
609
626
  @dblclick="onDoubleClick"
610
627
  @click="onViewportClick"
611
628
  >
@@ -15,7 +15,7 @@ import { useGlass } from "../composables/useGlass"
15
15
  import { useModelOverlayHistory, type OverlayHistoryMode } from "../composables/useOverlayHistory"
16
16
  import { useLabels } from "../composables/useLpConfig"
17
17
  import { useShift } from "../composables/useShift"
18
- import { useModalLayer } from "./modalDepth"
18
+ import { MODAL_MAX_HEIGHT, useModalLayer } from "./modalDepth"
19
19
  import { CLOSE_ICON } from "./dropdown"
20
20
  import LpIcon from "./LpIcon.vue"
21
21
  import LpScrollArea from "./LpScrollArea.vue"
@@ -379,7 +379,7 @@ const guardOutside = useOutsideGuard()
379
379
  the link to DialogDescription. -->
380
380
  <DialogContent
381
381
  class="pointer-events-auto flex min-h-0 items-start 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)]"
382
- :style="{ maxHeight: 'min(90vh, calc(100dvh - 2rem))' }"
382
+ :style="{ maxHeight: MODAL_MAX_HEIGHT }"
383
383
  v-bind="describedByAttrs"
384
384
  @open-auto-focus="onOpenAutoFocus"
385
385
  @interact-outside="onInteractOutside"
@@ -395,9 +395,9 @@ const guardOutside = useOutsideGuard()
395
395
  </aside>
396
396
  <div
397
397
  ref="panelRef"
398
- class="relative flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
398
+ class="relative flex min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
399
399
  :class="[frosted ? (layer.nested ? 'lp-glass-overlay lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
400
- :style="{ ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
400
+ :style="{ maxHeight: MODAL_MAX_HEIGHT, ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
401
401
  >
402
402
  <DialogTitle v-if="!(title || $slots.title) && ariaLabel" class="sr-only">
403
403
  {{ ariaLabel }}
@@ -18,6 +18,7 @@ import { useGlass } from "../composables/useGlass"
18
18
  import { useLabels } from "../composables/useLpConfig"
19
19
  import LpButton from "./LpButton.vue"
20
20
  import LpIcon from "./LpIcon.vue"
21
+ import { MODAL_MAX_HEIGHT } from "./modalDepth"
21
22
  import LpModal from "./LpModal.vue"
22
23
  import LpSidebar from "./LpSidebar.vue"
23
24
  import type { SidebarItem, SidebarSection } from "./sidebar"
@@ -132,7 +133,7 @@ const triggerClass = computed(() => TRIGGER_VISIBILITY[props.mobileBreakpoint])
132
133
  * mirrors LpModal's max-height, which is not a class this file can put in the
133
134
  * consuming app's scan.
134
135
  */
135
- const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" }
136
+ const railStyle = { maxHeight: MODAL_MAX_HEIGHT }
136
137
 
137
138
  /*
138
139
  * The rail is part of the dialog, so it takes the same surface: a glass
@@ -177,7 +177,7 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
177
177
  :id="field.id"
178
178
  :aria-describedby="describedBy"
179
179
  :aria-invalid="field.invalid || undefined"
180
- class="min-w-0 flex-1 bg-transparent text-ink outline-none placeholder:text-muted [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
180
+ class="lp-entry min-w-0 flex-1 bg-transparent text-ink outline-none placeholder:text-muted [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
181
181
  :class="steppers ? 'text-center @max-[8.5rem]:px-3 @max-[8.5rem]:text-left' : icon || $slots.leading ? 'pl-2 pr-3' : 'px-3'"
182
182
  />
183
183
  <!-- The unit sits between the value and the increment button, so a long one
@@ -55,7 +55,7 @@ import { useFieldNotice, type FieldNoticeOptions } from "../composables/useField
55
55
  import { useInputFilter } from "../composables/useInputFilter"
56
56
  import { useLabels } from "../composables/useLpConfig"
57
57
  import { type Country, filterCountries, flagEmoji, loadCountries, matchCountryByValue } from "./countries"
58
- import { OPTION_ITEM, optionStagger, COMBOBOX_PANEL, POPOVER_PANEL } from "./dropdown"
58
+ import { OPTION_ITEM, optionStagger, COMBOBOX_FIT, COMBOBOX_PANEL, POPOVER_PANEL } from "./dropdown"
59
59
  import LpFieldNotice from "./LpFieldNotice.vue"
60
60
  import LpIcon from "./LpIcon.vue"
61
61
  import {
@@ -403,7 +403,7 @@ const guardOutside = useOutsideGuard()
403
403
  @escape-key-down="guardOutside"
404
404
  v-bind="COMBOBOX_PANEL"
405
405
  align="start"
406
- :class="[POPOVER_PANEL, 'z-(--z-popover) max-h-72 w-72 overflow-hidden']"
406
+ :class="[POPOVER_PANEL, 'z-(--z-popover) w-72 max-w-[calc(100vw-1rem)] overflow-hidden', COMBOBOX_FIT]"
407
407
  >
408
408
  <div class="border-b border-line p-2">
409
409
  <!--
@@ -417,11 +417,11 @@ const guardOutside = useOutsideGuard()
417
417
  :placeholder="searchPlaceholder ?? text.search"
418
418
  :aria-label="text.searchLabel"
419
419
  auto-focus
420
- class="h-(--size-control-sm) w-full rounded-inner border border-line bg-surface-soft px-2.5 text-sm text-ink outline-none placeholder:text-muted focus:border-brand"
420
+ class="lp-entry h-(--size-control-sm) w-full rounded-inner border border-line bg-surface-soft px-2.5 text-sm text-ink outline-none placeholder:text-muted focus:border-brand"
421
421
  @input="query = ($event.target as HTMLInputElement).value"
422
422
  />
423
423
  </div>
424
- <ComboboxViewport class="max-h-56 overflow-y-auto p-1">
424
+ <ComboboxViewport class="max-h-56 min-h-0 flex-1 overflow-y-auto p-1">
425
425
  <ComboboxEmpty class="px-3 py-4 text-center text-sm italic text-muted">
426
426
  {{ emptyText ?? text.empty }}
427
427
  </ComboboxEmpty>
@@ -454,7 +454,7 @@ const guardOutside = useOutsideGuard()
454
454
  :required="field.required || undefined"
455
455
  :aria-invalid="field.invalid || undefined"
456
456
  :aria-describedby="mergeDescribedBy(rest['aria-describedby'], field.describedBy, notice.message.value ? noticeId : undefined)"
457
- class="lp-autofill min-w-0 flex-1 self-stretch bg-transparent px-3 outline-none placeholder:text-muted"
457
+ class="lp-autofill lp-entry min-w-0 flex-1 self-stretch bg-transparent px-3 outline-none placeholder:text-muted"
458
458
  @beforeinput="onBeforeEdit"
459
459
  @paste="onPaste"
460
460
  @compositionend="onCompositionEnd"