@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,203 @@
|
|
|
1
|
+
import { prefersReducedMotion } from '@flux-ui/internals';
|
|
2
|
+
import { unref, watchEffect, type Ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
export type BorderBeamVariant = 'sm' | 'md' | 'line' | 'pulse-inner' | 'pulse-outside';
|
|
5
|
+
|
|
6
|
+
type PulseOscillator = {
|
|
7
|
+
readonly prop: string;
|
|
8
|
+
readonly a: number;
|
|
9
|
+
readonly b: number;
|
|
10
|
+
readonly delay: number;
|
|
11
|
+
readonly period: number;
|
|
12
|
+
readonly unit: '' | 'px';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type PulseConfig = {
|
|
16
|
+
readonly huePeriod: number | null;
|
|
17
|
+
readonly oscillators: PulseOscillator[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type PulseInstance = {
|
|
21
|
+
readonly config: PulseConfig;
|
|
22
|
+
readonly element: HTMLElement;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const FRAME_INTERVAL = 1000 / 30 - 2;
|
|
26
|
+
const TWO_PI = Math.PI * 2;
|
|
27
|
+
|
|
28
|
+
const instances = new Set<PulseInstance>();
|
|
29
|
+
let lastFrame = 0;
|
|
30
|
+
let rafId: number | null = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Cosine ease-in-out factor in [0, 1]: 0 at phase 0/1, 1 at phase 0.5.
|
|
34
|
+
*
|
|
35
|
+
* @param phase The current phase within the oscillation period.
|
|
36
|
+
*
|
|
37
|
+
* @author Bas Milius <bas@mili.us>
|
|
38
|
+
* @since 1.0.0
|
|
39
|
+
*/
|
|
40
|
+
function pingPong(phase: number): number {
|
|
41
|
+
return (1 - Math.cos(TWO_PI * phase)) / 2;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Single shared requestAnimationFrame loop, throttled to ~30fps, that drives the
|
|
46
|
+
* breathing motion of every registered pulse instance by writing CSS custom
|
|
47
|
+
* properties. The breathing is very slow (1.6–6.4s periods), so a capped JS loop
|
|
48
|
+
* repaints the gradient layers far less often than per-instance CSS keyframes
|
|
49
|
+
* running at the display refresh rate would.
|
|
50
|
+
*
|
|
51
|
+
* @param ts The current timestamp provided by requestAnimationFrame.
|
|
52
|
+
*
|
|
53
|
+
* @author Bas Milius <bas@mili.us>
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
*/
|
|
56
|
+
function frame(ts: number): void {
|
|
57
|
+
rafId = requestAnimationFrame(frame);
|
|
58
|
+
|
|
59
|
+
if (ts - lastFrame < FRAME_INTERVAL) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
lastFrame = ts;
|
|
64
|
+
|
|
65
|
+
const tSec = ts / 1000;
|
|
66
|
+
|
|
67
|
+
instances.forEach(({config, element}) => {
|
|
68
|
+
for (const osc of config.oscillators) {
|
|
69
|
+
const phase = (tSec - osc.delay) / osc.period;
|
|
70
|
+
const value = osc.a + (osc.b - osc.a) * pingPong(phase);
|
|
71
|
+
|
|
72
|
+
element.style.setProperty(osc.prop, osc.unit === 'px' ? `${value.toFixed(2)}px` : value.toFixed(4));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (config.huePeriod !== null) {
|
|
76
|
+
const value = ((tSec / config.huePeriod) % 1) * 360;
|
|
77
|
+
|
|
78
|
+
element.style.setProperty('--beam-hue', `${value.toFixed(2)}deg`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Registers an element to be driven by the shared pulse loop and returns a
|
|
85
|
+
* cleanup function that unregisters it again, stopping the loop once no
|
|
86
|
+
* instances remain.
|
|
87
|
+
*
|
|
88
|
+
* @param element The border beam wrapper element.
|
|
89
|
+
* @param config The oscillator configuration for the instance.
|
|
90
|
+
*
|
|
91
|
+
* @author Bas Milius <bas@mili.us>
|
|
92
|
+
* @since 1.0.0
|
|
93
|
+
*/
|
|
94
|
+
function registerPulseInstance(element: HTMLElement, config: PulseConfig): () => void {
|
|
95
|
+
const instance: PulseInstance = {config, element};
|
|
96
|
+
instances.add(instance);
|
|
97
|
+
|
|
98
|
+
if (rafId === null) {
|
|
99
|
+
lastFrame = 0;
|
|
100
|
+
rafId = requestAnimationFrame(frame);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
instances.delete(instance);
|
|
105
|
+
|
|
106
|
+
if (instances.size === 0 && rafId !== null) {
|
|
107
|
+
cancelAnimationFrame(rafId);
|
|
108
|
+
rafId = null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Builds the theme/variant/duration-tuned oscillator table for a pulse instance.
|
|
115
|
+
* Kept in sync with the gradient geometry in BorderBeam.module.scss.
|
|
116
|
+
*
|
|
117
|
+
* @param variant The pulse variant.
|
|
118
|
+
* @param isDark Whether the instance is rendered within a dark themed tree.
|
|
119
|
+
* @param duration The breathing duration in seconds.
|
|
120
|
+
* @param staticColors Whether the hue drift is disabled.
|
|
121
|
+
*
|
|
122
|
+
* @author Bas Milius <bas@mili.us>
|
|
123
|
+
* @since 1.0.0
|
|
124
|
+
*/
|
|
125
|
+
function createPulseConfig(variant: 'pulse-inner' | 'pulse-outside', isDark: boolean, duration: number, staticColors: boolean): PulseConfig {
|
|
126
|
+
const durScale = duration / 2.3;
|
|
127
|
+
const isInner = variant === 'pulse-inner';
|
|
128
|
+
|
|
129
|
+
const sp = isInner ? .28 : (isDark ? .28 : .36);
|
|
130
|
+
const dr = isInner ? (isDark ? 33 : 40) : (isDark ? 14 : 19);
|
|
131
|
+
const op = isInner ? (isDark ? .48 : .45) : (isDark ? .46 : 0);
|
|
132
|
+
const gh = isInner ? (isDark ? .34 : .22) : (isDark ? .16 : .58);
|
|
133
|
+
const bs = (isInner ? (isDark ? 1.9 : 2.6) : (isDark ? 2.3 : 3.7)) * durScale;
|
|
134
|
+
const ss = (isInner ? (isDark ? 2.6 : 4.6) : (isDark ? 6.4 : 4.6)) * durScale;
|
|
135
|
+
const ghs = (isInner ? (isDark ? 2.4 : 5.5) : (isDark ? 2.4 : 3.8)) * durScale;
|
|
136
|
+
const huePeriod = isInner ? 16 : 14;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
huePeriod: staticColors ? null : huePeriod,
|
|
140
|
+
oscillators: [
|
|
141
|
+
{prop: '--beam-bw1', a: 1 - sp, b: 1 + sp * 1.1, period: ss * .9, delay: 0, unit: ''},
|
|
142
|
+
{prop: '--beam-bh1', a: 1 + sp * .9, b: 1 - sp * .85, period: ss * 1.26, delay: 0, unit: ''},
|
|
143
|
+
{prop: '--beam-bx1', a: -dr, b: dr * .9, period: bs * 1.6, delay: 0, unit: 'px'},
|
|
144
|
+
{prop: '--beam-by1', a: dr * .55, b: -dr * .7, period: bs * 1.6, delay: 0, unit: 'px'},
|
|
145
|
+
{prop: '--beam-bw2', a: 1 + sp, b: 1 - sp * .85, period: ss * 1.1, delay: 0, unit: ''},
|
|
146
|
+
{prop: '--beam-bh2', a: 1 - sp * .8, b: 1 + sp * 1.05, period: ss * .81, delay: 0, unit: ''},
|
|
147
|
+
{prop: '--beam-bx2', a: dr * .8, b: -dr * .9, period: bs * 1.88, delay: 0, unit: 'px'},
|
|
148
|
+
{prop: '--beam-by2', a: -dr, b: dr * .65, period: bs * 1.88, delay: 0, unit: 'px'},
|
|
149
|
+
{prop: '--beam-bw3', a: 1 - sp * .6, b: 1 + sp * 1.15, period: ss * .98, delay: 0, unit: ''},
|
|
150
|
+
{prop: '--beam-bh3', a: 1 + sp * .75, b: 1 - sp, period: ss * 1.4, delay: 0, unit: ''},
|
|
151
|
+
{prop: '--beam-bx3', a: -dr * .6, b: dr, period: bs * 1.45, delay: 0, unit: 'px'},
|
|
152
|
+
{prop: '--beam-by3', a: -dr * .85, b: dr * .45, period: bs * 1.45, delay: 0, unit: 'px'},
|
|
153
|
+
{prop: '--beam-bgh', a: 1 - gh, b: 1 + gh, period: ghs, delay: 0, unit: ''},
|
|
154
|
+
{prop: '--beam-bop-tl', a: 1 - op, b: 1, period: bs, delay: 0, unit: ''},
|
|
155
|
+
{prop: '--beam-bop-tr', a: 1 - op, b: 1, period: bs * 1.32, delay: bs * .28, unit: ''},
|
|
156
|
+
{prop: '--beam-bop-bl', a: 1 - op, b: 1, period: bs * .84, delay: bs * .55, unit: ''},
|
|
157
|
+
{prop: '--beam-bop-br', a: 1 - op, b: 1, period: bs * 1.58, delay: bs * .83, unit: ''}
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type UseBorderBeamPulseOptions = {
|
|
163
|
+
readonly duration: Ref<number>;
|
|
164
|
+
readonly elementRef: Ref<HTMLElement | null>;
|
|
165
|
+
readonly enabled: Ref<boolean>;
|
|
166
|
+
readonly staticColors: Ref<boolean>;
|
|
167
|
+
readonly variant: Ref<BorderBeamVariant>;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Drives the breathing of a pulse border beam from the shared, frame-rate-capped
|
|
172
|
+
* animation loop while the instance is enabled. Respects prefers-reduced-motion
|
|
173
|
+
* and resolves the theme from the nearest `[dark]` ancestor.
|
|
174
|
+
*
|
|
175
|
+
* @param options The reactive instance options.
|
|
176
|
+
*
|
|
177
|
+
* @author Bas Milius <bas@mili.us>
|
|
178
|
+
* @since 1.0.0
|
|
179
|
+
*/
|
|
180
|
+
export default function useBorderBeamPulse(options: UseBorderBeamPulseOptions): void {
|
|
181
|
+
watchEffect(onCleanup => {
|
|
182
|
+
const variant = unref(options.variant);
|
|
183
|
+
|
|
184
|
+
if (variant !== 'pulse-inner' && variant !== 'pulse-outside') {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const element = unref(options.elementRef);
|
|
189
|
+
|
|
190
|
+
if (!element || !unref(options.enabled)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (prefersReducedMotion()) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const isDark = element.closest('[dark]') !== null;
|
|
199
|
+
const config = createPulseConfig(variant, isDark, unref(options.duration), unref(options.staticColors));
|
|
200
|
+
|
|
201
|
+
onCleanup(registerPulseInstance(element, config));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { annotationGroup } from 'rough-notation';
|
|
2
|
+
import { inject, onScopeDispose, provide, type InjectionKey } from 'vue';
|
|
3
|
+
|
|
4
|
+
type Annotation = Parameters<typeof annotationGroup>[0][number];
|
|
5
|
+
|
|
6
|
+
export type HighlighterVariant = 'highlight' | 'box' | 'circle' | 'underline' | 'strike-through' | 'crossed-off' | 'bracket';
|
|
7
|
+
|
|
8
|
+
export type HighlighterGroupProps = {
|
|
9
|
+
readonly variant?: HighlighterVariant;
|
|
10
|
+
readonly color?: string;
|
|
11
|
+
readonly strokeWidth?: number;
|
|
12
|
+
readonly animationDuration?: number;
|
|
13
|
+
readonly iterations?: number;
|
|
14
|
+
readonly padding?: number;
|
|
15
|
+
readonly multiline?: boolean;
|
|
16
|
+
readonly whenInView?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type HighlighterGroupEntry = {
|
|
20
|
+
readonly element: HTMLElement;
|
|
21
|
+
getAnnotation(): Annotation | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type HighlighterGroupContext = {
|
|
25
|
+
readonly defaults: HighlighterGroupProps;
|
|
26
|
+
add(entry: HighlighterGroupEntry): void;
|
|
27
|
+
remove(entry: HighlighterGroupEntry): void;
|
|
28
|
+
notify(): void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const FluxVisualHighlighterGroupInjectionKey: InjectionKey<HighlighterGroupContext> = Symbol('flux-visual-highlighter-group');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Injects the enclosing highlighter group, if any. A `FluxVisualHighlighter`
|
|
35
|
+
* that finds a group registers its annotation with it and lets the group drive
|
|
36
|
+
* the cascade instead of drawing itself.
|
|
37
|
+
*
|
|
38
|
+
* @author Bas Milius <bas@mili.us>
|
|
39
|
+
* @since 1.0.0
|
|
40
|
+
*/
|
|
41
|
+
export function useHighlighterGroupInjection(): HighlighterGroupContext | null {
|
|
42
|
+
return inject(FluxVisualHighlighterGroupInjectionKey, null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Collects the annotations of the descendant highlighters and reveals them as a
|
|
47
|
+
* single rough-notation group, so they draw one after another in document order
|
|
48
|
+
* rather than all at once. The draw is debounced so the initial burst of child
|
|
49
|
+
* registrations (and any surrounding chrome settling its layout) coalesces into
|
|
50
|
+
* one cascade, and — when `whenInView` is set — it waits until the first
|
|
51
|
+
* highlighter scrolls into view. rough-notation keeps the drawn annotations
|
|
52
|
+
* aligned on later resizes itself.
|
|
53
|
+
*
|
|
54
|
+
* The reactive props object is provided as `defaults` on the group context, so
|
|
55
|
+
* descendant highlighters can inherit annotation props they don't set themselves.
|
|
56
|
+
*
|
|
57
|
+
* @param props The reactive props of the group component.
|
|
58
|
+
*
|
|
59
|
+
* @author Bas Milius <bas@mili.us>
|
|
60
|
+
* @since 1.0.0
|
|
61
|
+
*/
|
|
62
|
+
export default function useHighlighterGroup(props: HighlighterGroupProps): void {
|
|
63
|
+
const whenInView = props.whenInView ?? false;
|
|
64
|
+
const entries = new Set<HighlighterGroupEntry>();
|
|
65
|
+
|
|
66
|
+
let group: ReturnType<typeof annotationGroup> | null = null;
|
|
67
|
+
let timer: number | undefined;
|
|
68
|
+
let settleObserver: ResizeObserver | null = null;
|
|
69
|
+
let inViewObserver: IntersectionObserver | null = null;
|
|
70
|
+
let inView = !whenInView;
|
|
71
|
+
|
|
72
|
+
function orderedAnnotations(): Annotation[] {
|
|
73
|
+
return [...entries]
|
|
74
|
+
.sort((a, b) => a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1)
|
|
75
|
+
.map(entry => entry.getAnnotation())
|
|
76
|
+
.filter((annotation): annotation is Annotation => annotation !== null);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function draw(): void {
|
|
80
|
+
if (!inView) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const annotations = orderedAnnotations();
|
|
85
|
+
|
|
86
|
+
if (annotations.length === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// annotationGroup writes cumulative animation delays onto the annotations,
|
|
91
|
+
// so a fresh group is rebuilt whenever the members change.
|
|
92
|
+
group?.hide();
|
|
93
|
+
group = annotationGroup(annotations);
|
|
94
|
+
group.show();
|
|
95
|
+
|
|
96
|
+
stopSettleWatch();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function schedule(): void {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
timer = setTimeout(() => draw(), 80);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stopSettleWatch(): void {
|
|
105
|
+
settleObserver?.disconnect();
|
|
106
|
+
settleObserver = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Draw the first cascade only once the surrounding layout has stopped moving,
|
|
110
|
+
// so the animation plays over the text instead of where it sat pre-layout.
|
|
111
|
+
function watchSettle(): void {
|
|
112
|
+
if (settleObserver || typeof ResizeObserver === 'undefined') {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
settleObserver = new ResizeObserver(() => schedule());
|
|
117
|
+
settleObserver.observe(document.body);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function watchInView(element: HTMLElement): void {
|
|
121
|
+
if (!whenInView || inViewObserver || typeof IntersectionObserver === 'undefined') {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
inViewObserver = new IntersectionObserver(observed => {
|
|
126
|
+
if (observed.some(entry => entry.isIntersecting)) {
|
|
127
|
+
inView = true;
|
|
128
|
+
inViewObserver?.disconnect();
|
|
129
|
+
inViewObserver = null;
|
|
130
|
+
schedule();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
inViewObserver.observe(element);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
provide(FluxVisualHighlighterGroupInjectionKey, {
|
|
138
|
+
defaults: props,
|
|
139
|
+
add(entry) {
|
|
140
|
+
entries.add(entry);
|
|
141
|
+
watchInView(entry.element);
|
|
142
|
+
watchSettle();
|
|
143
|
+
schedule();
|
|
144
|
+
},
|
|
145
|
+
remove(entry) {
|
|
146
|
+
entries.delete(entry);
|
|
147
|
+
schedule();
|
|
148
|
+
},
|
|
149
|
+
notify() {
|
|
150
|
+
schedule();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
onScopeDispose(() => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
stopSettleWatch();
|
|
157
|
+
inViewObserver?.disconnect();
|
|
158
|
+
inViewObserver = null;
|
|
159
|
+
group?.hide();
|
|
160
|
+
group = null;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
.pulse {
|
|
2
|
+
animation: visualAttentionPulse calc(var(--attention-duration) * 1ms) ease-in-out;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.shake {
|
|
6
|
+
animation: visualAttentionShake calc(var(--attention-duration) * 1ms) ease-in-out;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.bounce {
|
|
10
|
+
animation: visualAttentionBounce calc(var(--attention-duration) * 1ms) ease;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.tada {
|
|
14
|
+
animation: visualAttentionTada calc(var(--attention-duration) * 1ms) ease-in-out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media (prefers-reduced-motion: reduce) {
|
|
18
|
+
.pulse,
|
|
19
|
+
.shake,
|
|
20
|
+
.bounce,
|
|
21
|
+
.tada {
|
|
22
|
+
animation: none;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@keyframes visualAttentionPulse {
|
|
27
|
+
0% {
|
|
28
|
+
transform: scale(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
50% {
|
|
32
|
+
transform: scale(1.06);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
100% {
|
|
36
|
+
transform: scale(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@keyframes visualAttentionShake {
|
|
41
|
+
0%,
|
|
42
|
+
100% {
|
|
43
|
+
transform: translateX(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
20% {
|
|
47
|
+
transform: translateX(-6px);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
40% {
|
|
51
|
+
transform: translateX(6px);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
60% {
|
|
55
|
+
transform: translateX(-3px);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
80% {
|
|
59
|
+
transform: translateX(3px);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@keyframes visualAttentionBounce {
|
|
64
|
+
0%,
|
|
65
|
+
100% {
|
|
66
|
+
transform: translateY(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
30% {
|
|
70
|
+
transform: translateY(-9px);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
60% {
|
|
74
|
+
transform: translateY(-3px);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@keyframes visualAttentionTada {
|
|
79
|
+
0% {
|
|
80
|
+
transform: scale(1) rotate(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
10%,
|
|
84
|
+
20% {
|
|
85
|
+
transform: scale(.94) rotate(-3deg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
30%,
|
|
89
|
+
50%,
|
|
90
|
+
70%,
|
|
91
|
+
90% {
|
|
92
|
+
transform: scale(1.06) rotate(3deg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
40%,
|
|
96
|
+
60%,
|
|
97
|
+
80% {
|
|
98
|
+
transform: scale(1.06) rotate(-3deg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
100% {
|
|
102
|
+
transform: scale(1) rotate(0);
|
|
103
|
+
}
|
|
104
|
+
}
|