@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.
- package/README.md +49 -3
- package/dist/assets/{LpCommandPalette-DCP5g2ik.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
- package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
- package/dist/assets/{LpShortcutsDialog-ChUyU7oV.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
- package/dist/assets/{LpTable-DDQNdf86.css → LpTable-CPwWxTJS.css} +49 -9
- package/dist/chunks/{ContextMenuItems-CrfeNPWT.js → ContextMenuItems-BtKU7074.js} +2 -1
- package/dist/chunks/{LayoutCanvas-D-jPOAvg.js → LayoutCanvas-BkaGXTjx.js} +1 -1
- package/dist/chunks/{LayoutNode-D8Xy9N-o.js → LayoutNode-B3_9E1Xa.js} +9 -5
- package/dist/chunks/{LpActionBar-BbwCF6l5.js → LpActionBar-CnfFVIwC.js} +3 -3
- package/dist/chunks/{LpAddressInput-BrOqkZqg.js → LpAddressInput-BwX8uo4_.js} +7 -7
- package/dist/chunks/{LpAlert-DkOIjsbq.js → LpAlert-B6ilrM7T.js} +1 -1
- package/dist/chunks/{LpAppShell-BUKPcHco.js → LpAppShell-C53_RaGF.js} +4 -4
- package/dist/chunks/{LpAutocomplete-DxV3Y9h4.js → LpAutocomplete-C8IgSMsi.js} +2 -1
- package/dist/chunks/{LpAvatar-ClBbEYZN.js → LpAvatar-C1XStdOo.js} +3 -3
- package/dist/chunks/{LpBreadcrumbs-B8IJgFIc.js → LpBreadcrumbs-CQJd73Gm.js} +1 -1
- package/dist/chunks/{LpButton-kqJBHIIb.js → LpButton-CKSwC6Mu.js} +1 -1
- package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
- package/dist/chunks/{LpCard-CpRByR0q.js → LpCard-C2IvucmX.js} +1 -1
- package/dist/chunks/{LpCheckbox-DpqchzH4.js → LpCheckbox-Bkj13CbW.js} +1 -1
- package/dist/chunks/{LpCodeBlock-DeHJPq7p.js → LpCodeBlock-ca7ANFyW.js} +1 -1
- package/dist/chunks/{LpColorPicker-Bhf1qxhB.js → LpColorPicker-zz4VGkG4.js} +46 -20
- package/dist/chunks/{LpConfirmDialog-CIUOSnWo.js → LpConfirmDialog-DCw5jR1t.js} +3 -3
- package/dist/chunks/{LpConfirmHost-CDm0Wtpv.js → LpConfirmHost-CbywMpMP.js} +1 -1
- package/dist/chunks/{LpContextMenu-DQw5dGfG.js → LpContextMenu-BqkAYa9X.js} +2 -1
- package/dist/chunks/{LpDatePicker-Drirx1AK.js → LpDatePicker-DuusgT69.js} +13 -9
- package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
- package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
- package/dist/chunks/{LpDrawer-bScx-KMO.js → LpDrawer-Dt4aB4Fr.js} +2 -2
- package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
- package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
- package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
- package/dist/chunks/{LpFileTree-CSI_5SK2.js → LpFileTree-CNurOvkV.js} +3 -3
- package/dist/chunks/{LpFileTreeNode-B4MiSQX2.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
- package/dist/chunks/{LpFormField-CIk5qosh.js → LpFormField-CtVYj6tO.js} +29 -15
- package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
- package/dist/chunks/{LpInput-C0e8y8_d.js → LpInput-D-YGQbEv.js} +39 -16
- package/dist/chunks/{LpLightbox-Mr0W_6HK.js → LpLightbox-DSDahF23.js} +1 -1
- package/dist/chunks/{LpLogViewer-YBOaFnc_.js → LpLogViewer-KypbgOyo.js} +252 -58
- package/dist/chunks/{LpMapPicker-NUzS9e37.js → LpMapPicker-Cc84LaM3.js} +1 -1
- package/dist/chunks/{LpMediaGallery-DfJmMW0I.js → LpMediaGallery-YKr-cQPN.js} +2 -2
- package/dist/chunks/{LpModal-DPwGKBPv.js → LpModal-DMMDQHAu.js} +3 -3
- package/dist/chunks/{LpModalNav-DPg5H9f3.js → LpModalNav-BVPDyIup.js} +4 -4
- package/dist/chunks/{LpNotificationBell-t5yn-siX.js → LpNotificationBell-CCwyJP5C.js} +26 -14
- package/dist/chunks/{LpNumberField-DWnQKktS.js → LpNumberField-B77O-z28.js} +1 -1
- package/dist/chunks/{LpPagination-DFngX7Xo.js → LpPagination-BcDlo3r_.js} +1 -1
- package/dist/chunks/{LpPasswordInput-CWVDuBJ5.js → LpPasswordInput-eKzQhbQP.js} +2 -2
- package/dist/chunks/{LpPhoneInput-CD-ZTeSd.js → LpPhoneInput-BCbrrhgE.js} +126 -23
- package/dist/chunks/{LpPie-BLco31Nx.js → LpPie-BEJ2SfI6.js} +9 -10
- package/dist/chunks/{LpPopover-p_e_shN3.js → LpPopover-DCKeqiVn.js} +1 -0
- package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
- package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
- package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
- package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
- package/dist/chunks/{LpSegmented-Dejd7cIx.js → LpSegmented-C09EEam2.js} +1 -1
- package/dist/chunks/{LpSelect-DrRD9I7L.js → LpSelect-DNIL6MB9.js} +20 -23
- package/dist/chunks/{LpSidebar-BgwUNWpJ.js → LpSidebar-CQNB0-yU.js} +3 -3
- package/dist/chunks/{LpSidebarNav-Bqxz8aWT.js → LpSidebarNav-kx-LdqNU.js} +3 -3
- package/dist/chunks/{LpStat-dXtyrdX9.js → LpStat-B-7udBRP.js} +1 -1
- package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
- package/dist/chunks/{LpSwitch-B1aLKulj.js → LpSwitch-OrcOJX9m.js} +1 -1
- package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
- package/dist/chunks/{LpTabs-_qMVaZJ6.js → LpTabs-29iW1XOx.js} +1 -1
- package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
- package/dist/chunks/{LpTextarea-yP8hGedR.js → LpTextarea-uuScWo6-.js} +1 -1
- package/dist/chunks/{LpThemeProvider-Wjm7Tuit.js → LpThemeProvider-Be4r2TQX.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-CKLc10KZ.js → LpThemeSwitcher-B4aVPbNC.js} +19 -22
- package/dist/chunks/{LpTimeField-C5HRgS5H.js → LpTimeField-Cqe7fXnh.js} +1 -1
- package/dist/chunks/{LpToaster--oT6lfhL.js → LpToaster-DpJvoCOh.js} +37 -3
- package/dist/chunks/{LpTooltip-CBdzeKNf.js → LpTooltip-CXVKcOXT.js} +3 -2
- package/dist/chunks/{LpTopologyCanvas-DocsLal3.js → LpTopologyCanvas-6ozNrQKh.js} +2 -1
- package/dist/chunks/{LpUptimeBar-DLtxOMRy.js → LpUptimeBar-ugcv25xj.js} +2 -2
- package/dist/chunks/icons-CkA0njzm.js +43 -0
- package/dist/chunks/{useTheme-C8JHUlPM.js → useTheme-DJm1JSsY.js} +47 -50
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.d.ts +2 -2
- package/dist/components/LpButton.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.d.ts +16 -1
- package/dist/components/LpChart.vue.js +63 -5
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +2 -2
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +6 -0
- package/dist/components/LpCommandPalette.vue.js +61 -10
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
- package/dist/components/LpDateRangePicker.vue.js +4 -0
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.d.ts +8 -0
- package/dist/components/LpFieldNotice.vue.js +4 -0
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.d.ts +4 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpIcon.vue.d.ts +0 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +2 -2
- package/dist/components/LpInput.vue.d.ts +19 -0
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.d.ts +52 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.d.ts +13 -0
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPopover.vue.js +1 -1
- package/dist/components/LpProgress.vue.d.ts +1 -1
- package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
- package/dist/components/LpRangeCalendar.vue.js +4 -0
- package/dist/components/LpRollingText.vue.d.ts +1 -1
- package/dist/components/LpRollingText.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +2 -2
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +6 -0
- package/dist/components/LpShortcutsDialog.vue.js +56 -14
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.d.ts +84 -2
- package/dist/components/LpTable.vue.js +481 -107
- package/dist/components/LpTableFilter.vue.d.ts +31 -0
- package/dist/components/LpTableFilter.vue.js +4 -0
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpTagsInput.vue.d.ts +77 -0
- package/dist/components/LpTagsInput.vue.js +4 -0
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeProvider.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.d.ts +2 -2
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/chart.js +3 -2
- package/dist/components/commandSearch.js +3 -0
- package/dist/components/dateRange.d.ts +43 -0
- package/dist/components/dateRange.js +76 -0
- package/dist/components/exportData.d.ts +58 -0
- package/dist/components/exportData.js +165 -0
- package/dist/components/logSearch.d.ts +18 -0
- package/dist/components/logSearch.js +36 -0
- package/dist/components/phone.d.ts +41 -0
- package/dist/components/phone.js +80 -0
- package/dist/components/tableFilter.d.ts +36 -0
- package/dist/components/tableFilter.js +84 -0
- package/dist/components/tags.d.ts +20 -0
- package/dist/components/tags.js +37 -0
- package/dist/composables/useField.d.ts +4 -0
- package/dist/composables/useFieldNotice.d.ts +17 -0
- package/dist/composables/useFieldNotice.js +28 -0
- package/dist/composables/useForm.d.ts +53 -0
- package/dist/composables/useForm.js +207 -0
- package/dist/composables/useHotkeys.d.ts +65 -5
- package/dist/composables/useHotkeys.js +170 -9
- package/dist/composables/useInputMask.d.ts +59 -0
- package/dist/composables/useInputMask.js +170 -0
- package/dist/composables/usePaintSelect.d.ts +22 -0
- package/dist/composables/usePaintSelect.js +169 -0
- package/dist/composables/useShift.js +7 -2
- package/dist/icons-all.d.ts +1 -0
- package/dist/icons-all.js +3 -0
- package/dist/icons.d.ts +14 -1
- package/dist/index.d.ts +29 -3
- package/dist/index.js +195 -138
- package/dist/kit-icons.d.ts +2 -0
- package/dist/theme/backdrop.d.ts +12 -0
- package/dist/theme/presets.d.ts +0 -2
- package/package.json +13 -4
- package/src/component-names.ts +4 -0
- package/src/components/ContextMenuItems.vue +1 -1
- package/src/components/LayoutNode.vue +11 -2
- package/src/components/LpActionBar.vue +1 -1
- package/src/components/LpAutocomplete.vue +1 -0
- package/src/components/LpAvatar.vue +2 -2
- package/src/components/LpButton.vue +1 -1
- package/src/components/LpChart.vue +80 -1
- package/src/components/LpColorPicker.vue +34 -1
- package/src/components/LpCommandPalette.vue +81 -7
- package/src/components/LpContextMenu.vue +1 -1
- package/src/components/LpDatePicker.vue +8 -3
- package/src/components/LpDateRangePicker.vue +224 -0
- package/src/components/LpDropdownMenu.vue +1 -0
- package/src/components/LpFieldNotice.vue +65 -0
- package/src/components/LpFormField.vue +33 -4
- package/src/components/LpIcon.vue +22 -4
- package/src/components/LpInput.vue +46 -6
- package/src/components/LpLogViewer.vue +225 -22
- package/src/components/LpModal.vue +1 -1
- package/src/components/LpNotificationBell.vue +15 -7
- package/src/components/LpPhoneInput.vue +179 -20
- package/src/components/LpPie.vue +8 -5
- package/src/components/LpPopover.vue +1 -0
- package/src/components/LpRangeCalendar.vue +365 -0
- package/src/components/LpRollingText.vue +3 -0
- package/src/components/LpScrollArea.vue +22 -2
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpShortcutsDialog.vue +77 -8
- package/src/components/LpStepper.vue +23 -10
- package/src/components/LpSwitch.vue +1 -1
- package/src/components/LpTable.vue +584 -65
- package/src/components/LpTableFilter.vue +175 -0
- package/src/components/LpTagsInput.vue +364 -0
- package/src/components/LpThemeSwitcher.vue +19 -23
- package/src/components/LpToaster.vue +49 -1
- package/src/components/LpTooltip.vue +9 -2
- package/src/components/LpTopologyCanvas.vue +7 -1
- package/src/components/chart.ts +5 -3
- package/src/components/commandSearch.ts +6 -0
- package/src/components/dateRange.ts +134 -0
- package/src/components/exportData.ts +228 -0
- package/src/components/logSearch.ts +46 -0
- package/src/components/phone.ts +140 -0
- package/src/components/tableFilter.ts +122 -0
- package/src/components/tags.ts +62 -0
- package/src/composables/useField.ts +4 -0
- package/src/composables/useFieldNotice.ts +59 -0
- package/src/composables/useForm.ts +314 -0
- package/src/composables/useHotkeys.ts +269 -13
- package/src/composables/useInputMask.ts +254 -0
- package/src/composables/usePaintSelect.ts +243 -0
- package/src/composables/useShift.ts +13 -2
- package/src/iconSubset.d.mts +10 -0
- package/src/iconSubset.mjs +51 -0
- package/src/icons-all.ts +10 -0
- package/src/icons.ts +59 -8
- package/src/index.ts +37 -3
- package/src/kit-icons.ts +5 -0
- package/src/nuxt.ts +30 -2
- package/src/runtime/theme.plugin.ts +2 -2
- package/src/theme/backdrop.ts +69 -0
- package/src/theme/presets.ts +6 -64
- 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="
|
|
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
|
|
@@ -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() || !
|
|
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
|
|
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
|
-
|
|
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="
|
|
1049
|
-
{{
|
|
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
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
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']"
|