@flux-ui/visuals 0.0.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 +32 -0
- package/dist/component/FluxVisualAnimatedColors.vue.d.ts +10 -0
- package/dist/component/FluxVisualAttention.vue.d.ts +40 -0
- package/dist/component/FluxVisualBorderBeam.vue.d.ts +31 -0
- package/dist/component/FluxVisualBorderShine.vue.d.ts +53 -0
- package/dist/component/FluxVisualDotPattern.vue.d.ts +11 -0
- package/dist/component/FluxVisualFlickeringGrid.vue.d.ts +10 -0
- package/dist/component/FluxVisualGridPattern.vue.d.ts +10 -0
- package/dist/component/FluxVisualHighlighter.vue.d.ts +36 -0
- package/dist/component/FluxVisualHighlighterGroup.vue.d.ts +13 -0
- package/dist/component/FluxVisualNoise.vue.d.ts +9 -0
- package/dist/component/FluxVisualNumberFlow.vue.d.ts +14 -0
- package/dist/component/FluxVisualPing.vue.d.ts +8 -0
- package/dist/component/FluxVisualSlotText.vue.d.ts +41 -0
- package/dist/component/FluxVisualTextScramble.vue.d.ts +20 -0
- package/dist/component/FluxVisualTextShimmer.vue.d.ts +18 -0
- package/dist/component/index.d.ts +15 -0
- package/dist/composable/private/index.d.ts +2 -0
- package/dist/composable/private/useBorderBeamPulse.d.ts +21 -0
- package/dist/composable/private/useHighlighterGroup.d.ts +51 -0
- package/dist/index.css +1110 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12320 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/package.json +71 -0
- package/src/component/FluxVisualAnimatedColors.vue +187 -0
- package/src/component/FluxVisualAttention.vue +86 -0
- package/src/component/FluxVisualBorderBeam.vue +171 -0
- package/src/component/FluxVisualBorderShine.vue +36 -0
- package/src/component/FluxVisualDotPattern.vue +119 -0
- package/src/component/FluxVisualFlickeringGrid.vue +159 -0
- package/src/component/FluxVisualGridPattern.vue +128 -0
- package/src/component/FluxVisualHighlighter.vue +217 -0
- package/src/component/FluxVisualHighlighterGroup.vue +20 -0
- package/src/component/FluxVisualNoise.vue +31 -0
- package/src/component/FluxVisualNumberFlow.vue +235 -0
- package/src/component/FluxVisualPing.vue +26 -0
- package/src/component/FluxVisualSlotText.vue +457 -0
- package/src/component/FluxVisualTextScramble.vue +168 -0
- package/src/component/FluxVisualTextShimmer.vue +34 -0
- package/src/component/index.ts +15 -0
- package/src/composable/private/index.ts +2 -0
- package/src/composable/private/useBorderBeamPulse.ts +203 -0
- package/src/composable/private/useHighlighterGroup.ts +162 -0
- package/src/css/component/Attention.module.scss +104 -0
- package/src/css/component/BorderBeam.module.scss +1178 -0
- package/src/css/component/Highlighter.module.scss +7 -0
- package/src/css/component/Noise.module.scss +80 -0
- package/src/css/component/NumberFlow.module.scss +5 -0
- package/src/css/component/PatternGlow.module.scss +19 -0
- package/src/css/component/Ping.module.scss +44 -0
- package/src/css/component/SlotText.module.scss +43 -0
- package/src/css/component/TextScramble.module.scss +5 -0
- package/src/css/component/TextShimmer.module.scss +54 -0
- package/src/css/component/Visual.module.scss +71 -0
- package/src/index.ts +1 -0
- package/src/types.d.ts +13 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
ref="label"
|
|
4
|
+
:aria-label="accessibleValue"
|
|
5
|
+
:class="$style.numberFlow">{{ initialText }}</span>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script
|
|
9
|
+
lang="ts"
|
|
10
|
+
setup>
|
|
11
|
+
import { prefersReducedMotion } from '@flux-ui/internals';
|
|
12
|
+
import { computed, onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue';
|
|
13
|
+
import $style from '~flux/visuals/css/component/NumberFlow.module.scss';
|
|
14
|
+
|
|
15
|
+
type NumberFlowEasingKeyword = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
16
|
+
|
|
17
|
+
type NumberFlowEasingFunction = (t: number) => number;
|
|
18
|
+
|
|
19
|
+
type NumberFlowEasing = NumberFlowEasingKeyword | `cubic-bezier(${string})` | NumberFlowEasingFunction;
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
value,
|
|
23
|
+
animateOnMount = true,
|
|
24
|
+
duration = 800,
|
|
25
|
+
// The default matches the --swift-out custom property from @flux-ui/components
|
|
26
|
+
// (packages/components/src/css/variables.scss). Keep these control points in
|
|
27
|
+
// sync with that variable so the tween matches the rest of the design system.
|
|
28
|
+
easing = 'cubic-bezier(0.55, 0, 0.1, 1)',
|
|
29
|
+
format,
|
|
30
|
+
locale
|
|
31
|
+
} = defineProps<{
|
|
32
|
+
readonly value: number;
|
|
33
|
+
readonly animateOnMount?: boolean;
|
|
34
|
+
readonly duration?: number;
|
|
35
|
+
readonly easing?: NumberFlowEasing;
|
|
36
|
+
readonly format?: Intl.NumberFormatOptions;
|
|
37
|
+
readonly locale?: string;
|
|
38
|
+
}>();
|
|
39
|
+
|
|
40
|
+
// Named easing curves for the rAF tween. A CSS easing string cannot be
|
|
41
|
+
// sampled per frame, so the value tween uses these functions instead.
|
|
42
|
+
const EASINGS: Record<NumberFlowEasingKeyword, NumberFlowEasingFunction> = {
|
|
43
|
+
'linear': progress => progress,
|
|
44
|
+
'ease-in': progress => progress * progress,
|
|
45
|
+
'ease-out': progress => 1 - (1 - progress) * (1 - progress),
|
|
46
|
+
'ease-in-out': progress => progress < .5 ? 2 * progress * progress : 1 - ((-2 * progress + 2) ** 2) / 2
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const CUBIC_BEZIER_PATTERN = /^cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)$/;
|
|
50
|
+
|
|
51
|
+
// Evaluate a cubic-bezier(x1, y1, x2, y2) timing function in JS. The control
|
|
52
|
+
// points describe x(t) and y(t); for a given progress we need y at the t where
|
|
53
|
+
// x(t) === progress. x is solved with Newton-Raphson and a bisection fallback,
|
|
54
|
+
// mirroring the standard UnitBezier approach browsers use for CSS easings.
|
|
55
|
+
function cubicBezier(x1: number, y1: number, x2: number, y2: number): NumberFlowEasingFunction {
|
|
56
|
+
const ax = 3 * x1 - 3 * x2 + 1;
|
|
57
|
+
const bx = 3 * x2 - 6 * x1;
|
|
58
|
+
const cx = 3 * x1;
|
|
59
|
+
|
|
60
|
+
const ay = 3 * y1 - 3 * y2 + 1;
|
|
61
|
+
const by = 3 * y2 - 6 * y1;
|
|
62
|
+
const cy = 3 * y1;
|
|
63
|
+
|
|
64
|
+
const sampleX = (t: number): number => ((ax * t + bx) * t + cx) * t;
|
|
65
|
+
const sampleY = (t: number): number => ((ay * t + by) * t + cy) * t;
|
|
66
|
+
const slopeX = (t: number): number => (3 * ax * t + 2 * bx) * t + cx;
|
|
67
|
+
|
|
68
|
+
const solveX = (x: number): number => {
|
|
69
|
+
let t = x;
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < 8; ++i) {
|
|
72
|
+
const error = sampleX(t) - x;
|
|
73
|
+
|
|
74
|
+
if (Math.abs(error) < 1e-6) {
|
|
75
|
+
return t;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const slope = slopeX(t);
|
|
79
|
+
|
|
80
|
+
if (Math.abs(slope) < 1e-6) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
t -= error / slope;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let low = 0;
|
|
88
|
+
let high = 1;
|
|
89
|
+
t = x;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < 20; ++i) {
|
|
92
|
+
const estimate = sampleX(t);
|
|
93
|
+
|
|
94
|
+
if (Math.abs(estimate - x) < 1e-6) {
|
|
95
|
+
return t;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (estimate < x) {
|
|
99
|
+
low = t;
|
|
100
|
+
} else {
|
|
101
|
+
high = t;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
t = (low + high) / 2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return t;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return progress => {
|
|
111
|
+
if (progress <= 0) {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (progress >= 1) {
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return sampleY(solveX(progress));
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Parse a cubic-bezier(...) string into a solver, or return null when the
|
|
124
|
+
// string is malformed so the caller can fall back to the default easing.
|
|
125
|
+
function parseCubicBezier(input: string): NumberFlowEasingFunction | null {
|
|
126
|
+
const match = CUBIC_BEZIER_PATTERN.exec(input.trim());
|
|
127
|
+
|
|
128
|
+
if (!match) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return cubicBezier(Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4]));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fallback for an unrecognised easing value, matching the --swift-out default.
|
|
136
|
+
const swiftOutEasing = cubicBezier(0.55, 0, 0.1, 1);
|
|
137
|
+
|
|
138
|
+
// Resolve the easing prop to a plain (t) => number function the tween can
|
|
139
|
+
// sample: functions pass through, keywords map to the table above and a
|
|
140
|
+
// cubic-bezier(...) string is parsed into a solver, falling back to swift-out.
|
|
141
|
+
const easingFunction = computed<NumberFlowEasingFunction>(() => {
|
|
142
|
+
if (typeof easing === 'function') {
|
|
143
|
+
return easing;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const keyword = EASINGS[easing as NumberFlowEasingKeyword];
|
|
147
|
+
|
|
148
|
+
if (keyword) {
|
|
149
|
+
return keyword;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return parseCubicBezier(easing) ?? swiftOutEasing;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Default to whole numbers so a mid-tween value never sprouts stray decimals.
|
|
156
|
+
// Currency, percentage and fractional displays opt in through `format`.
|
|
157
|
+
const formatter = computed(() => new Intl.NumberFormat(locale, format ?? {maximumFractionDigits: 0}));
|
|
158
|
+
|
|
159
|
+
// Rendered once for SSR / first paint only. The engine owns the span's text
|
|
160
|
+
// after mount, so this must NOT be reactive - a reactive {{ value }} would make
|
|
161
|
+
// Vue re-patch the text node every frame and wipe the tween. The accessible
|
|
162
|
+
// name stays current through the reactive :aria-label binding.
|
|
163
|
+
const initialText = formatter.value.format(animateOnMount ? 0 : value);
|
|
164
|
+
|
|
165
|
+
// Screen readers always read the final, settled value rather than the
|
|
166
|
+
// intermediate frames streaming past on screen.
|
|
167
|
+
const accessibleValue = computed(() => formatter.value.format(value));
|
|
168
|
+
|
|
169
|
+
const labelRef = useTemplateRef('label');
|
|
170
|
+
|
|
171
|
+
let frame = 0;
|
|
172
|
+
let current = animateOnMount ? 0 : value;
|
|
173
|
+
|
|
174
|
+
function render(next: number): void {
|
|
175
|
+
current = next;
|
|
176
|
+
|
|
177
|
+
const element = labelRef.value;
|
|
178
|
+
|
|
179
|
+
if (element) {
|
|
180
|
+
element.textContent = formatter.value.format(next);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cancel(): void {
|
|
185
|
+
if (frame) {
|
|
186
|
+
cancelAnimationFrame(frame);
|
|
187
|
+
frame = 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function tween(from: number, to: number): void {
|
|
192
|
+
cancel();
|
|
193
|
+
|
|
194
|
+
// Reduced motion, no distance or no duration: snap straight to the value.
|
|
195
|
+
if (from === to || duration <= 0 || prefersReducedMotion()) {
|
|
196
|
+
render(to);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const ease = easingFunction.value;
|
|
201
|
+
const delta = to - from;
|
|
202
|
+
const startTime = performance.now();
|
|
203
|
+
|
|
204
|
+
const step = (now: number): void => {
|
|
205
|
+
const progress = Math.min(1, (now - startTime) / duration);
|
|
206
|
+
render(from + delta * ease(progress));
|
|
207
|
+
|
|
208
|
+
if (progress < 1) {
|
|
209
|
+
frame = requestAnimationFrame(step);
|
|
210
|
+
} else {
|
|
211
|
+
frame = 0;
|
|
212
|
+
render(to);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
frame = requestAnimationFrame(step);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
onMounted(() => {
|
|
220
|
+
if (animateOnMount) {
|
|
221
|
+
tween(0, value);
|
|
222
|
+
} else {
|
|
223
|
+
render(value);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Tween from wherever the display currently sits, so a value that changes
|
|
228
|
+
// mid-tween keeps rolling smoothly instead of jumping.
|
|
229
|
+
watch(() => value, next => tween(current, next));
|
|
230
|
+
|
|
231
|
+
// Re-render in place when the locale or format changes.
|
|
232
|
+
watch(formatter, () => render(current));
|
|
233
|
+
|
|
234
|
+
onBeforeUnmount(cancel);
|
|
235
|
+
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
aria-hidden="true"
|
|
4
|
+
:class="$style.ping"
|
|
5
|
+
:style="{
|
|
6
|
+
'--ping-color': `var(--${color}-500)`,
|
|
7
|
+
'--ping-size': `${size}px`,
|
|
8
|
+
'--ping-duration': duration
|
|
9
|
+
}"/>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script
|
|
13
|
+
lang="ts"
|
|
14
|
+
setup>
|
|
15
|
+
import $style from '~flux/visuals/css/component/Ping.module.scss';
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
color = 'success',
|
|
19
|
+
size = 9,
|
|
20
|
+
duration = 1.4
|
|
21
|
+
} = defineProps<{
|
|
22
|
+
readonly color?: 'gray' | 'primary' | 'danger' | 'info' | 'success' | 'warning';
|
|
23
|
+
readonly size?: number;
|
|
24
|
+
readonly duration?: number;
|
|
25
|
+
}>();
|
|
26
|
+
</script>
|