@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.
Files changed (97) hide show
  1. package/dist/assets/LpNumberFlow-rZkwQwlU.css +116 -0
  2. package/dist/assets/LpRollingText-CR1mb0Vt.css +74 -0
  3. package/dist/assets/LpSkeleton-D7xU8IPV.css +29 -0
  4. package/dist/assets/LpSparkline-0mKTq7pb.css +51 -0
  5. package/dist/assets/LpTooltip-B61SYd4K.css +17 -0
  6. package/dist/assets/LpUptimeBar-B98o9krI.css +18 -0
  7. package/dist/chunks/LpChart-tNXs1ttr.js +671 -0
  8. package/dist/chunks/{LpConfirmDialog-CObRah9c.js → LpConfirmDialog-CjOGcj1e.js} +32 -4
  9. package/dist/chunks/{LpFileTree-H7Ho8uoq.js → LpFileTree-BaV7f0O7.js} +1 -1
  10. package/dist/chunks/{LpFileTreeNode-BEZIvxHi.js → LpFileTreeNode-BzpBa7R7.js} +5 -7
  11. package/dist/chunks/{LpLogViewer-B1aLUVdz.js → LpLogViewer-CcQ3s3Fp.js} +2 -1
  12. package/dist/chunks/{LpMapPicker-DFKrd1Ne.js → LpMapPicker-CXvoTrAe.js} +2 -4
  13. package/dist/chunks/{LpNotificationBell-X1M30e0X.js → LpNotificationBell-Dvu-b6Vn.js} +56 -27
  14. package/dist/chunks/LpNumberFlow-amWYaeuA.js +221 -0
  15. package/dist/chunks/LpPie-BkjWlfSw.js +525 -0
  16. package/dist/chunks/LpProgress-DoIP4jQR.js +198 -0
  17. package/dist/chunks/LpRollingText-CM5gkSeF.js +50 -0
  18. package/dist/chunks/LpSkeleton-t5mMYl3a.js +29 -0
  19. package/dist/chunks/LpSparkline-C6xQ2dwv.js +175 -0
  20. package/dist/chunks/{LpStat-CdbEFOvn.js → LpStat-CDanpzU0.js} +43 -5
  21. package/dist/chunks/{LpThemeSwitcher-BgpJgrnP.js → LpThemeSwitcher-7mAK6bI7.js} +2 -4
  22. package/dist/chunks/LpTooltip-hCLPAqMG.js +131 -0
  23. package/dist/chunks/LpUptimeBar-DcQ8HikG.js +152 -0
  24. package/dist/component-names.d.ts +1 -1
  25. package/dist/components/LpChart.vue.d.ts +88 -0
  26. package/dist/components/LpChart.vue.js +4 -0
  27. package/dist/components/LpConfirmDialog.vue.d.ts +18 -2
  28. package/dist/components/LpConfirmDialog.vue.js +1 -1
  29. package/dist/components/LpFileTree.vue.d.ts +1 -1
  30. package/dist/components/LpFileTree.vue.js +1 -1
  31. package/dist/components/LpFileTreeNode.vue.js +1 -1
  32. package/dist/components/LpLightbox.vue.d.ts +1 -1
  33. package/dist/components/LpLogViewer.vue.d.ts +1 -1
  34. package/dist/components/LpLogViewer.vue.js +1 -1
  35. package/dist/components/LpMapPicker.vue.js +1 -1
  36. package/dist/components/LpNotificationBell.vue.d.ts +12 -0
  37. package/dist/components/LpNotificationBell.vue.js +1 -1
  38. package/dist/components/LpNumberFlow.vue.d.ts +35 -0
  39. package/dist/components/LpNumberFlow.vue.js +4 -0
  40. package/dist/components/LpPie.vue.d.ts +134 -0
  41. package/dist/components/LpPie.vue.js +4 -0
  42. package/dist/components/LpProgress.vue.d.ts +56 -2
  43. package/dist/components/LpProgress.vue.js +1 -1
  44. package/dist/components/LpRollingText.vue.d.ts +33 -0
  45. package/dist/components/LpRollingText.vue.js +4 -0
  46. package/dist/components/LpSkeleton.vue.d.ts +11 -1
  47. package/dist/components/LpSkeleton.vue.js +1 -1
  48. package/dist/components/LpSparkline.vue.d.ts +48 -0
  49. package/dist/components/LpSparkline.vue.js +4 -0
  50. package/dist/components/LpStat.vue.d.ts +25 -2
  51. package/dist/components/LpStat.vue.js +1 -1
  52. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  53. package/dist/components/LpTooltip.vue.d.ts +24 -3
  54. package/dist/components/LpTooltip.vue.js +1 -1
  55. package/dist/components/LpUptimeBar.vue.js +1 -1
  56. package/dist/components/chart.d.ts +180 -0
  57. package/dist/components/chart.js +353 -0
  58. package/dist/composables/easing.d.ts +46 -0
  59. package/dist/composables/easing.js +23 -0
  60. package/dist/composables/usePillTransition.d.ts +13 -1
  61. package/dist/composables/usePillTransition.js +22 -2
  62. package/dist/composables/useReveal.d.ts +26 -0
  63. package/dist/composables/useReveal.js +45 -0
  64. package/dist/composables/useSettled.js +2 -2
  65. package/dist/composables/useShift.js +2 -4
  66. package/dist/index.d.ts +14 -0
  67. package/dist/index.js +125 -102
  68. package/package.json +2 -1
  69. package/src/component-names.ts +5 -0
  70. package/src/components/LpChart.vue +846 -0
  71. package/src/components/LpConfirmDialog.vue +49 -0
  72. package/src/components/LpFileTreeNode.vue +5 -9
  73. package/src/components/LpLogViewer.vue +2 -3
  74. package/src/components/LpMapPicker.vue +2 -5
  75. package/src/components/LpNotificationBell.vue +76 -15
  76. package/src/components/LpNumberFlow.vue +524 -0
  77. package/src/components/LpPie.vue +778 -0
  78. package/src/components/LpProgress.vue +283 -6
  79. package/src/components/LpRollingText.vue +157 -0
  80. package/src/components/LpSkeleton.vue +81 -8
  81. package/src/components/LpSparkline.vue +312 -0
  82. package/src/components/LpStat.vue +52 -4
  83. package/src/components/LpTooltip.vue +185 -3
  84. package/src/components/LpUptimeBar.vue +150 -53
  85. package/src/components/chart.ts +686 -0
  86. package/src/composables/easing.ts +88 -0
  87. package/src/composables/usePillTransition.ts +71 -6
  88. package/src/composables/useReveal.ts +113 -0
  89. package/src/composables/useSettled.ts +2 -4
  90. package/src/composables/useShift.ts +2 -8
  91. package/src/index.ts +19 -0
  92. package/src/theme/useTheme.ts +2 -8
  93. package/src/tokens/tokens.css +36 -0
  94. package/dist/chunks/LpProgress-BhI0IbjC.js +0 -37
  95. package/dist/chunks/LpSkeleton-eSFIo4kD.js +0 -22
  96. package/dist/chunks/LpTooltip-D1hbjQWG.js +0 -63
  97. 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 the sliding "pill" indicator used by LpTabs,
6
- * LpSegmented and LpPagination (a shared layoutId element that flies under the
7
- * active item). One spring keeps the feel identical across them; it collapses to
8
- * an instant move when the OS prefers reduced motion.
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 function usePillTransition(): ComputedRef<PillTransition> {
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 } : { type: "spring", stiffness: 520, damping: 40 },
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 reduced =
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 = reducedMotion() ? 0 : (toValue(options.duration) ?? TWEEN_MS)
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"
@@ -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: "cubic-bezier(0.2, 0, 0, 1)",
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
@@ -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
- };