@riverflowpkg/riverflow 1.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.
@@ -0,0 +1,309 @@
1
+ (function () {
2
+
3
+ /* ── Styles ─────────────────────────────────────────────────── */
4
+ function injectStyles() {
5
+ if (document.getElementById('terminal-styles')) return;
6
+ const s = document.createElement('style');
7
+ s.id = 'terminal-styles';
8
+ s.textContent = `
9
+ .terminal {
10
+ display: flex;
11
+ flex-direction: column;
12
+ font-family: 'Consolas', 'Fira Code', 'Menlo', monospace;
13
+ font-size: 13.5px;
14
+ line-height: 1.65;
15
+ background: #0d0d0d;
16
+ color: #e2e8f0;
17
+ border: 0.5px solid #2a2a2a;
18
+ border-radius: 10px;
19
+ overflow: hidden;
20
+ position: relative;
21
+ }
22
+
23
+ /* ── Header ── */
24
+ .t-header {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 8px;
28
+ padding: 8px 14px;
29
+ background: #161616;
30
+ border-bottom: 0.5px solid #222;
31
+ flex-shrink: 0;
32
+ }
33
+ .t-dot { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }
34
+ .t-dot-r { background: #ff5f57; }
35
+ .t-dot-y { background: #febc2e; }
36
+ .t-dot-g { background: #28c840; }
37
+ .t-title {
38
+ margin-left: 8px;
39
+ font-size: 12px;
40
+ color: #4a5568;
41
+ letter-spacing: 0.04em;
42
+ flex: 1;
43
+ text-align: center;
44
+ margin-right: 30px; /* optical center past the dots */
45
+ }
46
+
47
+ /* ── Body ── */
48
+ .t-body {
49
+ flex: 1;
50
+ overflow-y: auto;
51
+ padding: 14px 16px;
52
+ min-height: 120px;
53
+ }
54
+
55
+ .t-body::-webkit-scrollbar { width: 4px; }
56
+ .t-body::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 4px; }
57
+
58
+ /* ── Lines ── */
59
+ .t-line {
60
+ display: flex;
61
+ gap: 8px;
62
+ white-space: pre-wrap;
63
+ word-break: break-all;
64
+ }
65
+
66
+ .t-line-output { padding-left: 4px; }
67
+
68
+ /* Output types */
69
+ .t-info { color: #e2e8f0; }
70
+ .t-success { color: #c3e88d; }
71
+ .t-error { color: #f07178; }
72
+ .t-warn { color: #ffcb6b; }
73
+ .t-system { color: #4a5568; font-style: italic; }
74
+ .t-cmd { color: #82aaff; }
75
+
76
+ /* Prompt */
77
+ .t-prompt { color: #c792ea; user-select: none; flex-shrink: 0; }
78
+
79
+ /* ── Input row (interactive mode) ── */
80
+ .t-input-row {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 8px;
84
+ padding: 0 16px 14px;
85
+ flex-shrink: 0;
86
+ }
87
+
88
+ .t-input-prompt { color: #c792ea; user-select: none; flex-shrink: 0; }
89
+
90
+ .t-input {
91
+ flex: 1;
92
+ background: transparent;
93
+ border: none;
94
+ outline: none;
95
+ color: #82aaff;
96
+ font-family: inherit;
97
+ font-size: inherit;
98
+ line-height: inherit;
99
+ caret-color: #c792ea;
100
+ }
101
+
102
+ /* motion mode — animated caret */
103
+ .terminal-motion .t-input { caret-color: transparent; }
104
+ .terminal-motion .t-fake-caret {
105
+ display: inline-block;
106
+ width: 2px;
107
+ height: 1em;
108
+ background: #c792ea;
109
+ border-radius: 1px;
110
+ vertical-align: text-bottom;
111
+ margin-left: 1px;
112
+ animation: t-blink 1s steps(1) infinite;
113
+ }
114
+ @keyframes t-blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
115
+
116
+ /* motion — new output lines fade in */
117
+ .terminal-motion .t-line {
118
+ animation: t-fadeIn 0.18s ease both;
119
+ }
120
+ @keyframes t-fadeIn { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:translateY(0)} }
121
+ `;
122
+ document.head.appendChild(s);
123
+ }
124
+
125
+ /* ── Token colours for command echo ─────────────────────────── */
126
+ function colorizeCommand(text) {
127
+ return text
128
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
129
+ .replace(/^(\S+)/, '<span style="color:#c3e88d">$1</span>') /* command */
130
+ .replace(/ (-{1,2}[\w-]+)/g,' <span style="color:#ffcb6b">$1</span>') /* flags */
131
+ .replace(/ ("([^"]*)")/g,' <span style="color:#f78c6c">$1</span>'); /* strings */
132
+ }
133
+
134
+ /* ── Build terminal ──────────────────────────────────────────── */
135
+ function initTerminal(el) {
136
+ if (el.dataset.termInit) return;
137
+ el.dataset.termInit = '1';
138
+
139
+ const isInteractive = el.classList.contains('terminal-interactive');
140
+ const isMotion = el.classList.contains('terminal-motion');
141
+ const prompt = el.dataset.prompt || '~$';
142
+ const title = el.dataset.title || 'bash';
143
+
144
+ /* parse initial lines from innerHTML */
145
+ const rawLines = el.innerHTML
146
+ .split('\n')
147
+ .map(l => l.trim())
148
+ .filter(l => l.length);
149
+
150
+ el.innerHTML = '';
151
+
152
+ /* header */
153
+ const header = document.createElement('div');
154
+ header.className = 't-header';
155
+ header.innerHTML = `
156
+ <span class="t-dot t-dot-r"></span>
157
+ <span class="t-dot t-dot-y"></span>
158
+ <span class="t-dot t-dot-g"></span>
159
+ <span class="t-title">${title}</span>`;
160
+ el.appendChild(header);
161
+
162
+ /* body */
163
+ const body = document.createElement('div');
164
+ body.className = 't-body';
165
+ el.appendChild(body);
166
+
167
+ /* ── Render a line ── */
168
+ /*
169
+ Line syntax for the developer:
170
+ $ command args → shown with prompt + blue text
171
+ > output text → plain output (info)
172
+ + success message → green
173
+ ! error message → red
174
+ ? warn message → yellow
175
+ # system / comment → muted italic
176
+ (anything else) → plain info
177
+ */
178
+ function parseLine(raw) {
179
+ const first = raw[0];
180
+ const rest = raw.slice(1).trimStart();
181
+ if (first === '$') return { type: 'cmd', text: rest };
182
+ if (first === '>') return { type: 'info', text: rest };
183
+ if (first === '+') return { type: 'success', text: rest };
184
+ if (first === '!') return { type: 'error', text: rest };
185
+ if (first === '?') return { type: 'warn', text: rest };
186
+ if (first === '#') return { type: 'system', text: rest };
187
+ return { type: 'info', text: raw };
188
+ }
189
+
190
+ function appendLine(raw) {
191
+ const { type, text } = parseLine(raw);
192
+ const row = document.createElement('div');
193
+ row.className = 't-line';
194
+
195
+ if (type === 'cmd') {
196
+ row.innerHTML = `<span class="t-prompt">${prompt}</span><span class="t-cmd">${colorizeCommand(text)}</span>`;
197
+ } else {
198
+ row.className += ' t-line-output';
199
+ row.innerHTML = `<span class="t-${type}">${text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}</span>`;
200
+ }
201
+
202
+ body.appendChild(row);
203
+ body.scrollTop = body.scrollHeight;
204
+ }
205
+
206
+ /* render preset lines */
207
+ rawLines.forEach(l => appendLine(l));
208
+
209
+ /* ── Interactive mode ── */
210
+ if (isInteractive) {
211
+ const inputRow = document.createElement('div');
212
+ inputRow.className = 't-input-row';
213
+
214
+ const inputPrompt = document.createElement('span');
215
+ inputPrompt.className = 't-input-prompt';
216
+ inputPrompt.textContent = prompt;
217
+
218
+ const input = document.createElement('input');
219
+ input.className = 't-input';
220
+ input.type = 'text';
221
+ input.autocomplete = 'off';
222
+ input.spellcheck = false;
223
+ input.setAttribute('autocorrect', 'off');
224
+ input.setAttribute('autocapitalize', 'off');
225
+ input.placeholder = 'type a command...';
226
+
227
+ inputRow.appendChild(inputPrompt);
228
+ inputRow.appendChild(input);
229
+
230
+ /* fake caret for motion mode */
231
+ if (isMotion) {
232
+ const fakeCaret = document.createElement('span');
233
+ fakeCaret.className = 't-fake-caret';
234
+ inputRow.appendChild(fakeCaret);
235
+ }
236
+
237
+ el.appendChild(inputRow);
238
+
239
+ /* command history */
240
+ const history = [];
241
+ let histIdx = -1;
242
+
243
+ /* built-in commands */
244
+ const builtins = {
245
+ clear() { body.innerHTML = ''; },
246
+ help() {
247
+ ['+ Available commands:', '> clear — clears the terminal', '> help — shows this message'].forEach(appendLine);
248
+ },
249
+ };
250
+
251
+ /* custom commands — developer sets window.TerminalCommands */
252
+ function runCommand(cmd) {
253
+ const trimmed = cmd.trim();
254
+ if (!trimmed) return;
255
+
256
+ /* echo the command */
257
+ appendLine(`$ ${trimmed}`);
258
+
259
+ history.unshift(trimmed);
260
+ histIdx = -1;
261
+
262
+ const [name, ...args] = trimmed.split(/\s+/);
263
+
264
+ if (builtins[name]) {
265
+ builtins[name](args);
266
+ } else if (window.TerminalCommands && window.TerminalCommands[name]) {
267
+ const result = window.TerminalCommands[name](args, appendLine);
268
+ if (typeof result === 'string') appendLine(`> ${result}`);
269
+ } else {
270
+ appendLine(`! command not found: ${name}`);
271
+ }
272
+ }
273
+
274
+ input.addEventListener('keydown', e => {
275
+ if (e.key === 'Enter') {
276
+ runCommand(input.value);
277
+ input.value = '';
278
+ } else if (e.key === 'ArrowUp') {
279
+ e.preventDefault();
280
+ histIdx = Math.min(histIdx + 1, history.length - 1);
281
+ input.value = history[histIdx] || '';
282
+ } else if (e.key === 'ArrowDown') {
283
+ e.preventDefault();
284
+ histIdx = Math.max(histIdx - 1, -1);
285
+ input.value = histIdx === -1 ? '' : history[histIdx];
286
+ }
287
+ });
288
+
289
+ /* click anywhere on terminal focuses input */
290
+ el.addEventListener('click', () => input.focus());
291
+ }
292
+ }
293
+
294
+ /* ── Boot ────────────────────────────────────────────────────── */
295
+ function init() {
296
+ injectStyles();
297
+ document.querySelectorAll('.terminal').forEach(initTerminal);
298
+ new MutationObserver(muts => muts.forEach(m =>
299
+ m.addedNodes.forEach(n => {
300
+ if (n.nodeType !== 1) return;
301
+ if (n.classList.contains('terminal')) initTerminal(n);
302
+ n.querySelectorAll?.('.terminal').forEach(initTerminal);
303
+ })
304
+ )).observe(document.body, { childList: true, subtree: true });
305
+ }
306
+
307
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
308
+ else init();
309
+ })();
@@ -0,0 +1,105 @@
1
+ (function () {
2
+
3
+ function injectStyles() {
4
+ if (document.getElementById('typing-styles')) return;
5
+ const s = document.createElement('style');
6
+ s.id = 'typing-styles';
7
+ s.textContent = `
8
+ /* cursor */
9
+ .typing-cursor {
10
+ display: inline-block;
11
+ width: 2px;
12
+ height: 1em;
13
+ background: currentColor;
14
+ border-radius: 1px;
15
+ margin-left: 2px;
16
+ vertical-align: text-bottom;
17
+ }
18
+
19
+ /* default cursor — hard blink */
20
+ .typing .typing-cursor {
21
+ animation: cur-blink 1s steps(1) infinite;
22
+ }
23
+ @keyframes cur-blink {
24
+ 0%, 49% { opacity: 1; }
25
+ 50%, 100% { opacity: 0; }
26
+ }
27
+
28
+ /* smooth cursor — fade blink */
29
+ .typing-smooth .typing-cursor {
30
+ animation: cur-fade 1s ease-in-out infinite;
31
+ }
32
+ @keyframes cur-fade {
33
+ 0%, 100% { opacity: 1; }
34
+ 50% { opacity: 0; }
35
+ }
36
+
37
+ /* smooth char fade-in */
38
+ .typing-smooth .typing-char {
39
+ display: inline-block;
40
+ animation: char-fade 0.12s ease both;
41
+ }
42
+ @keyframes char-fade {
43
+ from { opacity: 0; transform: translateY(3px); }
44
+ to { opacity: 1; transform: translateY(0); }
45
+ }
46
+ `;
47
+ document.head.appendChild(s);
48
+ }
49
+
50
+ function initTyping(el) {
51
+ if (el.dataset.typingInit) return;
52
+ el.dataset.typingInit = '1';
53
+
54
+ const fullText = el.textContent.trim();
55
+ const isSmooth = el.classList.contains('typing-smooth');
56
+ const speed = parseInt(el.dataset.speed) || 50; /* ms per character */
57
+ const delay = parseInt(el.dataset.delay) || 0; /* ms before starting */
58
+
59
+ el.textContent = '';
60
+
61
+ /* cursor element */
62
+ const cursor = document.createElement('span');
63
+ cursor.className = 'typing-cursor';
64
+ el.appendChild(cursor);
65
+
66
+ let i = 0;
67
+
68
+ function typeNext() {
69
+ if (i >= fullText.length) return; /* done — cursor stays */
70
+
71
+ const ch = fullText[i];
72
+ i++;
73
+
74
+ if (isSmooth) {
75
+ const span = document.createElement('span');
76
+ span.className = 'typing-char';
77
+ span.textContent = ch;
78
+ el.insertBefore(span, cursor);
79
+ } else {
80
+ const text = document.createTextNode(ch);
81
+ el.insertBefore(text, cursor);
82
+ }
83
+
84
+ setTimeout(typeNext, speed);
85
+ }
86
+
87
+ setTimeout(typeNext, delay);
88
+ }
89
+
90
+ function init() {
91
+ injectStyles();
92
+ document.querySelectorAll('.typing').forEach(initTyping);
93
+ new MutationObserver(muts => muts.forEach(m =>
94
+ m.addedNodes.forEach(n => {
95
+ if (n.nodeType !== 1) return;
96
+ if (n.classList.contains('typing')) initTyping(n);
97
+ n.querySelectorAll?.('.typing').forEach(initTyping);
98
+ })
99
+ )).observe(document.body, { childList: true, subtree: true });
100
+ }
101
+
102
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
103
+ else init();
104
+
105
+ })();
@@ -0,0 +1,117 @@
1
+ (function () {
2
+ function injectStyles() {
3
+ if (document.getElementById('underwater-styles')) return;
4
+ const s = document.createElement('style');
5
+ s.id = 'underwater-styles';
6
+ s.textContent = `
7
+ #effect-underwater {
8
+ position: fixed;
9
+ inset: 0;
10
+ pointer-events: none;
11
+ z-index: 9998;
12
+ background: linear-gradient(180deg,
13
+ rgba(0,60,120,0.38) 0%,
14
+ rgba(0,100,160,0.22) 50%,
15
+ rgba(0,40,80,0.42) 100%);
16
+ }
17
+ #effect-underwater-caustics {
18
+ position: fixed;
19
+ inset: 0;
20
+ pointer-events: none;
21
+ z-index: 9997;
22
+ overflow: hidden;
23
+ }
24
+ #effect-underwater-caustics canvas {
25
+ position: absolute;
26
+ inset: 0;
27
+ width: 100%;
28
+ height: 100%;
29
+ opacity: 0.13;
30
+ }
31
+ #effect-underwater-blur {
32
+ position: fixed;
33
+ inset: 0;
34
+ pointer-events: none;
35
+ z-index: 9999;
36
+ backdrop-filter: blur(0.6px) saturate(1.3);
37
+ -webkit-backdrop-filter: blur(0.6px) saturate(1.3);
38
+ }
39
+ `;
40
+ document.head.appendChild(s);
41
+ }
42
+
43
+ function init() {
44
+ if (!document.querySelector('.effect-underwater')) return;
45
+ injectStyles();
46
+
47
+ const overlay = document.createElement('div');
48
+ overlay.id = 'effect-underwater';
49
+ document.body.appendChild(overlay);
50
+
51
+ const blurLayer = document.createElement('div');
52
+ blurLayer.id = 'effect-underwater-blur';
53
+ document.body.appendChild(blurLayer);
54
+
55
+ const causticWrap = document.createElement('div');
56
+ causticWrap.id = 'effect-underwater-caustics';
57
+ const canvas = document.createElement('canvas');
58
+ causticWrap.appendChild(canvas);
59
+ document.body.appendChild(causticWrap);
60
+
61
+ const ctx = canvas.getContext('2d');
62
+ let W, H;
63
+ function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
64
+ resize();
65
+ window.addEventListener('resize', resize);
66
+
67
+ const nodes = Array.from({ length: 10 }, () => ({
68
+ x: Math.random() * 1.2 - 0.1, y: Math.random() * 1.2 - 0.1,
69
+ vx: (Math.random() - 0.5) * 0.0006, vy: (Math.random() - 0.5) * 0.0006,
70
+ r: 0.12 + Math.random() * 0.2,
71
+ }));
72
+
73
+ let scrollOffset = 0;
74
+ window.addEventListener('scroll', () => { scrollOffset = window.scrollY; }, { passive: true });
75
+
76
+ function draw() {
77
+ ctx.clearRect(0, 0, W, H);
78
+ const shift = scrollOffset * 0.3;
79
+ nodes.forEach(n => {
80
+ n.x += n.vx; n.y += n.vy;
81
+ if (n.x < -0.2 || n.x > 1.2) n.vx *= -1;
82
+ if (n.y < -0.2 || n.y > 1.2) n.vy *= -1;
83
+ const cx = n.x * W, cy = n.y * H - shift;
84
+ const rx = n.r * W, ry = n.r * H * 0.5;
85
+ const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(rx, ry));
86
+ grad.addColorStop(0, 'rgba(100,200,255,0.55)');
87
+ grad.addColorStop(0.4, 'rgba(60,160,220,0.2)');
88
+ grad.addColorStop(1, 'rgba(0,80,160,0)');
89
+ ctx.save();
90
+ ctx.scale(1, ry / rx);
91
+ ctx.beginPath();
92
+ ctx.arc(cx, cy * (rx / ry), rx, 0, Math.PI * 2);
93
+ ctx.fillStyle = grad;
94
+ ctx.fill();
95
+ ctx.restore();
96
+ });
97
+ requestAnimationFrame(draw);
98
+ }
99
+
100
+ /* SVG wave distortion */
101
+ const svgFilter = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
102
+ svgFilter.style.cssText = 'position:fixed;width:0;height:0;';
103
+ svgFilter.innerHTML = `<defs><filter id="underwater-wave">
104
+ <feTurbulence type="fractalNoise" baseFrequency="0.012 0.018" numOctaves="3" seed="5" result="noise">
105
+ <animate attributeName="baseFrequency" values="0.012 0.018;0.014 0.020;0.012 0.018" dur="8s" repeatCount="indefinite"/>
106
+ </feTurbulence>
107
+ <feDisplacementMap in="SourceGraphic" in2="noise" scale="6" xChannelSelector="R" yChannelSelector="G"/>
108
+ </filter></defs>`;
109
+ document.body.appendChild(svgFilter);
110
+ overlay.style.filter = 'url(#underwater-wave)';
111
+
112
+ requestAnimationFrame(draw);
113
+ }
114
+
115
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
116
+ else init();
117
+ })();
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@riverflowpkg/riverflow",
3
+ "version": "1.0.0",
4
+ "description": "Drop-in vanilla JS browser widgets: top loading bar, smooth scroll, typing effect, underwater overlay, and a code editor with syntax highlighting.",
5
+ "keywords": [
6
+ "loading-bar",
7
+ "smooth-scroll",
8
+ "typing-effect",
9
+ "code-editor",
10
+ "vanilla-js",
11
+ "browser-widget",
12
+ "ui"
13
+ ],
14
+ "author": "RiverFlowPkg",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/RiverFlowPkg/RiverFlow.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/RiverFlowPkg/RiverFlow/issues"
22
+ },
23
+ "homepage": "https://github.com/RiverFlowPkg/RiverFlow#readme",
24
+ "files": [
25
+ "bar/",
26
+ "effects/",
27
+ "scroll/",
28
+ "editor/",
29
+ "README.md",
30
+ "LICENSE"
31
+ ]
32
+ }
@@ -0,0 +1,91 @@
1
+ html.has-scroll-smooth {
2
+ overflow: hidden
3
+ }
4
+
5
+ html.has-scroll-dragging {
6
+ -webkit-user-select: none;
7
+ -moz-user-select: none;
8
+ -ms-user-select: none;
9
+ user-select: none
10
+ }
11
+
12
+ .has-scroll-smooth body {
13
+ overflow: hidden
14
+ }
15
+
16
+ .has-scroll-smooth [data-scroll-container] {
17
+ min-height: 100vh
18
+ }
19
+
20
+ [data-scroll-direction=horizontal] [data-scroll-container] {
21
+ display: inline-block;
22
+ height: 100vh;
23
+ white-space: nowrap
24
+ }
25
+
26
+ [data-scroll-direction=horizontal] [data-scroll-section] {
27
+ display: inline-block;
28
+ height: 100%;
29
+ vertical-align: top;
30
+ white-space: nowrap
31
+ }
32
+
33
+ .c-scrollbar {
34
+ height: 100%;
35
+ opacity: 0;
36
+ position: absolute;
37
+ right: 0;
38
+ top: 0;
39
+ transform-origin: center right;
40
+ transition: transform .3s, opacity .3s;
41
+ width: 11px
42
+ }
43
+
44
+ .c-scrollbar:hover {
45
+ transform: scaleX(1.45)
46
+ }
47
+
48
+ .c-scrollbar:hover,
49
+ .has-scroll-dragging .c-scrollbar,
50
+ .has-scroll-scrolling .c-scrollbar {
51
+ opacity: 1
52
+ }
53
+
54
+ [data-scroll-direction=horizontal] .c-scrollbar {
55
+ bottom: 0;
56
+ height: 10px;
57
+ top: auto;
58
+ transform: scaleY(1);
59
+ width: 100%
60
+ }
61
+
62
+ [data-scroll-direction=horizontal] .c-scrollbar:hover {
63
+ transform: scaleY(1.3)
64
+ }
65
+
66
+ .c-scrollbar_thumb {
67
+ background-color: #000;
68
+ border-radius: 10px;
69
+ cursor: -webkit-grab;
70
+ cursor: grab;
71
+ margin: 2px;
72
+ opacity: .5;
73
+ position: absolute;
74
+ right: 0;
75
+ top: 0;
76
+ width: 7px
77
+ }
78
+
79
+ .has-scroll-dragging .c-scrollbar_thumb {
80
+ cursor: -webkit-grabbing;
81
+ cursor: grabbing
82
+ }
83
+
84
+ [data-scroll-direction=horizontal] .c-scrollbar_thumb {
85
+ bottom: 0;
86
+ right: auto
87
+ }
88
+
89
+ [data-scroll-sticky] {
90
+ will-change: transform;
91
+ }