@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.
Files changed (58) hide show
  1. package/README.md +32 -0
  2. package/dist/component/FluxVisualAnimatedColors.vue.d.ts +10 -0
  3. package/dist/component/FluxVisualAttention.vue.d.ts +40 -0
  4. package/dist/component/FluxVisualBorderBeam.vue.d.ts +31 -0
  5. package/dist/component/FluxVisualBorderShine.vue.d.ts +53 -0
  6. package/dist/component/FluxVisualDotPattern.vue.d.ts +11 -0
  7. package/dist/component/FluxVisualFlickeringGrid.vue.d.ts +10 -0
  8. package/dist/component/FluxVisualGridPattern.vue.d.ts +10 -0
  9. package/dist/component/FluxVisualHighlighter.vue.d.ts +36 -0
  10. package/dist/component/FluxVisualHighlighterGroup.vue.d.ts +13 -0
  11. package/dist/component/FluxVisualNoise.vue.d.ts +9 -0
  12. package/dist/component/FluxVisualNumberFlow.vue.d.ts +14 -0
  13. package/dist/component/FluxVisualPing.vue.d.ts +8 -0
  14. package/dist/component/FluxVisualSlotText.vue.d.ts +41 -0
  15. package/dist/component/FluxVisualTextScramble.vue.d.ts +20 -0
  16. package/dist/component/FluxVisualTextShimmer.vue.d.ts +18 -0
  17. package/dist/component/index.d.ts +15 -0
  18. package/dist/composable/private/index.d.ts +2 -0
  19. package/dist/composable/private/useBorderBeamPulse.d.ts +21 -0
  20. package/dist/composable/private/useHighlighterGroup.d.ts +51 -0
  21. package/dist/index.css +1110 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +12320 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/types.d.ts +13 -0
  26. package/package.json +71 -0
  27. package/src/component/FluxVisualAnimatedColors.vue +187 -0
  28. package/src/component/FluxVisualAttention.vue +86 -0
  29. package/src/component/FluxVisualBorderBeam.vue +171 -0
  30. package/src/component/FluxVisualBorderShine.vue +36 -0
  31. package/src/component/FluxVisualDotPattern.vue +119 -0
  32. package/src/component/FluxVisualFlickeringGrid.vue +159 -0
  33. package/src/component/FluxVisualGridPattern.vue +128 -0
  34. package/src/component/FluxVisualHighlighter.vue +217 -0
  35. package/src/component/FluxVisualHighlighterGroup.vue +20 -0
  36. package/src/component/FluxVisualNoise.vue +31 -0
  37. package/src/component/FluxVisualNumberFlow.vue +235 -0
  38. package/src/component/FluxVisualPing.vue +26 -0
  39. package/src/component/FluxVisualSlotText.vue +457 -0
  40. package/src/component/FluxVisualTextScramble.vue +168 -0
  41. package/src/component/FluxVisualTextShimmer.vue +34 -0
  42. package/src/component/index.ts +15 -0
  43. package/src/composable/private/index.ts +2 -0
  44. package/src/composable/private/useBorderBeamPulse.ts +203 -0
  45. package/src/composable/private/useHighlighterGroup.ts +162 -0
  46. package/src/css/component/Attention.module.scss +104 -0
  47. package/src/css/component/BorderBeam.module.scss +1178 -0
  48. package/src/css/component/Highlighter.module.scss +7 -0
  49. package/src/css/component/Noise.module.scss +80 -0
  50. package/src/css/component/NumberFlow.module.scss +5 -0
  51. package/src/css/component/PatternGlow.module.scss +19 -0
  52. package/src/css/component/Ping.module.scss +44 -0
  53. package/src/css/component/SlotText.module.scss +43 -0
  54. package/src/css/component/TextScramble.module.scss +5 -0
  55. package/src/css/component/TextShimmer.module.scss +54 -0
  56. package/src/css/component/Visual.module.scss +71 -0
  57. package/src/index.ts +1 -0
  58. 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>