@leavepulse/ui 0.24.1 → 0.26.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/dist/assets/LpNumberFlow-rZkwQwlU.css +116 -0
- package/dist/assets/LpRollingText-CR1mb0Vt.css +74 -0
- package/dist/assets/LpSkeleton-D7xU8IPV.css +29 -0
- package/dist/assets/LpSparkline-0mKTq7pb.css +51 -0
- package/dist/assets/LpTooltip-B61SYd4K.css +17 -0
- package/dist/assets/LpUptimeBar-B98o9krI.css +18 -0
- package/dist/chunks/LpChart-tNXs1ttr.js +671 -0
- package/dist/chunks/{LpConfirmDialog-CObRah9c.js → LpConfirmDialog-CjOGcj1e.js} +32 -4
- package/dist/chunks/{LpFileTree-H7Ho8uoq.js → LpFileTree-BaV7f0O7.js} +1 -1
- package/dist/chunks/{LpFileTreeNode-BEZIvxHi.js → LpFileTreeNode-BzpBa7R7.js} +5 -7
- package/dist/chunks/{LpLogViewer-B1aLUVdz.js → LpLogViewer-CcQ3s3Fp.js} +2 -1
- package/dist/chunks/{LpMapPicker-DFKrd1Ne.js → LpMapPicker-CXvoTrAe.js} +2 -4
- package/dist/chunks/{LpNotificationBell-X1M30e0X.js → LpNotificationBell-Dvu-b6Vn.js} +56 -27
- package/dist/chunks/LpNumberFlow-amWYaeuA.js +221 -0
- package/dist/chunks/LpPie-BkjWlfSw.js +525 -0
- package/dist/chunks/LpProgress-DoIP4jQR.js +198 -0
- package/dist/chunks/LpRollingText-CM5gkSeF.js +50 -0
- package/dist/chunks/LpSkeleton-t5mMYl3a.js +29 -0
- package/dist/chunks/LpSparkline-C6xQ2dwv.js +175 -0
- package/dist/chunks/{LpStat-CdbEFOvn.js → LpStat-CDanpzU0.js} +43 -5
- package/dist/chunks/{LpThemeSwitcher-BgpJgrnP.js → LpThemeSwitcher-7mAK6bI7.js} +2 -4
- package/dist/chunks/LpTooltip-hCLPAqMG.js +131 -0
- package/dist/chunks/LpUptimeBar-DcQ8HikG.js +152 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpChart.vue.d.ts +88 -0
- package/dist/components/LpChart.vue.js +4 -0
- package/dist/components/LpConfirmDialog.vue.d.ts +18 -2
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpFileTree.vue.d.ts +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpLightbox.vue.d.ts +1 -1
- package/dist/components/LpLogViewer.vue.d.ts +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.d.ts +12 -0
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberFlow.vue.d.ts +35 -0
- package/dist/components/LpNumberFlow.vue.js +4 -0
- package/dist/components/LpPie.vue.d.ts +134 -0
- package/dist/components/LpPie.vue.js +4 -0
- package/dist/components/LpProgress.vue.d.ts +56 -2
- package/dist/components/LpProgress.vue.js +1 -1
- package/dist/components/LpRollingText.vue.d.ts +33 -0
- package/dist/components/LpRollingText.vue.js +4 -0
- package/dist/components/LpSkeleton.vue.d.ts +11 -1
- package/dist/components/LpSkeleton.vue.js +1 -1
- package/dist/components/LpSparkline.vue.d.ts +48 -0
- package/dist/components/LpSparkline.vue.js +4 -0
- package/dist/components/LpStat.vue.d.ts +25 -2
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTooltip.vue.d.ts +24 -3
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/chart.d.ts +180 -0
- package/dist/components/chart.js +353 -0
- package/dist/composables/easing.d.ts +46 -0
- package/dist/composables/easing.js +23 -0
- package/dist/composables/usePillTransition.d.ts +13 -1
- package/dist/composables/usePillTransition.js +22 -2
- package/dist/composables/useReveal.d.ts +26 -0
- package/dist/composables/useReveal.js +45 -0
- package/dist/composables/useSettled.js +2 -2
- package/dist/composables/useShift.js +2 -4
- package/dist/index.d.ts +14 -0
- package/dist/index.js +125 -102
- package/package.json +2 -1
- package/src/component-names.ts +5 -0
- package/src/components/LpChart.vue +846 -0
- package/src/components/LpConfirmDialog.vue +49 -0
- package/src/components/LpFileTreeNode.vue +5 -9
- package/src/components/LpLogViewer.vue +2 -3
- package/src/components/LpMapPicker.vue +2 -5
- package/src/components/LpNotificationBell.vue +76 -15
- package/src/components/LpNumberFlow.vue +524 -0
- package/src/components/LpPie.vue +778 -0
- package/src/components/LpProgress.vue +283 -6
- package/src/components/LpRollingText.vue +157 -0
- package/src/components/LpSkeleton.vue +81 -8
- package/src/components/LpSparkline.vue +312 -0
- package/src/components/LpStat.vue +52 -4
- package/src/components/LpTooltip.vue +185 -3
- package/src/components/LpUptimeBar.vue +150 -53
- package/src/components/chart.ts +686 -0
- package/src/composables/easing.ts +88 -0
- package/src/composables/usePillTransition.ts +71 -6
- package/src/composables/useReveal.ts +113 -0
- package/src/composables/useSettled.ts +2 -4
- package/src/composables/useShift.ts +2 -8
- package/src/index.ts +19 -0
- package/src/theme/useTheme.ts +2 -8
- package/src/tokens/tokens.css +36 -0
- package/dist/chunks/LpProgress-BhI0IbjC.js +0 -37
- package/dist/chunks/LpSkeleton-eSFIo4kD.js +0 -22
- package/dist/chunks/LpTooltip-D1hbjQWG.js +0 -63
- package/dist/chunks/LpUptimeBar-CB366k0O.js +0 -127
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Easing curves for animations driven in JAVASCRIPT rather than by CSS.
|
|
3
|
+
*
|
|
4
|
+
* Most of the kit eases with a CSS custom property — `var(--ease-travel)` and
|
|
5
|
+
* friends in tokens.css. A few animations cannot: they interpolate a NUMBER
|
|
6
|
+
* that several elements then derive from (LpProgress drives an arc and its
|
|
7
|
+
* counter off one value; LpPie sweeps a clip path; LpNumberFlow moves a figure
|
|
8
|
+
* its digit columns are computed from). There is no property to transition, so
|
|
9
|
+
* the curve has to be a function.
|
|
10
|
+
*
|
|
11
|
+
* These are the counterparts of the CSS tokens, kept together so a component
|
|
12
|
+
* picks a named feel instead of open-coding a polynomial — which is how the
|
|
13
|
+
* curve in LpProgress and the one in LpNumberFlow drifted apart while a comment
|
|
14
|
+
* in each still claimed they matched.
|
|
15
|
+
*
|
|
16
|
+
* They are approximations of the cubic-beziers, not exact ports: a bezier needs
|
|
17
|
+
* solving per frame, and for these the shape is what matters, not the decimals.
|
|
18
|
+
* Change one and change its token in tokens.css.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Does the OS ask for less motion?
|
|
23
|
+
*
|
|
24
|
+
* Written out in eight places before this, in two different shapes — some
|
|
25
|
+
* guarded against SSR, some not, which is a crash in a Nuxt app rather than a
|
|
26
|
+
* style difference. Anything that animates asks here.
|
|
27
|
+
*
|
|
28
|
+
* A plain function, not a reactive composable: it is read at the moment an
|
|
29
|
+
* animation starts, and a component that also needs to RE-RENDER when the
|
|
30
|
+
* setting changes mid-session should use motion-v's `useReducedMotion`.
|
|
31
|
+
*/
|
|
32
|
+
export const prefersReducedMotion = (): boolean =>
|
|
33
|
+
typeof window !== "undefined" &&
|
|
34
|
+
(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A motion token's value, for an API that cannot take a `var()`.
|
|
38
|
+
*
|
|
39
|
+
* The Web Animations API and SVG's SMIL both want a literal easing string, so
|
|
40
|
+
* three call sites had `cubic-bezier(0.2, 0, 0, 1)` typed out — the value of
|
|
41
|
+
* `--ease-emphasized`, copied. A theme that retuned the token moved everything
|
|
42
|
+
* except those, and nothing said they were meant to match.
|
|
43
|
+
*
|
|
44
|
+
* Reading the custom property keeps them themeable. The fallback covers SSR and
|
|
45
|
+
* the frame before tokens are applied.
|
|
46
|
+
*/
|
|
47
|
+
export const easingToken = (
|
|
48
|
+
name: "ease-emphasized" | "ease-travel" | "ease-settle",
|
|
49
|
+
fallback = "cubic-bezier(0.2, 0, 0, 1)",
|
|
50
|
+
): string => {
|
|
51
|
+
if (typeof window === "undefined" || !window.getComputedStyle) return fallback
|
|
52
|
+
const value = getComputedStyle(document.documentElement).getPropertyValue(`--${name}`).trim()
|
|
53
|
+
return value || fallback
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Comes to rest — `--ease-settle`'s shape without the overshoot.
|
|
58
|
+
*
|
|
59
|
+
* Gently decelerating: the value keeps moving visibly for most of the travel
|
|
60
|
+
* instead of arriving early and then creeping. Use it wherever a figure counts
|
|
61
|
+
* up to something, so the digits keep turning across the whole duration.
|
|
62
|
+
*/
|
|
63
|
+
export const easeOut = (t: number): number => 1 - (1 - t) ** 2
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Travels a distance the eye follows — the JS form of `--ease-travel`.
|
|
67
|
+
*
|
|
68
|
+
* Nearly linear, and deliberately so: an ease-out spends about half its
|
|
69
|
+
* duration covering the last tenth of the path, so a stroke or sweep animated
|
|
70
|
+
* with one flashes past and then visibly creeps. Use this wherever the journey
|
|
71
|
+
* is the content — a line drawing itself, a wipe crossing a plot, a circle
|
|
72
|
+
* sweeping open.
|
|
73
|
+
*
|
|
74
|
+
* The ease is applied only near the ends, and the finish is NOT flattened onto
|
|
75
|
+
* the target: the motion is still moving when it lands, which is what keeps
|
|
76
|
+
* the tail short.
|
|
77
|
+
*/
|
|
78
|
+
export const easeTravel = (t: number): number => {
|
|
79
|
+
// Smooth the first and last ~20% and run straight through the middle.
|
|
80
|
+
const EDGE = 0.2
|
|
81
|
+
if (t < EDGE) return (t * t) / (2 * EDGE) + t * 0.1
|
|
82
|
+
if (t > 1 - EDGE) {
|
|
83
|
+
const r = 1 - t
|
|
84
|
+
return 1 - ((r * r) / (2 * EDGE) + r * 0.1)
|
|
85
|
+
}
|
|
86
|
+
const head = (EDGE * EDGE) / (2 * EDGE) + EDGE * 0.1
|
|
87
|
+
return head + ((t - EDGE) / (1 - 2 * EDGE)) * (1 - 2 * head)
|
|
88
|
+
}
|
|
@@ -2,18 +2,83 @@ import { useReducedMotion } from "motion-v"
|
|
|
2
2
|
import { computed, type ComputedRef } from "vue"
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
* Shared motion-v transition for
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Shared motion-v transition for an element that FLIES to a new position rather
|
|
6
|
+
* than cutting to it: the sliding "pill" indicator in LpTabs, LpSegmented and
|
|
7
|
+
* LpPagination (a shared layoutId element under the active item), and LpChart's
|
|
8
|
+
* tooltip trailing the cursor. One spring per feel keeps them consistent, and
|
|
9
|
+
* every preset collapses to an instant move when the OS prefers reduced motion.
|
|
10
|
+
*
|
|
11
|
+
* Two feels, because the two jobs want opposite things:
|
|
12
|
+
*
|
|
13
|
+
* "pill" — snappy. The move is a direct answer to a click, so it should read
|
|
14
|
+
* as already-arrived: high stiffness, almost no visible travel.
|
|
15
|
+
* "float" — trailing. The element chases a continuously moving pointer, so it
|
|
16
|
+
* wants perceptible lag and a soft landing; a pill-stiff spring
|
|
17
|
+
* tracks the cursor so exactly that it reads as jitter, not motion.
|
|
9
18
|
*/
|
|
10
19
|
export type PillTransition =
|
|
11
20
|
| { duration: number }
|
|
12
21
|
| { type: "spring"; stiffness: number; damping: number }
|
|
13
22
|
|
|
14
|
-
export
|
|
23
|
+
export type PillFeel = "pill" | "float"
|
|
24
|
+
|
|
25
|
+
const SPRING: Record<PillFeel, { type: "spring"; stiffness: number; damping: number }> = {
|
|
26
|
+
pill: { type: "spring", stiffness: 520, damping: 40 },
|
|
27
|
+
float: { type: "spring", stiffness: 210, damping: 26 },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function usePillTransition(feel: PillFeel = "pill"): ComputedRef<PillTransition> {
|
|
15
31
|
const reduceMotion = useReducedMotion()
|
|
16
32
|
return computed<PillTransition>(() =>
|
|
17
|
-
reduceMotion.value ? { duration: 0 } :
|
|
33
|
+
reduceMotion.value ? { duration: 0 } : SPRING[feel],
|
|
18
34
|
)
|
|
19
35
|
}
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* The same feels as a step function, for a follower that cannot be driven by
|
|
39
|
+
* motion-v.
|
|
40
|
+
*
|
|
41
|
+
* LpChart's readout is positioned inside its own plot, so it can hand motion-v
|
|
42
|
+
* an absolute target and let the library do the work. LpTooltip's panel is
|
|
43
|
+
* portalled into <body> and placed by reka; all it can contribute is an offset
|
|
44
|
+
* on top of that, which no declarative animation owns — so it integrates the
|
|
45
|
+
* spring itself, one frame at a time.
|
|
46
|
+
*
|
|
47
|
+
* Two mechanisms, then, but one place to tune them. The constants below are NOT
|
|
48
|
+
* the motion-v pair reused: an explicit integrator needs a mass-normalised
|
|
49
|
+
* stiffness and a damping ratio, and feeding it motion-v's numbers produced a
|
|
50
|
+
* spring so slack the panel crept toward the cursor and never arrived. They are
|
|
51
|
+
* matched by feel and kept beside their motion-v counterparts so that a change
|
|
52
|
+
* to one is an obvious prompt to check the other.
|
|
53
|
+
*/
|
|
54
|
+
const STEP_SPRING: Record<PillFeel, { stiffness: number; damping: number }> = {
|
|
55
|
+
// Critically damped: reaches the target quickly, never past it.
|
|
56
|
+
pill: { stiffness: 420, damping: 41 },
|
|
57
|
+
float: { stiffness: 260, damping: 32 },
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SpringState {
|
|
61
|
+
/** Current position; seed it with the start value. */
|
|
62
|
+
value: number
|
|
63
|
+
/** Current velocity; seed with 0. */
|
|
64
|
+
velocity: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function springStep(
|
|
68
|
+
state: SpringState,
|
|
69
|
+
target: number,
|
|
70
|
+
feel: PillFeel = "float",
|
|
71
|
+
/** Frame time in seconds; clamped so a backgrounded tab can't explode it. */
|
|
72
|
+
dt = 1 / 60,
|
|
73
|
+
): SpringState {
|
|
74
|
+
const { stiffness, damping } = STEP_SPRING[feel]
|
|
75
|
+
const step = Math.min(dt, 1 / 30)
|
|
76
|
+
const force = (target - state.value) * stiffness
|
|
77
|
+
const drag = state.velocity * damping
|
|
78
|
+
const velocity = state.velocity + (force - drag) * step
|
|
79
|
+
return { value: state.value + velocity * step, velocity }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** True once a spring is close enough to its target to stop animating. */
|
|
83
|
+
export const springSettled = (state: SpringState, target: number): boolean =>
|
|
84
|
+
Math.abs(target - state.value) < 0.3 && Math.abs(state.velocity) < 0.3
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { onBeforeUnmount, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from "vue"
|
|
2
|
+
import { prefersReducedMotion } from "./easing"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* "Has this element actually been SEEN yet?"
|
|
6
|
+
*
|
|
7
|
+
* An entrance animation is a statement about arrival, and mounting is not
|
|
8
|
+
* arrival. A component mounts when Vue builds its subtree — which happens while
|
|
9
|
+
* the tab is still painting its first frame, while the element sits below the
|
|
10
|
+
* fold, inside a panel that is closed, or in a tab nobody has switched to. By
|
|
11
|
+
* the time any of those become visible the animation has already run to the
|
|
12
|
+
* end, and the user sees the finished state and wonders why the thing they were
|
|
13
|
+
* told animates does not.
|
|
14
|
+
*
|
|
15
|
+
* The gap is easy to miss in development, because a hot reload remounts an
|
|
16
|
+
* element that is already on screen — the one case where mount and arrival do
|
|
17
|
+
* coincide. It shows up on a cold page load, which is the only load a user ever
|
|
18
|
+
* performs.
|
|
19
|
+
*
|
|
20
|
+
* So the trigger is intersection, not lifecycle: the flag flips the first time
|
|
21
|
+
* the element is both in the viewport and in a visible subtree, and never
|
|
22
|
+
* flips back. Entrances do not replay — an element that scrolls away and
|
|
23
|
+
* returns has already arrived, and re-drawing it every time it crosses the
|
|
24
|
+
* fold reads as a page that will not settle.
|
|
25
|
+
*
|
|
26
|
+
* Reduced motion resolves immediately: there is no entrance to protect, and
|
|
27
|
+
* making the content wait for an observer would only delay it.
|
|
28
|
+
*
|
|
29
|
+
* Every animated element in the kit routes through this, so "animate on
|
|
30
|
+
* appearance" means the same thing in a sparkline, a chart and a progress bar,
|
|
31
|
+
* and a consumer scrolling a dashboard sees each of them draw as it comes into
|
|
32
|
+
* view rather than finding them all already finished.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export interface UseRevealOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Skip the wait and reveal at once. For a caller that has switched its
|
|
38
|
+
* entrance off — there is nothing to gate, and holding content back for an
|
|
39
|
+
* observer that no longer guards anything only delays the first paint.
|
|
40
|
+
*/
|
|
41
|
+
immediate?: MaybeRefOrGetter<boolean | undefined>
|
|
42
|
+
/**
|
|
43
|
+
* Fraction of the element that must be on screen. A sliver of a tall chart
|
|
44
|
+
* scrolling into view is not yet an arrival; a quarter of it is.
|
|
45
|
+
*/
|
|
46
|
+
threshold?: number
|
|
47
|
+
/**
|
|
48
|
+
* Grow the viewport for the test, so an element just past the fold starts
|
|
49
|
+
* drawing as it is scrolled toward rather than after it lands.
|
|
50
|
+
*/
|
|
51
|
+
rootMargin?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface UseReveal {
|
|
55
|
+
/** Attach to the element whose appearance starts the animation. */
|
|
56
|
+
el: Ref<Element | null>
|
|
57
|
+
/** False until that element has been seen; true forever after. */
|
|
58
|
+
revealed: Ref<boolean>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useReveal(options: UseRevealOptions = {}): UseReveal {
|
|
62
|
+
const { threshold = 0.25, rootMargin = "0px 0px -10% 0px" } = options
|
|
63
|
+
|
|
64
|
+
const el = ref<Element | null>(null)
|
|
65
|
+
// SSR has no viewport to intersect with, and no animation runs there either;
|
|
66
|
+
// the client takes over on hydration and gates it properly then.
|
|
67
|
+
const revealed = ref(typeof window === "undefined")
|
|
68
|
+
|
|
69
|
+
let observer: IntersectionObserver | null = null
|
|
70
|
+
|
|
71
|
+
function stop() {
|
|
72
|
+
observer?.disconnect()
|
|
73
|
+
observer = null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function reveal() {
|
|
77
|
+
revealed.value = true
|
|
78
|
+
stop()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
watch(
|
|
82
|
+
[el, () => toValue(options.immediate)],
|
|
83
|
+
([node, immediate]) => {
|
|
84
|
+
if (revealed.value) return
|
|
85
|
+
if (immediate || prefersReducedMotion()) {
|
|
86
|
+
reveal()
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
stop()
|
|
90
|
+
if (!node) return
|
|
91
|
+
|
|
92
|
+
// No IntersectionObserver (older WebKitGTK in the launcher shell, jsdom
|
|
93
|
+
// under test): show the animation rather than withholding the content.
|
|
94
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
95
|
+
reveal()
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
observer = new IntersectionObserver(
|
|
100
|
+
(entries) => {
|
|
101
|
+
if (entries.some((entry) => entry.isIntersecting)) reveal()
|
|
102
|
+
},
|
|
103
|
+
{ threshold, rootMargin },
|
|
104
|
+
)
|
|
105
|
+
observer.observe(node)
|
|
106
|
+
},
|
|
107
|
+
{ immediate: true, flush: "post" },
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
onBeforeUnmount(stop)
|
|
111
|
+
|
|
112
|
+
return { el, revealed }
|
|
113
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { onScopeDispose, ref, watch, type MaybeRefOrGetter, toValue } from "vue"
|
|
2
|
+
import { prefersReducedMotion } from "./easing"
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
* "Has the opening animation had the frame to itself yet?"
|
|
@@ -73,10 +74,7 @@ export function useSettled(
|
|
|
73
74
|
// id stays open while its whole body is replaced, and that second mount
|
|
74
75
|
// is as expensive as the first.
|
|
75
76
|
settled.value = false
|
|
76
|
-
const
|
|
77
|
-
typeof window !== "undefined" &&
|
|
78
|
-
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
|
|
79
|
-
const wait = reduced ? 0 : (toValue(options.delay) ?? SETTLE_MS)
|
|
77
|
+
const wait = prefersReducedMotion() ? 0 : (toValue(options.delay) ?? SETTLE_MS)
|
|
80
78
|
timer = setTimeout(() => {
|
|
81
79
|
settled.value = true
|
|
82
80
|
timer = undefined
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { onBeforeUnmount, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from "vue"
|
|
2
|
+
import { prefersReducedMotion } from "./easing"
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
* "Let this box shift its neighbours smoothly instead of jolting them."
|
|
@@ -85,13 +86,6 @@ export function useShift(
|
|
|
85
86
|
const wantsHeight = () => axisOf() !== "width"
|
|
86
87
|
const wantsWidth = () => axisOf() !== "height"
|
|
87
88
|
|
|
88
|
-
function reducedMotion() {
|
|
89
|
-
return (
|
|
90
|
-
typeof window !== "undefined" &&
|
|
91
|
-
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
89
|
/*
|
|
96
90
|
* The size is driven imperatively rather than through a style binding. By the
|
|
97
91
|
* time replacement content is in the DOM the box has already reflowed to its
|
|
@@ -153,7 +147,7 @@ export function useShift(
|
|
|
153
147
|
inFlight = true
|
|
154
148
|
resizing.value = true
|
|
155
149
|
clearTimeout(settleTimer)
|
|
156
|
-
const wait =
|
|
150
|
+
const wait = prefersReducedMotion() ? 0 : (toValue(options.duration) ?? TWEEN_MS)
|
|
157
151
|
settleTimer = setTimeout(() => {
|
|
158
152
|
inFlight = false
|
|
159
153
|
resizing.value = false
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,15 @@ export type { Crumb } from "./components/LpBreadcrumbs.vue"
|
|
|
32
32
|
export { default as LpButton } from "./components/LpButton.vue"
|
|
33
33
|
export { default as LpCalendar } from "./components/LpCalendar.vue"
|
|
34
34
|
export { default as LpCard } from "./components/LpCard.vue"
|
|
35
|
+
export { default as LpChart } from "./components/LpChart.vue"
|
|
36
|
+
export type { ChartPointEvent } from "./components/LpChart.vue"
|
|
37
|
+
export {
|
|
38
|
+
CHART_PALETTE,
|
|
39
|
+
axisTicks,
|
|
40
|
+
formatTick,
|
|
41
|
+
niceScale,
|
|
42
|
+
} from "./components/chart"
|
|
43
|
+
export type { ChartAxis, ChartPoint, ChartSeries } from "./components/chart"
|
|
35
44
|
export { default as LpCheckbox } from "./components/LpCheckbox.vue"
|
|
36
45
|
export { default as LpCodeBlock } from "./components/LpCodeBlock.vue"
|
|
37
46
|
export type { CodeLang } from "./components/codeHighlight"
|
|
@@ -80,9 +89,14 @@ export type {
|
|
|
80
89
|
NotificationLabels,
|
|
81
90
|
} from "./components/LpNotificationBell.vue"
|
|
82
91
|
export { default as LpNumberField } from "./components/LpNumberField.vue"
|
|
92
|
+
export { default as LpNumberFlow } from "./components/LpNumberFlow.vue"
|
|
83
93
|
export { default as LpOtpInput } from "./components/LpOtpInput.vue"
|
|
84
94
|
export { default as LpPagination } from "./components/LpPagination.vue"
|
|
85
95
|
export { default as LpPasswordInput } from "./components/LpPasswordInput.vue"
|
|
96
|
+
export { default as LpPie } from "./components/LpPie.vue"
|
|
97
|
+
export type { SliceEvent } from "./components/LpPie.vue"
|
|
98
|
+
export { resolveSlices } from "./components/chart"
|
|
99
|
+
export type { PieSlice, ResolvedSlice } from "./components/chart"
|
|
86
100
|
export { default as LpPhoneInput } from "./components/LpPhoneInput.vue"
|
|
87
101
|
export type { PhoneDetail } from "./components/LpPhoneInput.vue"
|
|
88
102
|
export { flagEmoji, loadCountries, matchCountryByValue } from "./components/countries"
|
|
@@ -92,6 +106,7 @@ export { default as LpProgress } from "./components/LpProgress.vue"
|
|
|
92
106
|
export { default as LpRadio } from "./components/LpRadio.vue"
|
|
93
107
|
export { default as LpRadioGroup } from "./components/LpRadioGroup.vue"
|
|
94
108
|
export type { RadioOption } from "./components/LpRadioGroup.vue"
|
|
109
|
+
export { default as LpRollingText } from "./components/LpRollingText.vue"
|
|
95
110
|
export { default as LpScrollArea } from "./components/LpScrollArea.vue"
|
|
96
111
|
export { default as LpSegmented } from "./components/LpSegmented.vue"
|
|
97
112
|
export type { SegmentedOption } from "./components/LpSegmented.vue"
|
|
@@ -106,6 +121,7 @@ export { default as LpSidebar } from "./components/LpSidebar.vue"
|
|
|
106
121
|
export type { SidebarItem, SidebarSection } from "./components/LpSidebar.vue"
|
|
107
122
|
export { default as LpSkeleton } from "./components/LpSkeleton.vue"
|
|
108
123
|
export { default as LpSlider } from "./components/LpSlider.vue"
|
|
124
|
+
export { default as LpSparkline } from "./components/LpSparkline.vue"
|
|
109
125
|
export { default as LpStat } from "./components/LpStat.vue"
|
|
110
126
|
export { default as LpStepper } from "./components/LpStepper.vue"
|
|
111
127
|
export type { Step } from "./components/LpStepper.vue"
|
|
@@ -189,6 +205,9 @@ export { useHotkeys, isAppleKeyboard, appleModifierLabel } from "./composables/u
|
|
|
189
205
|
export type { HotkeyBinding } from "./composables/useHotkeys"
|
|
190
206
|
export { useInputFilter } from "./composables/useInputFilter"
|
|
191
207
|
export type { UseInputFilterOptions } from "./composables/useInputFilter"
|
|
208
|
+
export { easeOut, easeTravel, easingToken, prefersReducedMotion } from "./composables/easing"
|
|
209
|
+
export { useReveal } from "./composables/useReveal"
|
|
210
|
+
export type { UseReveal, UseRevealOptions } from "./composables/useReveal"
|
|
192
211
|
export { useRovingFocus } from "./composables/useRovingFocus"
|
|
193
212
|
export type { RovingItem, UseRovingFocusOptions } from "./composables/useRovingFocus"
|
|
194
213
|
export { useSettled } from "./composables/useSettled"
|
package/src/theme/useTheme.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* serialize/parse with validation.
|
|
6
6
|
*/
|
|
7
7
|
import { computed, readonly, shallowRef, type ComputedRef, type DeepReadonly, type Ref } from "vue"
|
|
8
|
+
import { easingToken, prefersReducedMotion } from "../composables/easing"
|
|
8
9
|
import { detectBackdropFilter } from "./backdrop"
|
|
9
10
|
import {
|
|
10
11
|
COLOR_VARS,
|
|
@@ -91,13 +92,6 @@ type ViewTransitionDocument = Document & {
|
|
|
91
92
|
startViewTransition?: (cb: () => void) => { ready: Promise<void>; finished: Promise<void> }
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
function prefersReducedMotion(): boolean {
|
|
95
|
-
return (
|
|
96
|
-
typeof window !== "undefined" &&
|
|
97
|
-
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
95
|
/*
|
|
102
96
|
* Apply a theme with the "circular reveal" transition — a clip-path circle
|
|
103
97
|
* grows from `origin` (usually the toggle button's centre), repainting the page
|
|
@@ -139,7 +133,7 @@ export function applyThemeWithTransition(
|
|
|
139
133
|
},
|
|
140
134
|
{
|
|
141
135
|
duration: durationMs,
|
|
142
|
-
easing: "
|
|
136
|
+
easing: easingToken("ease-emphasized"),
|
|
143
137
|
// Hold the fully-shrunk circle after the keyframes end. Without it the
|
|
144
138
|
// old snapshot snaps back to its default (un-clipped) state for the one
|
|
145
139
|
// frame between animation-end and the browser tearing down the
|
package/src/tokens/tokens.css
CHANGED
|
@@ -97,6 +97,42 @@
|
|
|
97
97
|
|
|
98
98
|
/* ── motion ──────────────────────────────────── */
|
|
99
99
|
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
|
|
100
|
+
/* For motion that TRAVELS a distance the eye is meant to follow: a line
|
|
101
|
+
drawing itself, a wipe crossing a plot, a bar filling — anything where the
|
|
102
|
+
journey is the content rather than a way of arriving.
|
|
103
|
+
|
|
104
|
+
Very nearly LINEAR, and that is the point.
|
|
105
|
+
|
|
106
|
+
The usual "ease-out by default" rule is about interaction: the fast start
|
|
107
|
+
answers a click, so the interface feels responsive. A chart drawing itself
|
|
108
|
+
answers no click, and ease-out actively hurts it — every ease-out curve,
|
|
109
|
+
tuned however you like, spends 45–65% of its duration covering the final
|
|
110
|
+
10% of the path. The stroke flashes past and then visibly creeps, so the
|
|
111
|
+
part worth watching is the part you miss.
|
|
112
|
+
|
|
113
|
+
The same reasoning the field applies to progress bars and other steady,
|
|
114
|
+
time-representing motion, where an even rate is what makes the progress
|
|
115
|
+
readable.
|
|
116
|
+
|
|
117
|
+
Note the second control point's y is 0.85, NOT 1. A curve ending on y=1
|
|
118
|
+
arrives with zero velocity and therefore always creeps, however gentle the
|
|
119
|
+
rest of it looks — that single coordinate is what separates a 22% tail
|
|
120
|
+
from a 46% one. Holding it below the line lets the stroke still be moving
|
|
121
|
+
when it lands. */
|
|
122
|
+
--ease-travel: cubic-bezier(0.2, 0.25, 0.5, 0.85);
|
|
123
|
+
/* For something that COMES TO REST: an odometer wheel stopping on its digit,
|
|
124
|
+
a value landing after a count, a knob settling after a flick.
|
|
125
|
+
|
|
126
|
+
It overshoots very slightly and drifts back, which is what a real mechanism
|
|
127
|
+
does — mass carries a wheel a hair past its mark before it settles. Without
|
|
128
|
+
it a digit arrives by simply ceasing to move, and a row of columns all
|
|
129
|
+
ceasing together reads as a screen redraw rather than as a thing coming to
|
|
130
|
+
a stop.
|
|
131
|
+
|
|
132
|
+
The overshoot is deliberately small. Anything more bounces, and a figure
|
|
133
|
+
that bounces past its value looks briefly WRONG — which matters when the
|
|
134
|
+
number is a real reading somebody is about to act on. */
|
|
135
|
+
--ease-settle: cubic-bezier(0.22, 1.12, 0.36, 1);
|
|
100
136
|
/* Three-step motion scale, themeable like everything else. fast = state
|
|
101
137
|
feedback (hover/press/focus); medium = transforms & most enter/exit;
|
|
102
138
|
slow = larger reveals (disclosure height, drawer slide).
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { defineComponent, computed, openBlock, createBlock, unref, withCtx, createVNode, normalizeStyle, normalizeClass } from "vue";
|
|
2
|
-
import { ProgressRoot, ProgressIndicator } from "reka-ui";
|
|
3
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
4
|
-
__name: "LpProgress",
|
|
5
|
-
props: {
|
|
6
|
-
value: { default: 0 },
|
|
7
|
-
max: { default: 100 },
|
|
8
|
-
tone: { default: "brand" }
|
|
9
|
-
},
|
|
10
|
-
setup(__props) {
|
|
11
|
-
const props = __props;
|
|
12
|
-
const pct = computed(() => Math.max(0, Math.min(100, props.value / props.max * 100)));
|
|
13
|
-
const bar = {
|
|
14
|
-
brand: "bg-brand",
|
|
15
|
-
action: "bg-action",
|
|
16
|
-
danger: "bg-danger"
|
|
17
|
-
};
|
|
18
|
-
return (_ctx, _cache) => {
|
|
19
|
-
return openBlock(), createBlock(unref(ProgressRoot), {
|
|
20
|
-
"model-value": __props.value,
|
|
21
|
-
max: __props.max,
|
|
22
|
-
class: "h-1.5 w-full overflow-hidden rounded-pill bg-surface-soft"
|
|
23
|
-
}, {
|
|
24
|
-
default: withCtx(() => [
|
|
25
|
-
createVNode(unref(ProgressIndicator), {
|
|
26
|
-
class: normalizeClass(["h-full rounded-pill transition-[width] duration-slow ease-[var(--ease-emphasized)]", bar[__props.tone]]),
|
|
27
|
-
style: normalizeStyle({ width: `${pct.value}%` })
|
|
28
|
-
}, null, 8, ["class", "style"])
|
|
29
|
-
]),
|
|
30
|
-
_: 1
|
|
31
|
-
}, 8, ["model-value", "max"]);
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
export {
|
|
36
|
-
_sfc_main as _
|
|
37
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { defineComponent, openBlock, createElementBlock, normalizeClass } from "vue";
|
|
2
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
3
|
-
__name: "LpSkeleton",
|
|
4
|
-
props: {
|
|
5
|
-
rounded: {}
|
|
6
|
-
},
|
|
7
|
-
setup(__props) {
|
|
8
|
-
return (_ctx, _cache) => {
|
|
9
|
-
return openBlock(), createElementBlock("div", {
|
|
10
|
-
"aria-hidden": "true",
|
|
11
|
-
class: normalizeClass(["animate-pulse bg-surface-soft", {
|
|
12
|
-
"rounded-control": __props.rounded === "control" || !__props.rounded,
|
|
13
|
-
"rounded-card": __props.rounded === "card",
|
|
14
|
-
"rounded-pill": __props.rounded === "pill"
|
|
15
|
-
}])
|
|
16
|
-
}, null, 2);
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
export {
|
|
21
|
-
_sfc_main as _
|
|
22
|
-
};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { defineComponent, openBlock, createBlock, unref, withCtx, createVNode, renderSlot, normalizeClass, createTextVNode, toDisplayString } from "vue";
|
|
2
|
-
import { TooltipProvider, TooltipRoot, TooltipTrigger, TooltipPortal, TooltipContent } from "reka-ui";
|
|
3
|
-
import { PANEL_SURFACE, TOOLTIP_ANIM } from "../components/dropdown.js";
|
|
4
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
5
|
-
__name: "LpTooltip",
|
|
6
|
-
props: {
|
|
7
|
-
content: {},
|
|
8
|
-
sideOffset: {},
|
|
9
|
-
side: { default: "top" },
|
|
10
|
-
label: { type: Boolean },
|
|
11
|
-
delay: { default: 200 },
|
|
12
|
-
showOnPointerFocus: { type: Boolean }
|
|
13
|
-
},
|
|
14
|
-
setup(__props) {
|
|
15
|
-
return (_ctx, _cache) => {
|
|
16
|
-
return openBlock(), createBlock(unref(TooltipProvider), { "delay-duration": __props.delay }, {
|
|
17
|
-
default: withCtx(() => [
|
|
18
|
-
createVNode(unref(TooltipRoot), {
|
|
19
|
-
"ignore-non-keyboard-focus": !__props.showOnPointerFocus
|
|
20
|
-
}, {
|
|
21
|
-
default: withCtx(() => [
|
|
22
|
-
createVNode(unref(TooltipTrigger), {
|
|
23
|
-
"as-child": "",
|
|
24
|
-
"aria-label": __props.label ? __props.content : void 0
|
|
25
|
-
}, {
|
|
26
|
-
default: withCtx(() => [
|
|
27
|
-
renderSlot(_ctx.$slots, "default")
|
|
28
|
-
]),
|
|
29
|
-
_: 3
|
|
30
|
-
}, 8, ["aria-label"]),
|
|
31
|
-
createVNode(unref(TooltipPortal), null, {
|
|
32
|
-
default: withCtx(() => [
|
|
33
|
-
createVNode(unref(TooltipContent), {
|
|
34
|
-
side: __props.side,
|
|
35
|
-
"side-offset": __props.sideOffset ?? 6,
|
|
36
|
-
class: normalizeClass([
|
|
37
|
-
unref(PANEL_SURFACE),
|
|
38
|
-
unref(TOOLTIP_ANIM),
|
|
39
|
-
"z-(--z-tooltip) max-w-[min(20rem,90vw)] px-2.5 py-1.5 text-xs text-ink"
|
|
40
|
-
])
|
|
41
|
-
}, {
|
|
42
|
-
default: withCtx(() => [
|
|
43
|
-
renderSlot(_ctx.$slots, "content", {}, () => [
|
|
44
|
-
createTextVNode(toDisplayString(__props.content), 1)
|
|
45
|
-
])
|
|
46
|
-
]),
|
|
47
|
-
_: 3
|
|
48
|
-
}, 8, ["side", "side-offset", "class"])
|
|
49
|
-
]),
|
|
50
|
-
_: 3
|
|
51
|
-
})
|
|
52
|
-
]),
|
|
53
|
-
_: 3
|
|
54
|
-
}, 8, ["ignore-non-keyboard-focus"])
|
|
55
|
-
]),
|
|
56
|
-
_: 3
|
|
57
|
-
}, 8, ["delay-duration"]);
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
export {
|
|
62
|
-
_sfc_main as _
|
|
63
|
-
};
|