@leavepulse/ui 0.44.0 → 0.45.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 (265) hide show
  1. package/README.md +49 -3
  2. package/dist/assets/{LpCommandPalette-DCP5g2ik.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
  3. package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
  4. package/dist/assets/{LpShortcutsDialog-ChUyU7oV.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
  5. package/dist/assets/{LpTable-DDQNdf86.css → LpTable-CPwWxTJS.css} +49 -9
  6. package/dist/chunks/{ContextMenuItems-CrfeNPWT.js → ContextMenuItems-BtKU7074.js} +2 -1
  7. package/dist/chunks/{LayoutCanvas-D-jPOAvg.js → LayoutCanvas-BkaGXTjx.js} +1 -1
  8. package/dist/chunks/{LayoutNode-D8Xy9N-o.js → LayoutNode-B3_9E1Xa.js} +9 -5
  9. package/dist/chunks/{LpActionBar-BbwCF6l5.js → LpActionBar-CnfFVIwC.js} +3 -3
  10. package/dist/chunks/{LpAddressInput-BrOqkZqg.js → LpAddressInput-BwX8uo4_.js} +7 -7
  11. package/dist/chunks/{LpAlert-DkOIjsbq.js → LpAlert-B6ilrM7T.js} +1 -1
  12. package/dist/chunks/{LpAppShell-BUKPcHco.js → LpAppShell-C53_RaGF.js} +4 -4
  13. package/dist/chunks/{LpAutocomplete-DxV3Y9h4.js → LpAutocomplete-C8IgSMsi.js} +2 -1
  14. package/dist/chunks/{LpAvatar-ClBbEYZN.js → LpAvatar-C1XStdOo.js} +3 -3
  15. package/dist/chunks/{LpBreadcrumbs-B8IJgFIc.js → LpBreadcrumbs-CQJd73Gm.js} +1 -1
  16. package/dist/chunks/{LpButton-kqJBHIIb.js → LpButton-CKSwC6Mu.js} +1 -1
  17. package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
  18. package/dist/chunks/{LpCard-CpRByR0q.js → LpCard-C2IvucmX.js} +1 -1
  19. package/dist/chunks/{LpCheckbox-DpqchzH4.js → LpCheckbox-Bkj13CbW.js} +1 -1
  20. package/dist/chunks/{LpCodeBlock-DeHJPq7p.js → LpCodeBlock-ca7ANFyW.js} +1 -1
  21. package/dist/chunks/{LpColorPicker-Bhf1qxhB.js → LpColorPicker-zz4VGkG4.js} +46 -20
  22. package/dist/chunks/{LpConfirmDialog-CIUOSnWo.js → LpConfirmDialog-DCw5jR1t.js} +3 -3
  23. package/dist/chunks/{LpConfirmHost-CDm0Wtpv.js → LpConfirmHost-CbywMpMP.js} +1 -1
  24. package/dist/chunks/{LpContextMenu-DQw5dGfG.js → LpContextMenu-BqkAYa9X.js} +2 -1
  25. package/dist/chunks/{LpDatePicker-Drirx1AK.js → LpDatePicker-DuusgT69.js} +13 -9
  26. package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
  27. package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
  28. package/dist/chunks/{LpDrawer-bScx-KMO.js → LpDrawer-Dt4aB4Fr.js} +2 -2
  29. package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
  30. package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
  31. package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
  32. package/dist/chunks/{LpFileTree-CSI_5SK2.js → LpFileTree-CNurOvkV.js} +3 -3
  33. package/dist/chunks/{LpFileTreeNode-B4MiSQX2.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
  34. package/dist/chunks/{LpFormField-CIk5qosh.js → LpFormField-CtVYj6tO.js} +29 -15
  35. package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
  36. package/dist/chunks/{LpInput-C0e8y8_d.js → LpInput-D-YGQbEv.js} +39 -16
  37. package/dist/chunks/{LpLightbox-Mr0W_6HK.js → LpLightbox-DSDahF23.js} +1 -1
  38. package/dist/chunks/{LpLogViewer-YBOaFnc_.js → LpLogViewer-KypbgOyo.js} +252 -58
  39. package/dist/chunks/{LpMapPicker-NUzS9e37.js → LpMapPicker-Cc84LaM3.js} +1 -1
  40. package/dist/chunks/{LpMediaGallery-DfJmMW0I.js → LpMediaGallery-YKr-cQPN.js} +2 -2
  41. package/dist/chunks/{LpModal-DPwGKBPv.js → LpModal-DMMDQHAu.js} +3 -3
  42. package/dist/chunks/{LpModalNav-DPg5H9f3.js → LpModalNav-BVPDyIup.js} +4 -4
  43. package/dist/chunks/{LpNotificationBell-t5yn-siX.js → LpNotificationBell-CCwyJP5C.js} +26 -14
  44. package/dist/chunks/{LpNumberField-DWnQKktS.js → LpNumberField-B77O-z28.js} +1 -1
  45. package/dist/chunks/{LpPagination-DFngX7Xo.js → LpPagination-BcDlo3r_.js} +1 -1
  46. package/dist/chunks/{LpPasswordInput-CWVDuBJ5.js → LpPasswordInput-eKzQhbQP.js} +2 -2
  47. package/dist/chunks/{LpPhoneInput-CD-ZTeSd.js → LpPhoneInput-BCbrrhgE.js} +126 -23
  48. package/dist/chunks/{LpPie-BLco31Nx.js → LpPie-BEJ2SfI6.js} +9 -10
  49. package/dist/chunks/{LpPopover-p_e_shN3.js → LpPopover-DCKeqiVn.js} +1 -0
  50. package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
  51. package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
  52. package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
  53. package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
  54. package/dist/chunks/{LpSegmented-Dejd7cIx.js → LpSegmented-C09EEam2.js} +1 -1
  55. package/dist/chunks/{LpSelect-DrRD9I7L.js → LpSelect-DNIL6MB9.js} +20 -23
  56. package/dist/chunks/{LpSidebar-BgwUNWpJ.js → LpSidebar-CQNB0-yU.js} +3 -3
  57. package/dist/chunks/{LpSidebarNav-Bqxz8aWT.js → LpSidebarNav-kx-LdqNU.js} +3 -3
  58. package/dist/chunks/{LpStat-dXtyrdX9.js → LpStat-B-7udBRP.js} +1 -1
  59. package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
  60. package/dist/chunks/{LpSwitch-B1aLKulj.js → LpSwitch-OrcOJX9m.js} +1 -1
  61. package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
  62. package/dist/chunks/{LpTabs-_qMVaZJ6.js → LpTabs-29iW1XOx.js} +1 -1
  63. package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
  64. package/dist/chunks/{LpTextarea-yP8hGedR.js → LpTextarea-uuScWo6-.js} +1 -1
  65. package/dist/chunks/{LpThemeProvider-Wjm7Tuit.js → LpThemeProvider-Be4r2TQX.js} +1 -1
  66. package/dist/chunks/{LpThemeSwitcher-CKLc10KZ.js → LpThemeSwitcher-B4aVPbNC.js} +19 -22
  67. package/dist/chunks/{LpTimeField-C5HRgS5H.js → LpTimeField-Cqe7fXnh.js} +1 -1
  68. package/dist/chunks/{LpToaster--oT6lfhL.js → LpToaster-DpJvoCOh.js} +37 -3
  69. package/dist/chunks/{LpTooltip-CBdzeKNf.js → LpTooltip-CXVKcOXT.js} +3 -2
  70. package/dist/chunks/{LpTopologyCanvas-DocsLal3.js → LpTopologyCanvas-6ozNrQKh.js} +2 -1
  71. package/dist/chunks/{LpUptimeBar-DLtxOMRy.js → LpUptimeBar-ugcv25xj.js} +2 -2
  72. package/dist/chunks/icons-CkA0njzm.js +43 -0
  73. package/dist/chunks/{useTheme-C8JHUlPM.js → useTheme-DJm1JSsY.js} +47 -50
  74. package/dist/component-names.d.ts +1 -1
  75. package/dist/components/ContextMenuItems.vue.js +1 -1
  76. package/dist/components/LayoutCanvas.vue.js +1 -1
  77. package/dist/components/LayoutNode.vue.js +1 -1
  78. package/dist/components/LpActionBar.vue.js +1 -1
  79. package/dist/components/LpAddressInput.vue.js +1 -1
  80. package/dist/components/LpAlert.vue.js +1 -1
  81. package/dist/components/LpAppShell.vue.js +1 -1
  82. package/dist/components/LpAutocomplete.vue.js +1 -1
  83. package/dist/components/LpAvatar.vue.js +1 -1
  84. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  85. package/dist/components/LpButton.vue.d.ts +2 -2
  86. package/dist/components/LpButton.vue.js +1 -1
  87. package/dist/components/LpCalendar.vue.js +1 -1
  88. package/dist/components/LpCard.vue.js +1 -1
  89. package/dist/components/LpChart.vue.d.ts +16 -1
  90. package/dist/components/LpChart.vue.js +63 -5
  91. package/dist/components/LpCheckbox.vue.js +1 -1
  92. package/dist/components/LpCodeBlock.vue.js +1 -1
  93. package/dist/components/LpColorPicker.vue.d.ts +2 -2
  94. package/dist/components/LpColorPicker.vue.js +1 -1
  95. package/dist/components/LpCommandPalette.vue.d.ts +6 -0
  96. package/dist/components/LpCommandPalette.vue.js +61 -10
  97. package/dist/components/LpConfirmDialog.vue.js +1 -1
  98. package/dist/components/LpConfirmHost.vue.js +1 -1
  99. package/dist/components/LpContextMenu.vue.js +1 -1
  100. package/dist/components/LpDatePicker.vue.js +1 -1
  101. package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
  102. package/dist/components/LpDateRangePicker.vue.js +4 -0
  103. package/dist/components/LpDisclosure.vue.js +1 -1
  104. package/dist/components/LpDrawer.vue.js +1 -1
  105. package/dist/components/LpDropdownMenu.vue.js +1 -1
  106. package/dist/components/LpDropzone.vue.js +1 -1
  107. package/dist/components/LpEmptyState.vue.js +1 -1
  108. package/dist/components/LpFieldNotice.vue.d.ts +8 -0
  109. package/dist/components/LpFieldNotice.vue.js +4 -0
  110. package/dist/components/LpFileTree.vue.js +1 -1
  111. package/dist/components/LpFileTreeNode.vue.js +1 -1
  112. package/dist/components/LpFormField.vue.d.ts +4 -1
  113. package/dist/components/LpFormField.vue.js +1 -1
  114. package/dist/components/LpIcon.vue.d.ts +0 -1
  115. package/dist/components/LpIcon.vue.js +1 -1
  116. package/dist/components/LpImageEditor.vue.js +2 -2
  117. package/dist/components/LpInput.vue.d.ts +19 -0
  118. package/dist/components/LpInput.vue.js +1 -1
  119. package/dist/components/LpLightbox.vue.js +1 -1
  120. package/dist/components/LpLogViewer.vue.d.ts +52 -1
  121. package/dist/components/LpLogViewer.vue.js +1 -1
  122. package/dist/components/LpMapPicker.vue.js +1 -1
  123. package/dist/components/LpMediaGallery.vue.js +1 -1
  124. package/dist/components/LpModal.vue.js +1 -1
  125. package/dist/components/LpModalNav.vue.js +1 -1
  126. package/dist/components/LpNotificationBell.vue.js +1 -1
  127. package/dist/components/LpNumberField.vue.js +1 -1
  128. package/dist/components/LpPagination.vue.js +1 -1
  129. package/dist/components/LpPasswordInput.vue.js +1 -1
  130. package/dist/components/LpPhoneInput.vue.d.ts +13 -0
  131. package/dist/components/LpPhoneInput.vue.js +1 -1
  132. package/dist/components/LpPie.vue.js +1 -1
  133. package/dist/components/LpPopover.vue.js +1 -1
  134. package/dist/components/LpProgress.vue.d.ts +1 -1
  135. package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
  136. package/dist/components/LpRangeCalendar.vue.js +4 -0
  137. package/dist/components/LpRollingText.vue.d.ts +1 -1
  138. package/dist/components/LpRollingText.vue.js +1 -1
  139. package/dist/components/LpScrollArea.vue.d.ts +2 -2
  140. package/dist/components/LpScrollArea.vue.js +1 -1
  141. package/dist/components/LpSection.vue.js +1 -1
  142. package/dist/components/LpSegmented.vue.js +1 -1
  143. package/dist/components/LpSelect.vue.js +1 -1
  144. package/dist/components/LpShortcutsDialog.vue.d.ts +6 -0
  145. package/dist/components/LpShortcutsDialog.vue.js +56 -14
  146. package/dist/components/LpSidebar.vue.js +1 -1
  147. package/dist/components/LpSidebarNav.vue.js +1 -1
  148. package/dist/components/LpStat.vue.js +1 -1
  149. package/dist/components/LpStepper.vue.js +1 -1
  150. package/dist/components/LpSwitch.vue.js +1 -1
  151. package/dist/components/LpTable.vue.d.ts +84 -2
  152. package/dist/components/LpTable.vue.js +481 -107
  153. package/dist/components/LpTableFilter.vue.d.ts +31 -0
  154. package/dist/components/LpTableFilter.vue.js +4 -0
  155. package/dist/components/LpTabs.vue.js +1 -1
  156. package/dist/components/LpTagsInput.vue.d.ts +77 -0
  157. package/dist/components/LpTagsInput.vue.js +4 -0
  158. package/dist/components/LpTextarea.vue.js +1 -1
  159. package/dist/components/LpThemeProvider.vue.js +1 -1
  160. package/dist/components/LpThemeSwitcher.vue.d.ts +2 -2
  161. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  162. package/dist/components/LpTimeField.vue.js +1 -1
  163. package/dist/components/LpToaster.vue.js +1 -1
  164. package/dist/components/LpTooltip.vue.js +1 -1
  165. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  166. package/dist/components/LpUptimeBar.vue.js +1 -1
  167. package/dist/components/chart.js +3 -2
  168. package/dist/components/commandSearch.js +3 -0
  169. package/dist/components/dateRange.d.ts +43 -0
  170. package/dist/components/dateRange.js +76 -0
  171. package/dist/components/exportData.d.ts +58 -0
  172. package/dist/components/exportData.js +165 -0
  173. package/dist/components/logSearch.d.ts +18 -0
  174. package/dist/components/logSearch.js +36 -0
  175. package/dist/components/phone.d.ts +41 -0
  176. package/dist/components/phone.js +80 -0
  177. package/dist/components/tableFilter.d.ts +36 -0
  178. package/dist/components/tableFilter.js +84 -0
  179. package/dist/components/tags.d.ts +20 -0
  180. package/dist/components/tags.js +37 -0
  181. package/dist/composables/useField.d.ts +4 -0
  182. package/dist/composables/useFieldNotice.d.ts +17 -0
  183. package/dist/composables/useFieldNotice.js +28 -0
  184. package/dist/composables/useForm.d.ts +53 -0
  185. package/dist/composables/useForm.js +207 -0
  186. package/dist/composables/useHotkeys.d.ts +65 -5
  187. package/dist/composables/useHotkeys.js +170 -9
  188. package/dist/composables/useInputMask.d.ts +59 -0
  189. package/dist/composables/useInputMask.js +170 -0
  190. package/dist/composables/usePaintSelect.d.ts +22 -0
  191. package/dist/composables/usePaintSelect.js +169 -0
  192. package/dist/composables/useShift.js +7 -2
  193. package/dist/icons-all.d.ts +1 -0
  194. package/dist/icons-all.js +3 -0
  195. package/dist/icons.d.ts +14 -1
  196. package/dist/index.d.ts +29 -3
  197. package/dist/index.js +195 -138
  198. package/dist/kit-icons.d.ts +2 -0
  199. package/dist/theme/backdrop.d.ts +12 -0
  200. package/dist/theme/presets.d.ts +0 -2
  201. package/package.json +13 -4
  202. package/src/component-names.ts +4 -0
  203. package/src/components/ContextMenuItems.vue +1 -1
  204. package/src/components/LayoutNode.vue +11 -2
  205. package/src/components/LpActionBar.vue +1 -1
  206. package/src/components/LpAutocomplete.vue +1 -0
  207. package/src/components/LpAvatar.vue +2 -2
  208. package/src/components/LpButton.vue +1 -1
  209. package/src/components/LpChart.vue +80 -1
  210. package/src/components/LpColorPicker.vue +34 -1
  211. package/src/components/LpCommandPalette.vue +81 -7
  212. package/src/components/LpContextMenu.vue +1 -1
  213. package/src/components/LpDatePicker.vue +8 -3
  214. package/src/components/LpDateRangePicker.vue +224 -0
  215. package/src/components/LpDropdownMenu.vue +1 -0
  216. package/src/components/LpFieldNotice.vue +65 -0
  217. package/src/components/LpFormField.vue +33 -4
  218. package/src/components/LpIcon.vue +22 -4
  219. package/src/components/LpInput.vue +46 -6
  220. package/src/components/LpLogViewer.vue +225 -22
  221. package/src/components/LpModal.vue +1 -1
  222. package/src/components/LpNotificationBell.vue +15 -7
  223. package/src/components/LpPhoneInput.vue +179 -20
  224. package/src/components/LpPie.vue +8 -5
  225. package/src/components/LpPopover.vue +1 -0
  226. package/src/components/LpRangeCalendar.vue +365 -0
  227. package/src/components/LpRollingText.vue +3 -0
  228. package/src/components/LpScrollArea.vue +22 -2
  229. package/src/components/LpSelect.vue +0 -0
  230. package/src/components/LpShortcutsDialog.vue +77 -8
  231. package/src/components/LpStepper.vue +23 -10
  232. package/src/components/LpSwitch.vue +1 -1
  233. package/src/components/LpTable.vue +584 -65
  234. package/src/components/LpTableFilter.vue +175 -0
  235. package/src/components/LpTagsInput.vue +364 -0
  236. package/src/components/LpThemeSwitcher.vue +19 -23
  237. package/src/components/LpToaster.vue +49 -1
  238. package/src/components/LpTooltip.vue +9 -2
  239. package/src/components/LpTopologyCanvas.vue +7 -1
  240. package/src/components/chart.ts +5 -3
  241. package/src/components/commandSearch.ts +6 -0
  242. package/src/components/dateRange.ts +134 -0
  243. package/src/components/exportData.ts +228 -0
  244. package/src/components/logSearch.ts +46 -0
  245. package/src/components/phone.ts +140 -0
  246. package/src/components/tableFilter.ts +122 -0
  247. package/src/components/tags.ts +62 -0
  248. package/src/composables/useField.ts +4 -0
  249. package/src/composables/useFieldNotice.ts +59 -0
  250. package/src/composables/useForm.ts +314 -0
  251. package/src/composables/useHotkeys.ts +269 -13
  252. package/src/composables/useInputMask.ts +254 -0
  253. package/src/composables/usePaintSelect.ts +243 -0
  254. package/src/composables/useShift.ts +13 -2
  255. package/src/iconSubset.d.mts +10 -0
  256. package/src/iconSubset.mjs +51 -0
  257. package/src/icons-all.ts +10 -0
  258. package/src/icons.ts +59 -8
  259. package/src/index.ts +37 -3
  260. package/src/kit-icons.ts +5 -0
  261. package/src/nuxt.ts +30 -2
  262. package/src/runtime/theme.plugin.ts +2 -2
  263. package/src/theme/backdrop.ts +69 -0
  264. package/src/theme/presets.ts +6 -64
  265. package/src/tokens/tokens.css +46 -0
@@ -1,11 +1,20 @@
1
1
  <script setup lang="ts">
2
- import { Motion } from "motion-v"
2
+ import { Motion, useReducedMotion } from "motion-v"
3
3
  import { computed, onBeforeUnmount, ref } from "vue"
4
4
  import type { LayoutNode, Leaf, Side, Split } from "../layout/tree"
5
5
  import type { BlockRegistry } from "../layout/registry"
6
6
  import { blockTitle } from "../layout/registry"
7
7
  import LpIcon from "./LpIcon.vue"
8
8
 
9
+ // Same spring as LpTabs' panel height; instant under reduced motion, which
10
+ // motion-v does not apply on its own.
11
+ const reduceMotion = useReducedMotion()
12
+ const layoutTransition = computed(() =>
13
+ reduceMotion.value
14
+ ? { duration: 0 }
15
+ : { type: "spring" as const, stiffness: 520, damping: 42, mass: 0.9 },
16
+ )
17
+
9
18
  const props = defineProps<{
10
19
  node: LayoutNode
11
20
  dragId: string | null
@@ -185,7 +194,7 @@ function startSplitter(ev: PointerEvent, parent: Split, index: number) {
185
194
  <Motion
186
195
  v-else-if="leaf"
187
196
  :layout="true"
188
- :transition="{ type: 'spring', stiffness: 520, damping: 42, mass: 0.9 }"
197
+ :transition="layoutTransition"
189
198
  class="relative flex min-h-0 min-w-0 flex-col"
190
199
  :class="{ 'opacity-35': dragId === node.id }"
191
200
  >
@@ -209,7 +209,7 @@ onBeforeUnmount(() => observer?.disconnect())
209
209
  Reduced motion skips it entirely rather than holding a frame it never
210
210
  animated to. -->
211
211
  <Transition
212
- enter-active-class="animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)]"
212
+ enter-active-class="animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none"
213
213
  leave-active-class="animate-[rise-out_var(--duration-fast)_ease_forwards] motion-reduce:animate-none"
214
214
  >
215
215
  <div
@@ -263,6 +263,7 @@ function clear() {
263
263
 
264
264
  <ComboboxPortal>
265
265
  <ComboboxContent
266
+ :collision-padding="8"
266
267
  position="popper"
267
268
  :side-offset="6"
268
269
  :class="[POPOVER_PANEL, 'z-(--z-popover) max-h-72 w-(--reka-combobox-trigger-width) overflow-hidden']"
@@ -98,12 +98,12 @@ onMounted(() => {
98
98
  <template>
99
99
  <LpContextMenu v-if="menuItems?.length" :items="menuItems">
100
100
  <AvatarRoot :class="rootClass" :style="sizeStyle" v-bind="rest">
101
- <AvatarImage v-if="src && mounted" :src="src" :alt="alt" class="size-full object-cover" />
101
+ <AvatarImage v-if="src && mounted" :src="src" :alt="alt" class="size-full object-cover animate-[fade-in_var(--duration-medium)_ease] motion-reduce:animate-none" />
102
102
  <AvatarFallback>{{ initials }}</AvatarFallback>
103
103
  </AvatarRoot>
104
104
  </LpContextMenu>
105
105
  <AvatarRoot v-else :class="rootClass" :style="sizeStyle" v-bind="rest">
106
- <AvatarImage v-if="src && mounted" :src="src" :alt="alt" class="size-full object-cover" />
106
+ <AvatarImage v-if="src && mounted" :src="src" :alt="alt" class="size-full object-cover animate-[fade-in_var(--duration-medium)_ease] motion-reduce:animate-none" />
107
107
  <AvatarFallback>{{ initials }}</AvatarFallback>
108
108
  </AvatarRoot>
109
109
  </template>
@@ -13,7 +13,7 @@ import { resolveLink, type LinkTarget } from "./link"
13
13
 
14
14
  const button = tv({
15
15
  base: [
16
- "inline-flex select-none items-center justify-center gap-2 font-semibold",
16
+ "inline-flex select-none items-center justify-center gap-2 whitespace-nowrap font-semibold",
17
17
  "rounded-control transition-[color,background-color,border-color,box-shadow,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]",
18
18
  // tactile press: dips slightly on click, springs back on release.
19
19
  "active:scale-[0.97] motion-reduce:active:scale-100",
@@ -49,6 +49,7 @@ import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
49
49
  import LpIcon from "./LpIcon.vue"
50
50
  import LpShift from "./LpShift.vue"
51
51
  import { useMergedAttrs } from "../composables/useMergedClass"
52
+ import { backgroundOf, downloadFile, fileStem, resolveCssColor, svgToPng, toCsv, type CsvOptions } from "./exportData"
52
53
 
53
54
  export interface ChartText {
54
55
  [key: string]: string
@@ -56,6 +57,15 @@ export interface ChartText {
56
57
  rightAxis: string
57
58
  /** Prefix of the keyboard readout, before the point's label. */
58
59
  point: string
60
+ /** Header of the label column in a CSV export. */
61
+ xColumn: string
62
+ }
63
+
64
+ export interface ChartExportOptions {
65
+ /** File name without extension. Defaults to the title. */
66
+ filename?: string
67
+ /** Return the data without offering a download. */
68
+ download?: boolean
59
69
  }
60
70
 
61
71
  /** What was under the pointer when a point was clicked or right-clicked. */
@@ -144,7 +154,7 @@ const props = withDefaults(
144
154
 
145
155
  const strings = useLabels<ChartText>(
146
156
  "chart",
147
- { empty: "No data", rightAxis: "Measured on the right axis", point: "Point" },
157
+ { empty: "No data", rightAxis: "Measured on the right axis", point: "Point", xColumn: "Label" },
148
158
  () => props.text,
149
159
  )
150
160
 
@@ -1012,6 +1022,74 @@ const readout = computed(() =>
1012
1022
  : "",
1013
1023
  )
1014
1024
 
1025
+ // ── export ─────────────────────────────────────
1026
+ // Every series, hidden ones included: what a reader toggled off for a closer
1027
+ // look is still part of the data they asked to take away.
1028
+ const svgEl = ref<SVGSVGElement | null>(null)
1029
+
1030
+ function exportCsv(options: ChartExportOptions & CsvOptions = {}): string {
1031
+ const rows = labels.value.map((label, i) => ({ label, i }))
1032
+ const csv = toCsv(rows, [
1033
+ { key: "label", label: strings.value.xColumn },
1034
+ ...allSeries.value.map((s) => ({ key: s.id, label: s.name, value: (r: { i: number }) => s.values[r.i] })),
1035
+ ], options)
1036
+ if (options.download !== false) {
1037
+ downloadFile(`${fileStem(options.filename ?? props.title, "chart")}.csv`, csv, "text/csv;charset=utf-8")
1038
+ }
1039
+ return csv
1040
+ }
1041
+
1042
+ /*
1043
+ * The plot as a picture, in the theme it is shown in. The legend and title are
1044
+ * HTML, so they are painted onto the canvas separately, in the page's font; the
1045
+ * SVG travels with its computed colours inlined, because an SVG drawn as an
1046
+ * image cannot see the page's custom properties.
1047
+ */
1048
+ async function exportPng(options: ChartExportOptions & { scale?: number; background?: string } = {}): Promise<Blob | null> {
1049
+ const svg = svgEl.value
1050
+ if (!svg || typeof document === "undefined") return null
1051
+ const style = getComputedStyle(root.value ?? svg)
1052
+ const font = style.fontFamily
1053
+ const ink = style.getPropertyValue("--color-ink").trim() || style.color
1054
+ const muted = style.getPropertyValue("--color-muted-strong").trim() || style.color
1055
+ const legend = props.showLegend && allSeries.value.length > 1 ? visible.value : []
1056
+ const blob = await svgToPng(svg, {
1057
+ scale: options.scale,
1058
+ padding: 16,
1059
+ background: options.background ?? backgroundOf(root.value),
1060
+ header: (ctx) => {
1061
+ let y = 0
1062
+ if (props.title) {
1063
+ ctx.font = `500 14px ${font}`
1064
+ ctx.fillStyle = ink
1065
+ ctx.textBaseline = "top"
1066
+ ctx.fillText(props.title, 0, y)
1067
+ y += 22
1068
+ }
1069
+ if (legend.length) {
1070
+ ctx.font = `12px ${font}`
1071
+ ctx.textBaseline = "middle"
1072
+ let x = 0
1073
+ for (const s of legend) {
1074
+ ctx.fillStyle = resolveCssColor(s.color, root.value ?? svg)
1075
+ ctx.beginPath()
1076
+ ctx.arc(x + 4, y + 8, 4, 0, Math.PI * 2)
1077
+ ctx.fill()
1078
+ ctx.fillStyle = muted
1079
+ ctx.fillText(s.name, x + 14, y + 8)
1080
+ x += 14 + ctx.measureText(s.name).width + 16
1081
+ }
1082
+ y += 24
1083
+ }
1084
+ return y ? y + 8 : 0
1085
+ },
1086
+ })
1087
+ if (options.download !== false) downloadFile(`${fileStem(options.filename ?? props.title, "chart")}.png`, blob)
1088
+ return blob
1089
+ }
1090
+
1091
+ defineExpose({ exportCsv, exportPng })
1092
+
1015
1093
  // The chart fills its container's width and sets its own gap between plot and legend — both are layout decisions the page makes, not the chart.
1016
1094
  const { class: rootClass, attrs: rest } = useMergedAttrs(
1017
1095
  "flex w-full flex-col gap-3",
@@ -1083,6 +1161,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1083
1161
 
1084
1162
  <svg
1085
1163
  v-else-if="width > 0"
1164
+ ref="svgEl"
1086
1165
  :width="width"
1087
1166
  :height="height"
1088
1167
  class="overflow-visible touch-none select-none"
@@ -36,7 +36,8 @@ import {
36
36
  PopoverRoot,
37
37
  PopoverTrigger,
38
38
  } from "reka-ui"
39
- import { computed, ref, watch } from "vue"
39
+ import { computed, nextTick, ref, watch } from "vue"
40
+ import { prefersReducedMotion } from "../composables/easing"
40
41
  import { useFieldControl } from "../composables/useField"
41
42
  import {
42
43
  clampHsv,
@@ -53,6 +54,7 @@ import { useClipboard } from "../composables/useClipboard"
53
54
  import { POPOVER_PANEL } from "./dropdown"
54
55
  import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
55
56
  import LpIcon from "./LpIcon.vue"
57
+ import LpRollingText from "./LpRollingText.vue"
56
58
 
57
59
  /**
58
60
  * Every user-facing string the picker renders on its own — the menu items, the
@@ -554,9 +556,28 @@ async function pasteColor() {
554
556
  * emit the picker would display rgb() while the app still stored hex, and the
555
557
  * next thing to read that value would disagree with the screen.
556
558
  */
559
+ /*
560
+ * A format switch rewrites the whole text in place ("#3b82f6" becomes
561
+ * "rgb(59 130 246)"), so the new value rolls in over the field the way the
562
+ * kit's other changing labels do. An <input> cannot animate its own text, so
563
+ * a rolling copy lies over it for the length of the roll; it only ever moves
564
+ * on a format switch, never while a colour is being dragged.
565
+ */
566
+ const ROLL_MS = 320
567
+ const rollText = ref(current.value)
568
+ const rolling = ref(false)
569
+ let rollTimer: ReturnType<typeof setTimeout> | undefined
570
+
557
571
  function setFormat(f: ColorFormat) {
558
572
  if (activeFormat.value === f) return
573
+ rollText.value = inputValue.value
559
574
  activeFormat.value = f
575
+ if (!prefersReducedMotion()) {
576
+ rolling.value = true
577
+ void nextTick(() => (rollText.value = current.value))
578
+ if (rollTimer) clearTimeout(rollTimer)
579
+ rollTimer = setTimeout(() => (rolling.value = false), ROLL_MS + 40)
580
+ }
560
581
  typed.value = null
561
582
  emit("update:modelValue", current.value)
562
583
  emit("change", current.value)
@@ -654,6 +675,7 @@ defineExpose({ open })
654
675
 
655
676
  <PopoverPortal>
656
677
  <PopoverContent
678
+ :collision-padding="8"
657
679
  :side="side"
658
680
  :align="align"
659
681
  :side-offset="6"
@@ -748,6 +770,15 @@ defineExpose({ open })
748
770
  class="size-4 shrink-0 rounded-[5px] border border-line"
749
771
  :style="{ backgroundColor: previewCss }"
750
772
  />
773
+ <span class="relative flex min-w-0 flex-1 items-center">
774
+ <LpRollingText
775
+ v-show="rolling"
776
+ :value="rollText"
777
+ :duration="ROLL_MS"
778
+ align="start"
779
+ aria-hidden="true"
780
+ class="pointer-events-none absolute inset-0 font-mono text-xs text-ink"
781
+ />
751
782
  <input
752
783
  :value="inputValue"
753
784
  :disabled="disabled"
@@ -756,10 +787,12 @@ defineExpose({ open })
756
787
  autocomplete="off"
757
788
  :aria-label="l.value"
758
789
  class="min-w-0 flex-1 select-text bg-transparent font-mono text-xs text-ink outline-none placeholder:text-muted"
790
+ :class="rolling ? 'text-transparent' : ''"
759
791
  @input="onInput"
760
792
  @keydown.enter="commitTyped"
761
793
  @blur="onInputBlur"
762
794
  />
795
+ </span>
763
796
  </div>
764
797
 
765
798
  <!-- Recent sits ABOVE the presets because it is the row that
@@ -181,6 +181,12 @@ const props = withDefaults(
181
181
  * so a stored history never resurrects a command that has gone away.
182
182
  */
183
183
  recent?: string[]
184
+ /**
185
+ * Let the palette keep the history itself, in localStorage under this key:
186
+ * every top-level command run is remembered, newest first. Ignored while
187
+ * `recent` is bound, which stays the way to store it somewhere else.
188
+ */
189
+ recentKey?: string
184
190
  /** How many recent commands to surface. */
185
191
  recentLimit?: number
186
192
  /** Headings the palette adds itself. */
@@ -351,6 +357,31 @@ function onTab(event: KeyboardEvent) {
351
357
  enter({ id: `${c.id}:actions`, label: c.label, icon: c.icon, children: actions })
352
358
  }
353
359
 
360
+ /*
361
+ * A mode holds the command it was entered through, and a static `children`
362
+ * list is a snapshot of that moment. When the app rebuilds its commands (a
363
+ * theme list whose tick follows the active theme), the open mode is matched
364
+ * to the new commands by id along its path and takes their children, so a
365
+ * `keepOpen` pick inside it shows its own effect.
366
+ */
367
+ watch(
368
+ () => props.commands,
369
+ (commands) => {
370
+ if (!trail.value.length) return
371
+ let level: Command[] = commands
372
+ const next: Command[] = []
373
+ for (const step of trail.value) {
374
+ const fresh = level.find((c) => c.id === step.id)
375
+ if (!fresh) return
376
+ next.push(fresh)
377
+ level = Array.isArray(fresh.children) ? fresh.children : []
378
+ }
379
+ trail.value = next
380
+ const last = next[next.length - 1]
381
+ if (last && Array.isArray(last.children)) modeItems.value = last.children
382
+ },
383
+ )
384
+
354
385
  function enter(c: Command) {
355
386
  trailState.value = [...trailState.value, { query: query.value, selectedId: selectedId.value }]
356
387
  trail.value = [...trail.value, c]
@@ -498,11 +529,36 @@ const fetched = computed<Command[]>(() => {
498
529
  * Only on the root's empty query: once someone types, or drills into a mode,
499
530
  * relevance is the better order and a duplicated row is just noise.
500
531
  */
532
+ const RECENT_KEEP = 20
533
+ const ownRecent = ref<string[]>([])
534
+ watch(
535
+ () => props.recentKey,
536
+ (key) => {
537
+ if (!key || typeof localStorage === "undefined") return
538
+ try {
539
+ const stored: unknown = JSON.parse(localStorage.getItem(key) ?? "[]")
540
+ ownRecent.value = Array.isArray(stored) ? stored.filter((v): v is string => typeof v === "string") : []
541
+ } catch {
542
+ ownRecent.value = []
543
+ }
544
+ },
545
+ { immediate: true },
546
+ )
547
+ function remember(id: string) {
548
+ const key = props.recentKey
549
+ if (!key || props.recent) return
550
+ ownRecent.value = [id, ...ownRecent.value.filter((v) => v !== id)].slice(0, RECENT_KEEP)
551
+ try {
552
+ localStorage.setItem(key, JSON.stringify(ownRecent.value))
553
+ } catch {}
554
+ }
555
+ const recentIds = computed(() => props.recent ?? (props.recentKey ? ownRecent.value : []))
556
+
501
557
  const recentItems = computed<Command[]>(() => {
502
- if (mode.value || query.value.trim() || !props.recent?.length) return []
558
+ if (mode.value || query.value.trim() || !recentIds.value.length) return []
503
559
  const byId = new Map(props.commands.map((c) => [c.id, c]))
504
560
  const out: Command[] = []
505
- for (const id of props.recent) {
561
+ for (const id of recentIds.value) {
506
562
  const c = byId.get(id)
507
563
  if (c && !c.disabled && !out.includes(c)) out.push(c)
508
564
  if (out.length >= props.recentLimit) break
@@ -590,6 +646,18 @@ const paragraphs = computed(() =>
590
646
  .filter(Boolean),
591
647
  )
592
648
 
649
+ /*
650
+ * The row's description heads the pane only when it says something the body
651
+ * does not: a command whose full text opens with its description (or is it)
652
+ * would otherwise print the same sentences twice, once small and once large.
653
+ */
654
+ const paneDescription = computed(() => {
655
+ const description = active.value?.description?.trim()
656
+ if (!description) return undefined
657
+ const body = paragraphs.value.join(" ")
658
+ return body.startsWith(description) ? undefined : description
659
+ })
660
+
593
661
  /** Stagger between paragraphs, capped so a long section does not trickle in. */
594
662
  const STAGGER_MS = 45
595
663
  const STAGGER_MAX = 10
@@ -768,11 +836,16 @@ function run(c: Command | undefined) {
768
836
  return
769
837
  }
770
838
  emit("run", c.id, c)
839
+ if (!mode.value) remember(c.id)
771
840
  if (c.keepOpen) {
772
841
  // Back to the top of a fresh list: the query that found this command is
773
842
  // spent, and leaving it would hide whatever the action just changed.
774
843
  query.value = ""
775
- select(0)
844
+ // Inside a mode the highlight stays on what was just picked, so trying
845
+ // themes one after another is a matter of the arrow keys; at the root the
846
+ // list it came from is gone and the top is the honest place to land.
847
+ if (mode.value) void nextTick(() => select(Math.max(0, flat.value.findIndex((x) => x.id === c.id))))
848
+ else select(0)
776
849
  inputRef.value?.focus()
777
850
  return
778
851
  }
@@ -1045,8 +1118,8 @@ useHotkeys(() =>
1045
1118
  <LpIcon v-if="active.icon" :name="active.icon" :size="16" class="shrink-0 text-muted" />
1046
1119
  <h3 class="min-w-0 text-base font-semibold">{{ active.label }}</h3>
1047
1120
  </div>
1048
- <p v-if="active.description" class="lp-cmd-para mt-0.5 text-xs text-muted">
1049
- {{ active.description }}
1121
+ <p v-if="paneDescription" class="lp-cmd-para mt-0.5 text-xs text-muted">
1122
+ {{ paneDescription }}
1050
1123
  </p>
1051
1124
  <p
1052
1125
  v-for="(para, i) in paragraphs"
@@ -1066,9 +1139,10 @@ useHotkeys(() =>
1066
1139
  </Transition>
1067
1140
  </div>
1068
1141
 
1069
- <!-- Footer hints -->
1142
+ <!-- Footer hints. Keyboard-only, so a touch screen without a keyboard
1143
+ gets the row back for results. -->
1070
1144
  <div
1071
- class="flex shrink-0 items-center gap-4 border-t border-line px-4 py-2.5 text-[11px] text-muted"
1145
+ class="flex shrink-0 items-center gap-4 border-t border-line px-4 py-2.5 text-[11px] text-muted [@media(hover:none)_and_(pointer:coarse)]:hidden"
1072
1146
  >
1073
1147
  <span class="flex items-center gap-1">
1074
1148
  <kbd class="rounded border border-line px-1 text-[10px]">↑</kbd>
@@ -87,7 +87,7 @@ function returnFocus(event: Event) {
87
87
  <slot />
88
88
  </ContextMenuTrigger>
89
89
  <ContextMenuPortal v-if="items.length">
90
- <ContextMenuContent :class="PANEL" @close-auto-focus="returnFocus">
90
+ <ContextMenuContent :collision-padding="8" :class="PANEL" @close-auto-focus="returnFocus">
91
91
  <ContextMenuItems :items="items" :panel="PANEL" />
92
92
  </ContextMenuContent>
93
93
  </ContextMenuPortal>
@@ -25,6 +25,7 @@ import { mergeClass } from "../composables/useMergedClass"
25
25
  import { POPOVER_PANEL } from "./dropdown"
26
26
  import LpCalendar from "./LpCalendar.vue"
27
27
  import LpIcon from "./LpIcon.vue"
28
+ import LpRollingText from "./LpRollingText.vue"
28
29
  import LpTimeField from "./LpTimeField.vue"
29
30
 
30
31
  const props = withDefaults(
@@ -145,9 +146,12 @@ const showClear = computed(() => props.clearable && !!props.modelValue && !field
145
146
  ]"
146
147
  >
147
148
  <LpIcon name="lucide:calendar" :size="16" class="shrink-0 text-muted" />
148
- <span class="min-w-0 flex-1 truncate" :class="display ? '' : 'text-muted'">
149
- {{ display || placeholder }}
150
- </span>
149
+ <LpRollingText
150
+ :value="display || placeholder || ''"
151
+ align="start"
152
+ class="min-w-0 flex-1"
153
+ :class="display ? '' : 'text-muted'"
154
+ />
151
155
  <span v-if="showClear" class="w-[15px] shrink-0" aria-hidden="true" />
152
156
  <LpIcon
153
157
  name="lucide:chevron-down"
@@ -158,6 +162,7 @@ const showClear = computed(() => props.clearable && !!props.modelValue && !field
158
162
  </PopoverTrigger>
159
163
  <PopoverPortal>
160
164
  <PopoverContent
165
+ :collision-padding="8"
161
166
  :side-offset="6"
162
167
  align="start"
163
168
  :class="[POPOVER_PANEL, 'z-(--z-popover) rounded-card p-0 outline-none']"