@leavepulse/ui 0.87.1 → 0.89.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.
Files changed (244) 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-BqZNpWgC.css → LpTable-DAVNb-x2.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.d.ts +7 -0
  158. package/dist/components/LpTable.vue.js +48 -41
  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/LpTooltip.vue.d.ts +9 -0
  168. package/dist/components/LpTooltip.vue.js +1 -1
  169. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  170. package/dist/components/LpTour.vue.js +1 -1
  171. package/dist/components/LpTruncate.vue.js +1 -1
  172. package/dist/components/LpUptimeBar.vue.js +1 -1
  173. package/dist/components/LpVideoCompress.vue.js +1 -1
  174. package/dist/components/LpVideoPlayer.vue.js +1 -1
  175. package/dist/components/LpVideoTrim.vue.js +4 -4
  176. package/dist/components/LpWindow.vue.js +1 -1
  177. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  178. package/dist/components/PickerField.vue.js +1 -1
  179. package/dist/components/RevealToggle.vue.js +1 -1
  180. package/dist/components/TourCard.vue.js +1 -1
  181. package/dist/components/VideoVolume.vue.js +1 -1
  182. package/dist/components/badge.d.ts +1 -1
  183. package/dist/components/badge.js +1 -1
  184. package/dist/components/dropdown.d.ts +14 -0
  185. package/dist/components/dropdown.js +7 -1
  186. package/dist/components/menuWhen.js +1 -1
  187. package/dist/components/modalDepth.d.ts +1 -0
  188. package/dist/components/modalDepth.js +2 -0
  189. package/dist/composables/useReveal.js +3 -3
  190. package/dist/composables/useTouchHold.d.ts +19 -0
  191. package/dist/composables/useTouchHold.js +60 -0
  192. package/dist/index.js +72 -71
  193. package/package.json +1 -1
  194. package/src/components/ContextMenuItems.vue +2 -1
  195. package/src/components/IconCloseButton.vue +1 -1
  196. package/src/components/LpAlert.vue +1 -1
  197. package/src/components/LpAutocomplete.vue +4 -4
  198. package/src/components/LpButton.vue +1 -1
  199. package/src/components/LpCheckbox.vue +1 -1
  200. package/src/components/LpCodeBlock.vue +2 -2
  201. package/src/components/LpColorPicker.vue +3 -3
  202. package/src/components/LpCommandPalette.vue +89 -7
  203. package/src/components/LpContextMenu.vue +2 -2
  204. package/src/components/LpDataList.vue +1 -1
  205. package/src/components/LpDrawer.vue +7 -4
  206. package/src/components/LpDropdownMenu.vue +2 -2
  207. package/src/components/LpDropzone.vue +2 -2
  208. package/src/components/LpEditable.vue +2 -2
  209. package/src/components/LpEmptyState.vue +1 -1
  210. package/src/components/LpFileTreeNode.vue +1 -1
  211. package/src/components/LpHintLayer.vue +2 -2
  212. package/src/components/LpHoverCard.vue +11 -80
  213. package/src/components/LpImageEditor.vue +1 -1
  214. package/src/components/LpInput.vue +1 -1
  215. package/src/components/LpLightbox.vue +20 -3
  216. package/src/components/LpModal.vue +4 -4
  217. package/src/components/LpModalNav.vue +2 -1
  218. package/src/components/LpNumberField.vue +1 -1
  219. package/src/components/LpPhoneInput.vue +5 -5
  220. package/src/components/LpPopover.vue +2 -2
  221. package/src/components/LpRadio.vue +1 -1
  222. package/src/components/LpRadioGroup.vue +1 -1
  223. package/src/components/LpRangeSlider.vue +1 -1
  224. package/src/components/LpSection.vue +9 -6
  225. package/src/components/LpSegmented.vue +1 -1
  226. package/src/components/LpSelect.vue +4 -4
  227. package/src/components/LpSlider.vue +1 -1
  228. package/src/components/LpStat.vue +1 -1
  229. package/src/components/LpSwitch.vue +1 -1
  230. package/src/components/LpTable.vue +13 -1
  231. package/src/components/LpTableFilter.vue +1 -1
  232. package/src/components/LpTagsInput.vue +2 -2
  233. package/src/components/LpTextarea.vue +1 -1
  234. package/src/components/LpThemeSwitcher.vue +2 -2
  235. package/src/components/LpTimeField.vue +1 -1
  236. package/src/components/LpToaster.vue +1 -1
  237. package/src/components/LpTooltip.vue +59 -5
  238. package/src/components/badge.ts +1 -1
  239. package/src/components/dropdown.ts +24 -0
  240. package/src/components/modalDepth.ts +6 -0
  241. package/src/composables/useReveal.ts +7 -4
  242. package/src/composables/useTouchHold.ts +92 -0
  243. package/src/locales/uk.ts +1 -0
  244. package/src/tokens/utilities.css +34 -0
@@ -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"
@@ -7,7 +7,7 @@ import {
7
7
  } from "reka-ui"
8
8
  import { computed, useAttrs } from "vue"
9
9
  import { mergeClass } from "../composables/useMergedClass"
10
- import { POPOVER_PANEL } from "./dropdown"
10
+ import { PANEL_FIT, POPOVER_PANEL } from "./dropdown"
11
11
  import { useOutsideGuard, usePortalTarget } from "../composables/useView"
12
12
 
13
13
  // The trigger owns its attributes through the slot; fallthrough tags the panel.
@@ -37,7 +37,7 @@ const open = defineModel<boolean>("open")
37
37
  // Merged rather than appended, so `panelClass="w-80 p-0"` replaces the default
38
38
  // width and padding instead of racing them in stylesheet order.
39
39
  const panel = computed(() =>
40
- mergeClass(POPOVER_PANEL, "z-(--z-popover) w-64 rounded-card p-3 text-sm text-ink outline-none", props.panelClass),
40
+ mergeClass(POPOVER_PANEL, PANEL_FIT, "z-(--z-popover) w-64 rounded-card p-3 text-sm text-ink outline-none", props.panelClass),
41
41
  )
42
42
 
43
43
  /*
@@ -32,7 +32,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
32
32
  <RadioGroupItem
33
33
  :value="value"
34
34
  :disabled="disabled"
35
- class="flex size-[18px] shrink-0 items-center justify-center rounded-full border border-line-strong bg-surface-soft outline-none transition-[color,border-color,box-shadow,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] not-data-[state=checked]:group-hover/radio:border-brand active:scale-90 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring data-[state=checked]:border-brand disabled:cursor-not-allowed disabled:opacity-55"
35
+ class="lp-tap flex size-[18px] shrink-0 items-center justify-center rounded-full border border-line-strong bg-surface-soft outline-none transition-[color,border-color,box-shadow,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] not-data-[state=checked]:group-hover/radio:border-brand active:scale-90 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring data-[state=checked]:border-brand disabled:cursor-not-allowed disabled:opacity-55"
36
36
  >
37
37
  <RadioGroupIndicator
38
38
  class="flex data-[state=checked]:animate-[indicator-in_140ms_var(--ease-emphasized)]"
@@ -68,7 +68,7 @@ usePaletteControl({ disabled: () => field.value.disabled })
68
68
  :disabled="opt.disabled"
69
69
  :aria-describedby="opt.description ? `${uid}-${i}-desc` : undefined"
70
70
  :class="opt.description ? 'mt-px' : ''"
71
- class="flex size-[18px] shrink-0 items-center justify-center rounded-full border border-line-strong bg-surface-soft outline-none transition-[color,border-color,box-shadow,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] not-data-[state=checked]:group-hover/radio:border-brand active:scale-90 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring data-[state=checked]:border-brand disabled:cursor-not-allowed disabled:opacity-55"
71
+ class="lp-tap flex size-[18px] shrink-0 items-center justify-center rounded-full border border-line-strong bg-surface-soft outline-none transition-[color,border-color,box-shadow,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] not-data-[state=checked]:group-hover/radio:border-brand active:scale-90 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring data-[state=checked]:border-brand disabled:cursor-not-allowed disabled:opacity-55"
72
72
  >
73
73
  <RadioGroupIndicator
74
74
  class="flex data-[state=checked]:animate-[indicator-in_140ms_var(--ease-emphasized)]"
@@ -64,7 +64,7 @@ function onCommit(v: number[] | undefined) {
64
64
  const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, disabled: props.disabled }))
65
65
 
66
66
  const { class: rootClass, attrs: rest } = useMergedAttrs(
67
- `relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55 ${SLIDER_GLIDE}`, () => ownAttrs())
67
+ `lp-tap relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55 ${SLIDER_GLIDE}`, () => ownAttrs())
68
68
  const { dragging, onPointerDown } = useSliderDrag()
69
69
 
70
70
  const thumbClass =
@@ -112,12 +112,15 @@ const classes = computed(() =>
112
112
  line of their own even when they would have fitted beside the title.
113
113
  With it the column takes the space that is left and shrinks first.
114
114
 
115
- `break-words` covers the title AND the description in one place. Both
116
- routinely carry something with no spaces in it (a hostname, a URL, a
117
- container id), and an unbreakable run pushes straight out through the
118
- side of whatever card the section sits in. min-w-0 lets the column
119
- shrink; only this lets the text inside it give way. -->
120
- <div class="min-w-0 flex-1 break-words">
115
+ `overflow-wrap: anywhere` covers the title AND the description in one
116
+ place. Both routinely carry something with no spaces in it (a
117
+ hostname, a URL, a container id), and an unbreakable run pushes
118
+ straight out through the side of whatever card the section sits in.
119
+ min-w-0 lets the column shrink; only this lets the text inside it give
120
+ way. `break-words` was not enough: the title is an item of a flex row,
121
+ and break-word leaves its minimum width at the whole run, so a hash
122
+ in a title widened a phone page until the browser zoomed it out. -->
123
+ <div class="min-w-0 flex-1 [overflow-wrap:anywhere]">
121
124
  <div class="flex flex-wrap items-center gap-2">
122
125
  <LpIcon v-if="icon" :name="icon" :size="16" class="shrink-0 text-muted" />
123
126
  <component :is="as" :class="classes.title()">
@@ -135,7 +135,7 @@ usePaletteControl({ disabled: () => field.value.disabled })
135
135
  :value="opt.value"
136
136
  :disabled="opt.disabled"
137
137
  :aria-label="opt.ariaLabel ?? (opt.label ? undefined : opt.value)"
138
- class="relative inline-flex h-full items-center justify-center gap-1.5 rounded-inner font-medium outline-none transition-colors duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] focus-visible:ring-2 focus-visible:ring-ring data-[state=on]:text-ink data-[state=off]:text-muted data-[state=off]:hover:text-ink disabled:cursor-not-allowed disabled:opacity-55"
138
+ class="lp-tap relative inline-flex h-full items-center justify-center gap-1.5 rounded-inner font-medium outline-none transition-colors duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] focus-visible:ring-2 focus-visible:ring-ring data-[state=on]:text-ink data-[state=off]:text-muted data-[state=off]:hover:text-ink disabled:cursor-not-allowed disabled:opacity-55"
139
139
  :class="[block ? 'flex-1' : '', size === 'lg' ? 'px-4' : 'px-3', hovered === opt.value ? 'text-ink' : '']"
140
140
  @pointerenter="onEnter(opt)"
141
141
  @pointerup="releaseFocusAfterPointer"
@@ -43,7 +43,7 @@ import FieldWrapper from "./FieldWrapper.vue"
43
43
  import { useFieldBlur, useOwnField, mergeDescribedBy } from "../composables/useField"
44
44
  import { useLabels } from "../composables/useLpConfig"
45
45
  import { useMergedAttrs } from "../composables/useMergedClass"
46
- import { OPTION_ITEM, optionStagger, POPOVER_PANEL } from "./dropdown"
46
+ import { COMBOBOX_FIT, OPTION_ITEM, optionStagger, POPOVER_PANEL } from "./dropdown"
47
47
  import LpIcon from "./LpIcon.vue"
48
48
  import OptionContent from "./OptionContent.vue"
49
49
  import LpRollingText from "./LpRollingText.vue"
@@ -599,18 +599,18 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
599
599
  @escape-key-down="guardOutside"
600
600
  position="popper"
601
601
  :side-offset="6"
602
- :class="[POPOVER_PANEL, 'z-(--z-popover) max-h-72 w-(--reka-combobox-trigger-width) overflow-hidden']"
602
+ :class="[POPOVER_PANEL, 'z-(--z-popover) w-(--reka-combobox-trigger-width) overflow-hidden', COMBOBOX_FIT]"
603
603
  >
604
604
  <div v-if="searchable || serverSide" class="border-b border-line p-2">
605
605
  <ComboboxInput
606
606
  v-model="query"
607
607
  :placeholder="searchPlaceholderText"
608
608
  :aria-label="labels.searchLabel"
609
- class="h-(--size-control-sm) w-full rounded-md border border-line bg-surface-soft px-2.5 text-sm text-ink outline-none placeholder:text-muted focus:border-brand"
609
+ class="lp-entry h-(--size-control-sm) w-full rounded-md border border-line bg-surface-soft px-2.5 text-sm text-ink outline-none placeholder:text-muted focus:border-brand"
610
610
  />
611
611
  </div>
612
612
 
613
- <ComboboxViewport class="max-h-56 overflow-y-auto p-1">
613
+ <ComboboxViewport class="max-h-56 min-h-0 flex-1 overflow-y-auto p-1">
614
614
  <div v-if="loading" class="px-3 py-4 text-center text-sm italic text-muted">
615
615
  {{ loadingLabel }}
616
616
  </div>
@@ -51,7 +51,7 @@ const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, di
51
51
 
52
52
  // A slider is full-width by default; putting one in a toolbar means constraining it, and a fixed h-5 has to yield to a denser row.
53
53
  const { class: rootClass, attrs: rest } = useMergedAttrs(
54
- `relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55 ${SLIDER_GLIDE}`, () => ownAttrs())
54
+ `lp-tap relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55 ${SLIDER_GLIDE}`, () => ownAttrs())
55
55
  const { dragging, onPointerDown } = useSliderDrag()
56
56
 
57
57
  usePaletteControl({ disabled: () => field.value.disabled })
@@ -174,7 +174,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
174
174
  the same as a live server row elsewhere on the page. -->
175
175
  <LpStatus v-if="online" state="online" size="sm" hide-label />
176
176
  <LpIcon v-if="icon" :name="icon" :size="14" class="text-muted" />
177
- <span class="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted">
177
+ <span class="min-w-0 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted [overflow-wrap:anywhere]">
178
178
  <Transition v-bind="FADE">
179
179
  <LpSkeleton v-if="loading && !label" class="inline-block h-[0.8em] w-20 align-middle" />
180
180
  <span v-else>{{ label }}</span>
@@ -58,7 +58,7 @@ const slots = useSlots()
58
58
  const labelled = computed(() => Boolean(props.label || props.description || slots.default))
59
59
 
60
60
  const TRACK =
61
- "group/switch inline-flex h-5 w-9 shrink-0 items-center rounded-pill border border-line bg-surface-soft px-0.5 outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring data-[state=checked]:border-transparent data-[state=checked]:bg-brand disabled:cursor-not-allowed disabled:opacity-55"
61
+ "lp-tap group/switch inline-flex h-5 w-9 shrink-0 items-center rounded-pill border border-line bg-surface-soft px-0.5 outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring data-[state=checked]:border-transparent data-[state=checked]:bg-brand disabled:cursor-not-allowed disabled:opacity-55"
62
62
  const THUMB =
63
63
  "h-3.5 w-3.5 rounded-full bg-ink shadow transition-[translate,width,background-color] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] group-active/switch:w-5 group-data-[state=checked]/switch:translate-x-4 group-data-[state=checked]/switch:bg-ink-inverse group-data-[state=checked]/switch:group-active/switch:translate-x-2.5 motion-reduce:group-active/switch:w-3.5"
64
64
 
@@ -2362,7 +2362,18 @@ usePaletteCommands(() => {
2362
2362
  </dl>
2363
2363
  </div>
2364
2364
  </slot>
2365
+ <!-- `#line` is to a list line what `#card` is to a card: the inside
2366
+ only. Without it the line is built from the columns: the first
2367
+ names the row, the rest follow it in one muted line, which
2368
+ reads well for words and badly for meters or several badges. -->
2365
2369
  <div v-else class="min-w-0 flex-1">
2370
+ <slot
2371
+ name="line"
2372
+ :row="entry.row"
2373
+ :selected="isSelected(keyFor(entry.row, entry.index))"
2374
+ :toggle="() => toggleRow(keyFor(entry.row, entry.index), !isSelected(keyFor(entry.row, entry.index)))"
2375
+ :open-menu="openRowMenu"
2376
+ >
2366
2377
  <div v-if="titleColumn" class="min-w-0 truncate font-medium text-ink">
2367
2378
  <slot :name="`cell-${titleColumn.key}`" :row="entry.row" :value="entry.row[titleColumn.key]" :open-menu="openRowMenu">{{ entry.row[titleColumn.key] }}</slot>
2368
2379
  </div>
@@ -2374,7 +2385,8 @@ usePaletteCommands(() => {
2374
2385
  </span>
2375
2386
  </template>
2376
2387
  </div>
2377
- </div>
2388
+ </slot>
2389
+ </div>
2378
2390
  </div>
2379
2391
  </template>
2380
2392
  </TransitionGroup>
@@ -163,7 +163,7 @@ const action =
163
163
  <span v-if="option.count !== undefined" class="text-xs text-muted tabular-nums">{{ option.count }}</span>
164
164
  <button
165
165
  type="button"
166
- class="rounded-control px-1.5 py-0.5 text-xs font-medium text-brand opacity-0 outline-none transition-opacity duration-fast group-hover/opt:opacity-100 hover:bg-brand-soft focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring"
166
+ class="rounded-control px-1.5 py-0.5 text-xs font-medium text-brand opacity-0 outline-none transition-opacity duration-fast group-hover/opt:opacity-100 [@media(hover:none)]:opacity-100 hover:bg-brand-soft focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring"
167
167
  @click="set(onlyValues(values, [option.value]))"
168
168
  >
169
169
  {{ labels.only }}
@@ -513,7 +513,7 @@ usePaletteControl({ label: () => props.label, disabled: () => field.value.disabl
513
513
  :aria-expanded="options ? listOpen : undefined"
514
514
  :aria-controls="options ? listId : undefined"
515
515
  :aria-activedescendant="listOpen && activeIndex >= 0 ? optionId(activeIndex) : undefined"
516
- class="min-w-24 flex-1 bg-transparent px-1 outline-none placeholder:text-muted"
516
+ class="lp-entry min-w-24 flex-1 bg-transparent px-1 outline-none placeholder:text-muted"
517
517
  @focus="onFocus"
518
518
  @keydown="onKeydown"
519
519
  @beforeinput="onBeforeInput"
@@ -535,7 +535,7 @@ usePaletteControl({ label: () => props.label, disabled: () => field.value.disabl
535
535
  align="start"
536
536
  :side-offset="6"
537
537
  :collision-padding="8"
538
- :class="[POPOVER_PANEL, 'z-(--z-popover) max-h-72 w-(--reka-popper-anchor-width) overflow-y-auto p-1 outline-none']"
538
+ :class="[POPOVER_PANEL, 'z-(--z-popover) max-h-[min(18rem,var(--reka-popper-available-height,18rem))] w-(--reka-popper-anchor-width) overflow-y-auto p-1 outline-none']"
539
539
  @open-auto-focus.prevent
540
540
  @close-auto-focus.prevent
541
541
  @interact-outside="keepOpenInside"
@@ -82,7 +82,7 @@ defineExpose({ el: box, fit })
82
82
  const textareaClass = computed(() =>
83
83
  mergeClass(
84
84
  props.autosize ? "w-full resize-none" : "w-full resize-y",
85
- "rounded-control border bg-surface-soft px-3 py-2 text-sm text-ink outline-none transition-colors duration-[var(--duration-fast)] placeholder:text-muted focus:border-brand focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-55",
85
+ "lp-entry rounded-control border bg-surface-soft px-3 py-2 text-sm text-ink outline-none transition-colors duration-[var(--duration-fast)] placeholder:text-muted focus:border-brand focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-55",
86
86
  field.value.invalid ? "border-danger focus:border-danger focus:ring-danger-soft" : "border-line",
87
87
  hasField.value ? "" : (attrs.class as string),
88
88
  ),
@@ -20,7 +20,7 @@ import { presets } from "../theme/presets"
20
20
  import { STUDIO_SYSTEM, useThemeStudio } from "../theme/studio"
21
21
  import type { TokenSet } from "../theme/tokens"
22
22
  import { useTheme } from "../theme/useTheme"
23
- import { POPOVER_PANEL } from "./dropdown"
23
+ import { PANEL_FIT, POPOVER_PANEL } from "./dropdown"
24
24
  import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
25
25
  import LpIcon from "./LpIcon.vue"
26
26
  import LpRollingText from "./LpRollingText.vue"
@@ -374,7 +374,7 @@ usePaletteTheme(() => tuner ?? undefined, { hide: props.studioHide })
374
374
  align="end"
375
375
  :side-offset="6"
376
376
  :aria-label="text.studio"
377
- :class="[POPOVER_PANEL, 'z-(--z-popover) w-96 max-w-[calc(100vw-24px)] rounded-card p-3 text-sm text-ink outline-none']"
377
+ :class="[POPOVER_PANEL, PANEL_FIT, 'z-(--z-popover) w-96 rounded-card p-3 text-sm text-ink outline-none']"
378
378
  data-lp-glide-position
379
379
  @interact-outside="keepForTrigger"
380
380
  @escape-key-down="guardOutside"
@@ -80,7 +80,7 @@ const shellSize = CONTROL_SIZE
80
80
  // `w-full` is the default, not a rule: a time column of `w-28` must win over it.
81
81
  const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
82
82
  [
83
- "flex w-full items-center gap-0.5 rounded-control border bg-surface-soft px-3 text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55",
83
+ "lp-entry flex w-full items-center gap-0.5 rounded-control border bg-surface-soft px-3 text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55",
84
84
  shellSize[props.size],
85
85
  field.value.invalid ? "border-danger focus-within:ring-danger-soft" : "border-line focus-within:border-brand",
86
86
  ].join(" "), () => ownAttrs())
@@ -330,7 +330,7 @@ const portalTo = usePortalTarget()
330
330
  <Teleport :to="portalTo">
331
331
  <ToastViewport
332
332
  :data-lp-toasts="viewportId"
333
- class="fixed bottom-0 right-0 z-(--z-toast) flex w-[min(92vw,22rem)] flex-col gap-2 p-4 outline-none"
333
+ class="fixed bottom-0 right-0 z-(--z-toast) flex w-[min(92vw,22rem)] flex-col gap-2 p-4 pr-[max(1rem,env(safe-area-inset-right))] pb-[max(1rem,env(safe-area-inset-bottom))] outline-none"
334
334
  />
335
335
  </Teleport>
336
336
  </ToastProvider>
@@ -36,13 +36,14 @@ import {
36
36
  TooltipRoot,
37
37
  TooltipTrigger,
38
38
  } from "reka-ui"
39
- import { onBeforeUnmount, ref, watch, type FunctionalComponent } from "vue"
39
+ import { computed, mergeProps, onBeforeUnmount, ref, useAttrs, watch, type FunctionalComponent } from "vue"
40
40
  import {
41
41
  springSettled,
42
42
  springStep,
43
43
  type SpringState,
44
44
  } from "../composables/usePillTransition"
45
45
  import { PANEL_SURFACE, TOOLTIP_ANIM } from "./dropdown"
46
+ import { useTouchHold } from "../composables/useTouchHold"
46
47
  import { prefersReducedMotion } from "../composables/easing"
47
48
  import { useOutsideGuard, usePortalTarget, useView, viewWindow } from "../composables/useView"
48
49
 
@@ -86,8 +87,16 @@ const props = withDefaults(
86
87
  * a scroll that should have dismissed it.
87
88
  */
88
89
  disabled?: boolean
90
+ /**
91
+ * Touch has no hover, and a tap is the control's own. With `hold` a
92
+ * press-and-hold shows the tip (and that press does not also press the
93
+ * control); the next touch anywhere, or Escape, hides it. `none` leaves
94
+ * touch to the trigger. A followed tip (`followCursor`) never holds: its
95
+ * trigger is a chart or canvas with its own touch reading.
96
+ */
97
+ touch?: "hold" | "none"
89
98
  }>(),
90
- { side: "top" },
99
+ { side: "top", touch: "hold" },
91
100
  )
92
101
 
93
102
  /*
@@ -123,6 +132,9 @@ const target = { x: 0, y: 0 }
123
132
  const offset = ref({ x: 0, y: 0 })
124
133
  const tipBody = ref<HTMLElement | null>(null)
125
134
 
135
+ /* How long a finger rests on the trigger before the tip shows. */
136
+ const HOLD_MS = 450
137
+
126
138
  /* reka's own collision padding, which the follow offset would otherwise undo. */
127
139
  const EDGE = 8
128
140
 
@@ -241,7 +253,30 @@ const open = ref(false)
241
253
  /* What reka last asked for, regardless of whether it was allowed at the time. */
242
254
  const wanted = ref(false)
243
255
 
256
+ /*
257
+ * A tip opened by a touch hold stays until the next touch or Escape. reka
258
+ * would close it as soon as the finger lifts (a touch leaves the trigger
259
+ * then), so while it is held its own requests to close are not taken.
260
+ */
261
+ const heldOpen = ref(false)
262
+ const hold = useTouchHold({
263
+ enabled: () => props.touch === "hold" && !props.followCursor && !props.disabled,
264
+ delay: () => HOLD_MS,
265
+ onHold: () => {
266
+ heldOpen.value = true
267
+ wanted.value = true
268
+ open.value = true
269
+ },
270
+ })
271
+
272
+ function release() {
273
+ heldOpen.value = false
274
+ wanted.value = false
275
+ open.value = false
276
+ }
277
+
244
278
  const onOpenChange = (next: boolean) => {
279
+ if (!next && heldOpen.value) return
245
280
  // What reka wants, kept apart from what is currently allowed.
246
281
  wanted.value = next
247
282
  open.value = next && !props.disabled
@@ -265,8 +300,11 @@ const onOpenChange = (next: boolean) => {
265
300
  watch(
266
301
  () => props.disabled,
267
302
  (off) => {
268
- if (off) open.value = false
269
- else if (wanted.value) open.value = true
303
+ if (off) {
304
+ hold.cancel()
305
+ heldOpen.value = false
306
+ open.value = false
307
+ } else if (wanted.value) open.value = true
270
308
  },
271
309
  )
272
310
 
@@ -289,6 +327,18 @@ watch(
289
327
  },
290
328
  )
291
329
 
330
+ watch(heldOpen, (held, _, onCleanup) => {
331
+ if (!held) return
332
+ const win = viewWindow(view.value)
333
+ const onKey = (event: KeyboardEvent) => event.key === "Escape" && release()
334
+ win.addEventListener("pointerdown", release, true)
335
+ win.addEventListener("keydown", onKey, true)
336
+ onCleanup(() => {
337
+ win.removeEventListener("pointerdown", release, true)
338
+ win.removeEventListener("keydown", onKey, true)
339
+ })
340
+ })
341
+
292
342
  onBeforeUnmount(stopSpring)
293
343
 
294
344
  // Overlays open in the window this component renders in (an LpFrame's, inside one).
@@ -302,6 +352,9 @@ const guardOutside = useOutsideGuard()
302
352
  // LpPopover's, as-child) lost its id, ARIA state and click, and its popover
303
353
  // never opened. They go to the trigger, which merges them onto the slot's element.
304
354
  defineOptions({ inheritAttrs: false })
355
+ // The caller's own listeners on the trigger run beside the hold's, not instead of them.
356
+ const attrs = useAttrs()
357
+ const triggerAttrs = computed(() => mergeProps(attrs, hold.handlers))
305
358
  </script>
306
359
 
307
360
  <template>
@@ -324,8 +377,9 @@ defineOptions({ inheritAttrs: false })
324
377
  lands on the real button rather than on a wrapper. -->
325
378
  <TooltipTrigger
326
379
  as-child
327
- v-bind="$attrs"
380
+ v-bind="triggerAttrs"
328
381
  :aria-label="label ? content : undefined"
382
+ :class="touch === 'hold' && !followCursor ? '[-webkit-touch-callout:none]' : undefined"
329
383
  @pointermove="onPointerMove"
330
384
  >
331
385
  <slot />
@@ -5,4 +5,4 @@
5
5
  * keyboard handling), so it still looks like the badge's own.
6
6
  */
7
7
  export const BADGE_REMOVE =
8
- "grid size-4 shrink-0 place-items-center rounded-pill text-muted outline-none transition-colors duration-fast hover:bg-ink/10 hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
8
+ "lp-tap grid size-4 shrink-0 place-items-center rounded-pill text-muted outline-none transition-colors duration-fast hover:bg-ink/10 hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
@@ -45,6 +45,21 @@ export const PANEL_SURFACE =
45
45
  */
46
46
  export const POPOVER_PANEL = PANEL_SURFACE + " " + POPOVER_ANIM
47
47
 
48
+ /**
49
+ * Keeps a free-form panel (popover, menu, hover card) inside the window. Its
50
+ * width stops 0.5rem short of each edge, so a w-96 panel or a long menu label
51
+ * no longer runs off a phone; its height stops at the room reka measured on the
52
+ * side it opened, and the rest scrolls. A date picker opened in landscape used
53
+ * to hang off the top of the screen with its first weeks out of reach.
54
+ */
55
+ export const PANEL_FIT = "max-w-[calc(100vw-1rem)] max-h-(--reka-popper-available-height) overflow-y-auto"
56
+
57
+ /**
58
+ * A list row on a touch screen: 2.5rem tall instead of 2, so a thumb picks the
59
+ * row it is on and not its neighbour. A mouse keeps the compact row.
60
+ */
61
+ export const TOUCH_ROW = "[@media(hover:none)_and_(pointer:coarse)]:py-2.5"
62
+
48
63
  /**
49
64
  * A selectable option row: layout, highlight state, and the staggered cascade.
50
65
  * Cascades downward by default and upward when the panel is flipped above the
@@ -53,6 +68,7 @@ export const POPOVER_PANEL = PANEL_SURFACE + " " + POPOVER_ANIM
53
68
  */
54
69
  export const OPTION_ITEM =
55
70
  "flex cursor-pointer items-center gap-2 rounded-inner px-2.5 py-1.5 text-sm text-ink outline-none " +
71
+ TOUCH_ROW + " " +
56
72
  "animate-[rise-in_180ms_var(--ease-emphasized)_both] [--rise:-6px] group-data-[side=top]:[--rise:6px] " +
57
73
  "data-[highlighted]:bg-brand-soft data-[highlighted]:text-brand"
58
74
 
@@ -74,6 +90,14 @@ export const optionStagger = (index: number): { animationDelay: string } => ({
74
90
  /* Where a combobox list sits under its field. */
75
91
  export const COMBOBOX_PANEL = { collisionPadding: 8, position: "popper", sideOffset: 6 } as const
76
92
 
93
+ /*
94
+ * A combobox list's height: 18rem, or less where reka measured less room on the
95
+ * side it opened (a phone in landscape has about 170px under a field). The
96
+ * panel is a column so its option list, which carries `min-h-0 flex-1`, is what
97
+ * shrinks and scrolls; the search field above it stays.
98
+ */
99
+ export const COMBOBOX_FIT = "flex flex-col max-h-[min(18rem,var(--reka-popper-available-height,18rem))]"
100
+
77
101
  export const CLOSE_ICON =
78
102
  "transition-transform duration-medium ease-[var(--ease-emphasized)] " +
79
103
  "group-hover:rotate-90 group-active:scale-90 motion-reduce:group-hover:rotate-0"
@@ -2,6 +2,12 @@ import { computed, ref, type ComputedRef, type Ref } from "vue"
2
2
 
3
3
  const STEP = 20
4
4
 
5
+ /*
6
+ * A dialog's ceiling: 90% of the window, and never closer than 1rem to its
7
+ * edges or to a notch or home indicator on a phone that draws under them.
8
+ */
9
+ export const MODAL_MAX_HEIGHT = "min(90vh, calc(100dvh - 2rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)))"
10
+
5
11
  /*
6
12
  * One opening-order stack for every modal surface. Pinned to a global symbol
7
13
  * like the toast queue: a barrel import and a deep import can load this module
@@ -69,9 +69,12 @@ export function useReveal(options: UseRevealOptions = {}): UseReveal {
69
69
  const { threshold = 0.25, rootMargin = "0px" } = options
70
70
 
71
71
  const el = ref<Element | null>(null)
72
- // SSR has no viewport to intersect with, and no animation runs there either;
73
- // the client takes over on hydration and gates it properly then.
74
- const revealed = ref(typeof window === "undefined")
72
+ // The same starting value on the server and on the hydrating client, or every
73
+ // component with an entrance hydrated with a mismatch (the server said
74
+ // revealed, the client said waiting). Only an explicit `immediate` starts
75
+ // revealed; the observer, and reduced motion, decide on the client once
76
+ // mounted, the way useMediaQuery does.
77
+ const revealed = ref(!!toValue(options.immediate))
75
78
 
76
79
  let observer: IntersectionObserver | null = null
77
80
 
@@ -88,7 +91,7 @@ export function useReveal(options: UseRevealOptions = {}): UseReveal {
88
91
  watch(
89
92
  [el, () => toValue(options.immediate)],
90
93
  ([node, immediate]) => {
91
- if (revealed.value) return
94
+ if (revealed.value || typeof window === "undefined") return
92
95
  if (immediate || prefersReducedMotion()) {
93
96
  reveal()
94
97
  return