fancoolo-fx 1.5.0 → 1.6.1

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/.distignore CHANGED
@@ -1,15 +1,18 @@
1
- src/
2
- docs/
3
- skills/
4
1
  node_modules/
5
- package.json
6
- package-lock.json
7
- .npmignore
8
- .gitignore
2
+ src/
3
+
4
+ **/.DS_Store
5
+
9
6
  .distignore
7
+ .git/
10
8
  .github/
9
+ .gitignore
11
10
  .claude/
12
11
  .nojekyll
13
- .git/
12
+
13
+ docs/
14
+ skills/
15
+ package-lock.json
16
+ package.json
14
17
  CLAUDE.md
15
18
  README.md
package/inc/Admin.php ADDED
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace FancooloFX;
4
+
5
+ if ( ! defined( 'ABSPATH' ) ) {
6
+ exit;
7
+ }
8
+
9
+ class Admin {
10
+
11
+ public static function register_menu() {
12
+ add_theme_page(
13
+ 'Fancoolo FX',
14
+ 'Fancoolo FX',
15
+ 'edit_theme_options',
16
+ 'fancoolo-fx',
17
+ array( AdminPage::class, 'render' )
18
+ );
19
+ }
20
+
21
+ public static function enqueue( $hook ) {
22
+ if ( 'appearance_page_fancoolo-fx' !== $hook ) {
23
+ return;
24
+ }
25
+
26
+ wp_enqueue_style( 'fancoolo-fx-admin', FANCOOLO_FX_URL . 'assets/admin.css', array(), FANCOOLO_FX_VERSION );
27
+ wp_enqueue_script( 'fancoolo-fx-admin', FANCOOLO_FX_URL . 'assets/admin.js', array( 'jquery' ), FANCOOLO_FX_VERSION, true );
28
+
29
+ $settings = wp_enqueue_code_editor( array( 'type' => 'text/javascript' ) );
30
+
31
+ if ( false !== $settings ) {
32
+ wp_add_inline_script(
33
+ 'code-editor',
34
+ sprintf(
35
+ 'jQuery(function($) {
36
+ if ($("#fancoolo-fx-editor").length) {
37
+ var editor = wp.codeEditor.initialize($("#fancoolo-fx-editor"), %s);
38
+ editor.codemirror.on("change", function(cm) {
39
+ cm.refresh();
40
+ });
41
+ }
42
+ });',
43
+ wp_json_encode( $settings )
44
+ )
45
+ );
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,398 @@
1
+ <?php
2
+
3
+ namespace FancooloFX;
4
+
5
+ if ( ! defined( 'ABSPATH' ) ) {
6
+ exit;
7
+ }
8
+
9
+ class AdminPage {
10
+
11
+ public static function render() {
12
+ $custom_file = Settings::get_custom_file_path();
13
+ $content = file_exists( $custom_file ) ? file_get_contents( $custom_file ) : '';
14
+ $s = Settings::get();
15
+
16
+ settings_errors( 'fancoolo_fx' );
17
+ ?>
18
+ <!-- Styles and scripts enqueued via Admin::enqueue() -->
19
+
20
+ <div class="wrap">
21
+ <h1>Fancoolo FX</h1>
22
+ <p>
23
+ GSAP animation wrapper is active. Add <code>.fx-*</code> classes to blocks in Gutenberg
24
+ and they will animate automatically.
25
+ </p>
26
+
27
+
28
+ <!-- ── Tabs ── -->
29
+ <div class="ffx-tabs">
30
+ <button class="ffx-tab active" data-tab="editor">Editor</button>
31
+ <button class="ffx-tab" data-tab="config">Config Reference</button>
32
+ <button class="ffx-tab" data-tab="classes">Classes Reference</button>
33
+ </div>
34
+
35
+ <!-- ═══ Editor Tab ═══ -->
36
+ <div class="ffx-panel ffx-panel-editor active" data-panel="editor">
37
+ <form method="post">
38
+ <?php wp_nonce_field( 'fancoolo_fx_save_action', 'fancoolo_fx_nonce' ); ?>
39
+
40
+ <!-- Editor main area -->
41
+ <div class="ffx-editor-main">
42
+ <input type="submit" name="fancoolo_fx_save" class="button button-primary ffx-save-btn" value="Save Changes">
43
+ <textarea
44
+ id="fancoolo-fx-editor"
45
+ name="fancoolo_fx_code"
46
+ rows="20"
47
+ style="width: 100%; font-family: monospace;"
48
+ ><?php echo esc_textarea( $content ); ?></textarea>
49
+ </div>
50
+
51
+ <!-- Settings sidebar -->
52
+ <div class="ffx-sidebar">
53
+ <h3>Settings</h3>
54
+
55
+ <label for="ffx-scroll-start">Scroll Start</label>
56
+ <input type="text" id="ffx-scroll-start" name="fancoolo_fx_scroll_start" value="<?php echo esc_attr( $s['scroll_start'] ); ?>" placeholder="top 85%">
57
+ <p class="ffx-hint">When scroll animations trigger (e.g. top 85%, top center)</p>
58
+
59
+ <label for="ffx-section-selector">Section Selector</label>
60
+ <input type="text" id="ffx-section-selector" name="fancoolo_fx_section_selector" value="<?php echo esc_attr( $s['section_selector'] ); ?>" placeholder="section">
61
+ <p class="ffx-hint">Containers for bare-class triggering</p>
62
+
63
+ <label for="ffx-exclude-selectors">Exclude Selectors</label>
64
+ <input type="text" id="ffx-exclude-selectors" name="fancoolo_fx_exclude_selectors" value="<?php echo esc_attr( $s['exclude_selectors'] ); ?>" placeholder=".no-fx, .wp-block-navigation">
65
+ <p class="ffx-hint">Elements matching these selectors are never animated</p>
66
+
67
+ <hr>
68
+
69
+ <div class="ffx-toggle">
70
+ <input type="checkbox" id="ffx-scroll-once" name="fancoolo_fx_scroll_once" value="1" <?php checked( $s['scroll_once'], '1' ); ?>>
71
+ <label for="ffx-scroll-once">Play once</label>
72
+ </div>
73
+ <p class="ffx-hint">Uncheck to replay animations on every scroll</p>
74
+
75
+ <div class="ffx-toggle">
76
+ <input type="checkbox" id="ffx-debug-markers" name="fancoolo_fx_debug_markers" value="1" <?php checked( $s['debug_markers'], '1' ); ?>>
77
+ <label for="ffx-debug-markers">Debug markers</label>
78
+ </div>
79
+ <p class="ffx-hint">Show ScrollTrigger markers (admin only, visitors won't see them)</p>
80
+
81
+ <div class="ffx-toggle">
82
+ <input type="checkbox" id="ffx-disable-mobile" name="fancoolo_fx_disable_mobile" value="1" <?php checked( $s['disable_mobile'], '1' ); ?>>
83
+ <label for="ffx-disable-mobile">Disable on mobile</label>
84
+ </div>
85
+ <div class="ffx-mobile-breakpoint" style="<?php echo $s['disable_mobile'] === '1' ? '' : 'display:none;'; ?>">
86
+ <label for="ffx-mobile-breakpoint">Breakpoint (px)</label>
87
+ <input type="text" id="ffx-mobile-breakpoint" name="fancoolo_fx_mobile_breakpoint" value="<?php echo esc_attr( $s['mobile_breakpoint'] ); ?>" placeholder="768" style="width: 80px; margin-bottom: 12px;">
88
+ </div>
89
+
90
+ <div class="ffx-toggle">
91
+ <input type="checkbox" id="ffx-respect-reduced-motion" name="fancoolo_fx_respect_reduced_motion" value="1" <?php checked( $s['respect_reduced_motion'], '1' ); ?>>
92
+ <label for="ffx-respect-reduced-motion">Respect reduced motion</label>
93
+ </div>
94
+ <p class="ffx-hint">Skip animations when OS has reduced motion enabled</p>
95
+
96
+ <label for="ffx-speed-multiplier">Speed Multiplier</label>
97
+ <select id="ffx-speed-multiplier" name="fancoolo_fx_speed_multiplier" style="width: 100%; margin-bottom: 12px;">
98
+ <?php foreach ( array( '0.5' => '0.5x (faster)', '0.75' => '0.75x', '1' => '1x (default)', '1.25' => '1.25x', '1.5' => '1.5x', '2' => '2x (slower)' ) as $val => $lbl ) : ?>
99
+ <option value="<?php echo esc_attr( $val ); ?>" <?php selected( $s['speed_multiplier'], $val ); ?>><?php echo esc_html( $lbl ); ?></option>
100
+ <?php endforeach; ?>
101
+ </select>
102
+
103
+ <div class="ffx-toggle">
104
+ <input type="checkbox" id="ffx-gutenberg-panel" name="fancoolo_fx_gutenberg_panel" value="1" <?php checked( $s['gutenberg_panel'], '1' ); ?>>
105
+ <label for="ffx-gutenberg-panel">Gutenberg integration</label>
106
+ </div>
107
+ <p class="ffx-hint">Show FX Animation panel in the block editor sidebar</p>
108
+
109
+ <hr>
110
+
111
+ <h3>Global Tag Map</h3>
112
+ <p class="ffx-hint" style="margin-top: -8px;">Auto-animate elements by tag inside sections. No classes needed.</p>
113
+
114
+ <div id="ffx-tagmap-rows">
115
+ <?php foreach ( $s['tag_map'] as $i => $row ) : ?>
116
+ <div class="ffx-tagmap-row">
117
+ <input type="text" name="fancoolo_fx_tag_map[<?php echo $i; ?>][selector]" value="<?php echo esc_attr( $row['selector'] ); ?>" placeholder="h1,h2,h3">
118
+ <select name="fancoolo_fx_tag_map[<?php echo $i; ?>][effect]">
119
+ <?php foreach ( self::get_effects_list() as $value => $label ) : ?>
120
+ <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $row['effect'], $value ); ?>><?php echo esc_html( $label ); ?></option>
121
+ <?php endforeach; ?>
122
+ </select>
123
+ <button type="button" class="ffx-tagmap-remove" title="Remove">&times;</button>
124
+ </div>
125
+ <?php endforeach; ?>
126
+ </div>
127
+ <button type="button" id="ffx-tagmap-add" class="button button-small">+ Add Rule</button>
128
+
129
+ <hr>
130
+
131
+ <h3>Import / Export</h3>
132
+ <div class="ffx-import-export">
133
+ <button type="button" id="ffx-export" class="button button-small" style="width: 100%; margin-bottom: 6px;">Export Settings</button>
134
+ <button type="button" id="ffx-import-btn" class="button button-small" style="width: 100%; margin-bottom: 6px;">Import Settings</button>
135
+ <input type="file" id="ffx-import-file" accept=".json" style="display: none;">
136
+ <button type="button" id="ffx-reset" class="button button-small" style="width: 100%; color: #a00;">Reset to Defaults</button>
137
+ </div>
138
+ </div>
139
+ </form>
140
+ </div>
141
+
142
+ <!-- ═══ Config Reference Tab ═══ -->
143
+ <div class="ffx-panel" data-panel="config">
144
+
145
+ <h3>Config Options</h3>
146
+ <table class="widefat fixed striped" style="max-width: 700px;">
147
+ <thead>
148
+ <tr><th>Option</th><th>Default</th><th>Description</th></tr>
149
+ </thead>
150
+ <tbody>
151
+ <tr><td><code>FX.config.tagMap</code></td><td><code>null</code></td><td>Auto-animate elements by tag/selector — no classes needed</td></tr>
152
+ <tr><td><code>FX.config.sectionSelector</code></td><td><code>'section'</code></td><td>Containers for bare-class and tagMap triggering</td></tr>
153
+ <tr><td><code>FX.config.scrollStart</code></td><td><code>'top 85%'</code></td><td>When scroll animations trigger (GSAP ScrollTrigger start format)</td></tr>
154
+ <tr><td><code>FX.config.scrollOnce</code></td><td><code>true</code></td><td>Play once or replay on every scroll</td></tr>
155
+ <tr><td><code>FX.config.excludeSelectors</code></td><td><code>''</code></td><td>CSS selectors for elements to never animate</td></tr>
156
+ <tr><td><code>FX.config.disableMobile</code></td><td><code>false</code></td><td>Skip all animations on mobile</td></tr>
157
+ <tr><td><code>FX.config.mobileBreakpoint</code></td><td><code>768</code></td><td>Width threshold for mobile detection (px)</td></tr>
158
+ <tr><td><code>FX.config.speedMultiplier</code></td><td><code>1</code></td><td>Global duration multiplier (0.5 = faster, 2 = slower)</td></tr>
159
+ <tr><td><code>FX.config.respectReducedMotion</code></td><td><code>true</code></td><td>Skip animations when OS prefers reduced motion</td></tr>
160
+ </tbody>
161
+ </table>
162
+
163
+ <h3 style="margin-top: 24px;">JS API Functions</h3>
164
+ <table class="widefat fixed striped" style="max-width: 700px;">
165
+ <thead>
166
+ <tr><th>Function</th><th>Description</th></tr>
167
+ </thead>
168
+ <tbody>
169
+ <tr><td><code>FX.textReveal(el, opts)</code></td><td>Split text lines, masked reveal upward</td></tr>
170
+ <tr><td><code>FX.reveal(el, opts)</code></td><td>Slide up with fade</td></tr>
171
+ <tr><td><code>FX.spinReveal(el, opts)</code></td><td>Rotate and scale in</td></tr>
172
+ <tr><td><code>FX.bgReveal(el, opts)</code></td><td>Background slide up</td></tr>
173
+ <tr><td><code>FX.scaleIn(el, opts)</code></td><td>Scale up with fade</td></tr>
174
+ <tr><td><code>FX.fadeIn(el, opts)</code></td><td>Opacity + subtle scale, no movement</td></tr>
175
+ <tr><td><code>FX.blurIn(el, opts)</code></td><td>Fade in while deblurring</td></tr>
176
+ <tr><td><code>FX.clipUp(el, opts)</code></td><td>Clip-path wipe from bottom</td></tr>
177
+ <tr><td><code>FX.clipDown(el, opts)</code></td><td>Clip-path wipe from top</td></tr>
178
+ <tr><td><code>FX.tiltIn(el, opts)</code></td><td>3D perspective reveal (scrub-based)</td></tr>
179
+ <tr><td><code>FX.typeWriter(el, opts)</code></td><td>Character-by-character typing reveal</td></tr>
180
+ <tr><td><code>FX.drawSVG(el, opts)</code></td><td>SVG stroke drawing animation</td></tr>
181
+ <tr><td><code>FX.parallax(el, opts)</code></td><td>Scroll-linked Y parallax shift</td></tr>
182
+ <tr><td><code>FX.splitWords(el, opts)</code></td><td>Word-by-word fade and slide up</td></tr>
183
+ <tr><td><code>FX.slideIn(el, opts)</code></td><td>Horizontal slide from left or right</td></tr>
184
+ <tr><td><code>FX.init()</code></td><td>Re-scan DOM — call after changing any config</td></tr>
185
+ </tbody>
186
+ </table>
187
+
188
+ <h3 style="margin-top: 24px;">Examples <span style="font-weight:normal;color:#646970;font-size:13px;">(click code to copy)</span></h3>
189
+
190
+ <h4 style="margin-top: 16px;">Auto-animate by tag</h4>
191
+ <div class="ffx-copy-wrap">
192
+ <button class="ffx-copy-btn" data-target="ex1">Copy</button>
193
+ <pre class="ffx-pre" id="ex1">FX.config.tagMap = {
194
+ 'h1,h2,h3,h4,h5,h6': 'textReveal',
195
+ 'p,blockquote': 'textReveal',
196
+ 'img,video': 'reveal',
197
+ };
198
+ FX.config.sectionSelector = 'section, .wp-block-group';
199
+ FX.init();</pre>
200
+ </div>
201
+
202
+ <h4 style="margin-top: 16px;">Change scroll trigger position</h4>
203
+ <div class="ffx-copy-wrap">
204
+ <button class="ffx-copy-btn" data-target="ex2">Copy</button>
205
+ <pre class="ffx-pre" id="ex2">FX.config.scrollStart = 'top center';
206
+ FX.init();</pre>
207
+ </div>
208
+
209
+ <h4 style="margin-top: 16px;">Replay animations on re-scroll</h4>
210
+ <div class="ffx-copy-wrap">
211
+ <button class="ffx-copy-btn" data-target="ex3">Copy</button>
212
+ <pre class="ffx-pre" id="ex3">FX.config.scrollOnce = false;
213
+ FX.init();</pre>
214
+ </div>
215
+
216
+ <h4 style="margin-top: 16px;">Compound sequence</h4>
217
+ <div class="ffx-copy-wrap">
218
+ <button class="ffx-copy-btn" data-target="ex4">Copy</button>
219
+ <pre class="ffx-pre" id="ex4">document.addEventListener('DOMContentLoaded', function () {
220
+ var hero = document.querySelector('.wp-block-cover');
221
+ if (!hero) return;
222
+
223
+ FX.scaleIn(hero, {
224
+ trigger: 'scroll',
225
+ scrollTrigger: { trigger: hero }
226
+ });
227
+
228
+ var heading = hero.querySelector('h2');
229
+ if (heading) {
230
+ FX.textReveal(heading, {
231
+ trigger: 'scroll',
232
+ delay: 0.2,
233
+ scrollTrigger: { trigger: hero }
234
+ });
235
+ }
236
+ });</pre>
237
+ </div>
238
+ </div>
239
+
240
+ <!-- ═══ Classes Reference Tab ═══ -->
241
+ <div class="ffx-panel" data-panel="classes">
242
+
243
+ <p style="margin-bottom: 16px; color: #646970;">
244
+ Add these in Gutenberg: select a block &rarr; Advanced &rarr; Additional CSS class(es). Click any class to copy it.
245
+ </p>
246
+
247
+ <?php self::render_classes_reference(); ?>
248
+ </div>
249
+
250
+ <p style="margin-top: 24px; color: #646970;">
251
+ This code loads after fx.js on the frontend. Leave empty to use defaults only.<br>
252
+ <strong style="color:#1d2327;">Important:</strong> Always add <code>FX.init();</code> at the end when changing config — it re-scans the page with your new settings.
253
+ </p>
254
+ <p style="margin-top: 12px;">
255
+ <a href="https://krstivoja.github.io/fancoolo-fx/documentation/" target="_blank">
256
+ Full Documentation &rarr;
257
+ </a>
258
+ </p>
259
+ </div>
260
+ <?php
261
+ }
262
+
263
+ public static function get_effects_list() {
264
+ return array(
265
+ 'textReveal' => 'Text Reveal',
266
+ 'reveal' => 'Reveal',
267
+ 'spinReveal' => 'Spin Reveal',
268
+ 'bgReveal' => 'BG Reveal',
269
+ 'scaleIn' => 'Scale In',
270
+ 'fadeIn' => 'Fade In',
271
+ 'blurIn' => 'Blur In',
272
+ 'clipUp' => 'Clip Up',
273
+ 'clipDown' => 'Clip Down',
274
+ 'tiltIn' => 'Tilt In',
275
+ 'typeWriter' => 'Type Writer',
276
+ 'drawSVG' => 'Draw SVG',
277
+ 'parallax' => 'Parallax',
278
+ 'splitWords' => 'Split Words',
279
+ 'slideIn' => 'Slide In',
280
+ );
281
+ }
282
+
283
+ private static function render_classes_reference() {
284
+ $groups = array(
285
+ 'Text Reveal' => array(
286
+ 'fx-text-reveal-pl' => 'Page load — masked line-by-line reveal',
287
+ 'fx-text-reveal-st' => 'Scroll triggered',
288
+ 'fx-text-reveal' => 'Auto triggered inside a section',
289
+ ),
290
+ 'Reveal' => array(
291
+ 'fx-reveal-pl' => 'Page load — slide up with fade',
292
+ 'fx-reveal-st' => 'Scroll triggered',
293
+ 'fx-reveal' => 'Auto triggered inside a section',
294
+ ),
295
+ 'Spin Reveal' => array(
296
+ 'fx-spin-reveal-pl' => 'Page load — rotate and scale in',
297
+ 'fx-spin-reveal-st' => 'Scroll triggered',
298
+ 'fx-spin-reveal' => 'Auto triggered inside a section',
299
+ ),
300
+ 'BG Reveal' => array(
301
+ 'fx-bg-reveal-pl' => 'Page load — background slide up',
302
+ 'fx-bg-reveal-st' => 'Scroll triggered',
303
+ 'fx-bg-reveal' => 'Auto triggered inside a section',
304
+ ),
305
+ 'Scale In' => array(
306
+ 'fx-scale-in-pl' => 'Page load — scale up with fade',
307
+ 'fx-scale-in-st' => 'Scroll triggered',
308
+ 'fx-scale-in' => 'Auto triggered inside a section',
309
+ ),
310
+ 'Fade In' => array(
311
+ 'fx-fade-in-pl' => 'Page load — opacity only, no movement',
312
+ 'fx-fade-in-st' => 'Scroll triggered',
313
+ 'fx-fade-in' => 'Auto triggered inside a section',
314
+ ),
315
+ 'Blur In' => array(
316
+ 'fx-blur-in-pl' => 'Page load — fade in while deblurring',
317
+ 'fx-blur-in-st' => 'Scroll triggered',
318
+ 'fx-blur-in' => 'Auto triggered inside a section',
319
+ ),
320
+ 'Clip Reveal' => array(
321
+ 'fx-clip-up-pl' => 'Page load — clip-path wipe from bottom',
322
+ 'fx-clip-up-st' => 'Scroll triggered',
323
+ 'fx-clip-up' => 'Auto triggered inside a section',
324
+ 'fx-clip-down-pl' => 'Page load — clip-path wipe from top',
325
+ 'fx-clip-down-st' => 'Scroll triggered',
326
+ 'fx-clip-down' => 'Auto triggered inside a section',
327
+ ),
328
+ 'Type Writer' => array(
329
+ 'fx-type-writer-pl' => 'Page load — character-by-character typing',
330
+ 'fx-type-writer-st' => 'Scroll triggered',
331
+ 'fx-type-writer' => 'Auto triggered inside a section',
332
+ ),
333
+ 'Draw SVG' => array(
334
+ 'fx-draw-svg-pl' => 'Page load — SVG stroke drawing animation',
335
+ 'fx-draw-svg-st' => 'Scroll triggered',
336
+ 'fx-draw-svg' => 'Auto triggered inside a section',
337
+ 'fx-draw-svg-scrub' => 'Draws progressively as you scroll (scrub-based)',
338
+ ),
339
+ 'Split Words' => array(
340
+ 'fx-split-words-pl' => 'Page load — word-by-word fade and slide',
341
+ 'fx-split-words-st' => 'Scroll triggered',
342
+ 'fx-split-words' => 'Auto triggered inside a section',
343
+ ),
344
+ 'Slide In' => array(
345
+ 'fx-slide-left-pl' => 'Page load — slide in from left',
346
+ 'fx-slide-left-st' => 'Scroll triggered — from left',
347
+ 'fx-slide-left' => 'Auto triggered inside a section — from left',
348
+ 'fx-slide-right-pl' => 'Page load — slide in from right',
349
+ 'fx-slide-right-st' => 'Scroll triggered — from right',
350
+ 'fx-slide-right' => 'Auto triggered inside a section — from right',
351
+ ),
352
+ 'Tilt In|scrub — tied to scroll position' => array(
353
+ 'fx-tilt-in-st' => '3D perspective reveal linked to scroll',
354
+ 'fx-tilt-in' => 'Auto triggered inside a section',
355
+ ),
356
+ 'Parallax|scrub — tied to scroll position' => array(
357
+ 'fx-parallax-st' => 'Parallax Y-shift linked to scroll',
358
+ 'fx-parallax' => 'Auto triggered inside a section',
359
+ ),
360
+ );
361
+
362
+ foreach ( $groups as $title => $classes ) {
363
+ $parts = explode( '|', $title );
364
+ $label = $parts[0];
365
+ $sub = isset( $parts[1] ) ? ' <span style="font-weight:normal;color:#646970;font-size:12px;">(' . esc_html( $parts[1] ) . ')</span>' : '';
366
+ echo '<div class="ffx-group-title">' . esc_html( $label ) . $sub . '</div>';
367
+ foreach ( $classes as $cls => $desc ) {
368
+ echo '<div class="ffx-class-row"><code data-copy>' . esc_html( $cls ) . '</code><span class="ffx-desc">' . esc_html( $desc ) . '</span></div>';
369
+ }
370
+ }
371
+
372
+ // Stagger Children
373
+ echo '<div class="ffx-group-title" style="margin-top: 20px;">Stagger Children <span style="font-weight:normal;color:#646970;font-size:12px;">(pair with an effect class)</span></div>';
374
+ $stagger = array(
375
+ 'fx-stagger-all-[img]' => 'Target all img children — requires effect class',
376
+ 'fx-stagger-all-[img,p]' => 'Target img and p children',
377
+ 'fx-stagger-all-[.card]' => 'Target children by CSS class',
378
+ );
379
+ foreach ( $stagger as $cls => $desc ) {
380
+ echo '<div class="ffx-class-row"><code data-copy>' . esc_html( $cls ) . '</code><span class="ffx-desc">' . esc_html( $desc ) . '</span></div>';
381
+ }
382
+
383
+ // Modifiers
384
+ echo '<div class="ffx-group-title" style="margin-top: 20px;">Modifiers <span style="font-weight:normal;color:#646970;font-size:12px;">(combine with any effect class)</span></div>';
385
+ $modifiers = array(
386
+ 'fx-duration-[1.5]' => 'Custom duration (seconds)',
387
+ 'fx-delay-[0.3]' => 'Delay before animating (seconds)',
388
+ 'fx-stagger-[0.25]' => 'Delay between staggered siblings',
389
+ 'fx-ease-[power2.inOut]' => 'GSAP easing function',
390
+ 'fx-start-[top center]' => 'Scroll trigger start position',
391
+ 'fx-y-[80]' => 'Parallax Y-shift intensity (parallax only)',
392
+ 'fx-scrub-[0.6]' => 'Scrub smoothing (drawSVG scrub mode)',
393
+ );
394
+ foreach ( $modifiers as $cls => $desc ) {
395
+ echo '<div class="ffx-class-row"><code data-copy>' . esc_html( $cls ) . '</code><span class="ffx-desc">' . esc_html( $desc ) . '</span></div>';
396
+ }
397
+ }
398
+ }
package/inc/Editor.php ADDED
@@ -0,0 +1,31 @@
1
+ <?php
2
+
3
+ namespace FancooloFX;
4
+
5
+ if ( ! defined( 'ABSPATH' ) ) {
6
+ exit;
7
+ }
8
+
9
+ class Editor {
10
+
11
+ public static function enqueue() {
12
+ $s = Settings::get();
13
+ if ( ! $s['gutenberg_panel'] ) {
14
+ return;
15
+ }
16
+
17
+ $asset_file = FANCOOLO_FX_PATH . 'assets/editor/index.asset.php';
18
+ if ( ! file_exists( $asset_file ) ) {
19
+ return;
20
+ }
21
+
22
+ $asset = include $asset_file;
23
+ wp_enqueue_script(
24
+ 'fancoolo-fx-editor',
25
+ FANCOOLO_FX_URL . 'assets/editor/index.js',
26
+ $asset['dependencies'],
27
+ $asset['version'],
28
+ true
29
+ );
30
+ }
31
+ }
@@ -0,0 +1,50 @@
1
+ <?php
2
+
3
+ namespace FancooloFX;
4
+
5
+ if ( ! defined( 'ABSPATH' ) ) {
6
+ exit;
7
+ }
8
+
9
+ class Frontend {
10
+
11
+ public static function enqueue() {
12
+ wp_enqueue_script( 'gsap', FANCOOLO_FX_URL . 'assets/gsap.min.js', array(), '3.14.2', true );
13
+ wp_enqueue_script( 'gsap-scrolltrigger', FANCOOLO_FX_URL . 'assets/ScrollTrigger.min.js', array( 'gsap' ), '3.14.2', true );
14
+ wp_enqueue_script( 'gsap-splittext', FANCOOLO_FX_URL . 'assets/SplitText.min.js', array( 'gsap' ), '3.14.2', true );
15
+ wp_enqueue_script( 'fancoolo-fx', FANCOOLO_FX_URL . 'assets/fx.js', array( 'gsap', 'gsap-scrolltrigger', 'gsap-splittext' ), FANCOOLO_FX_VERSION, true );
16
+
17
+ $s = Settings::get();
18
+ $config = array(
19
+ 'scrollStart' => $s['scroll_start'],
20
+ 'scrollOnce' => (bool) $s['scroll_once'],
21
+ 'sectionSelector' => $s['section_selector'],
22
+ 'disableMobile' => (bool) $s['disable_mobile'],
23
+ 'mobileBreakpoint' => (int) $s['mobile_breakpoint'],
24
+ 'speedMultiplier' => (float) $s['speed_multiplier'],
25
+ 'respectReducedMotion' => (bool) $s['respect_reduced_motion'],
26
+ 'excludeSelectors' => $s['exclude_selectors'],
27
+ );
28
+
29
+ if ( ! empty( $s['tag_map'] ) ) {
30
+ $tag_map_obj = array();
31
+ foreach ( $s['tag_map'] as $row ) {
32
+ $tag_map_obj[ $row['selector'] ] = $row['effect'];
33
+ }
34
+ $config['tagMap'] = (object) $tag_map_obj;
35
+ }
36
+
37
+ wp_add_inline_script( 'fancoolo-fx', 'window.__FX_CONFIG__ = ' . wp_json_encode( $config ) . ';', 'before' );
38
+
39
+ if ( $s['debug_markers'] && is_user_logged_in() && current_user_can( 'manage_options' ) ) {
40
+ wp_add_inline_script( 'fancoolo-fx', 'window.__FX_DEBUG_MARKERS__ = true;', 'before' );
41
+ }
42
+
43
+ $custom_file = Settings::get_custom_file_path();
44
+ if ( file_exists( $custom_file ) && filesize( $custom_file ) > 0 ) {
45
+ $upload_dir = wp_upload_dir();
46
+ $custom_url = $upload_dir['baseurl'] . '/fancoolo-fx/custom.js';
47
+ wp_enqueue_script( 'fancoolo-fx-custom', $custom_url, array( 'fancoolo-fx' ), filemtime( $custom_file ), true );
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,66 @@
1
+ <?php
2
+
3
+ namespace FancooloFX;
4
+
5
+ if ( ! defined( 'ABSPATH' ) ) {
6
+ exit;
7
+ }
8
+
9
+ class SaveHandler {
10
+
11
+ public static function handle() {
12
+ if ( ! isset( $_POST['fancoolo_fx_save'] ) ) {
13
+ return;
14
+ }
15
+
16
+ if ( ! check_admin_referer( 'fancoolo_fx_save_action', 'fancoolo_fx_nonce' ) ) {
17
+ return;
18
+ }
19
+
20
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
21
+ return;
22
+ }
23
+
24
+ // Save custom JS file.
25
+ $content = isset( $_POST['fancoolo_fx_code'] ) ? wp_unslash( $_POST['fancoolo_fx_code'] ) : '';
26
+ $upload_dir = wp_upload_dir();
27
+ $dir = $upload_dir['basedir'] . '/fancoolo-fx';
28
+
29
+ if ( ! file_exists( $dir ) ) {
30
+ wp_mkdir_p( $dir );
31
+ }
32
+
33
+ file_put_contents( $dir . '/custom.js', $content );
34
+
35
+ // Build tag map.
36
+ $tag_map = array();
37
+ if ( ! empty( $_POST['fancoolo_fx_tag_map'] ) && is_array( $_POST['fancoolo_fx_tag_map'] ) ) {
38
+ foreach ( $_POST['fancoolo_fx_tag_map'] as $row ) {
39
+ $selector = sanitize_text_field( $row['selector'] ?? '' );
40
+ $effect = sanitize_text_field( $row['effect'] ?? '' );
41
+ if ( '' !== $selector && '' !== $effect ) {
42
+ $tag_map[] = array( 'selector' => $selector, 'effect' => $effect );
43
+ }
44
+ }
45
+ }
46
+
47
+ // Save settings.
48
+ $settings = array(
49
+ 'scroll_start' => sanitize_text_field( $_POST['fancoolo_fx_scroll_start'] ?? 'top 85%' ),
50
+ 'scroll_once' => isset( $_POST['fancoolo_fx_scroll_once'] ) ? '1' : '0',
51
+ 'section_selector' => sanitize_text_field( $_POST['fancoolo_fx_section_selector'] ?? 'section' ),
52
+ 'debug_markers' => isset( $_POST['fancoolo_fx_debug_markers'] ) ? '1' : '0',
53
+ 'disable_mobile' => isset( $_POST['fancoolo_fx_disable_mobile'] ) ? '1' : '0',
54
+ 'mobile_breakpoint' => sanitize_text_field( $_POST['fancoolo_fx_mobile_breakpoint'] ?? '768' ),
55
+ 'speed_multiplier' => sanitize_text_field( $_POST['fancoolo_fx_speed_multiplier'] ?? '1' ),
56
+ 'respect_reduced_motion' => isset( $_POST['fancoolo_fx_respect_reduced_motion'] ) ? '1' : '0',
57
+ 'exclude_selectors' => sanitize_text_field( $_POST['fancoolo_fx_exclude_selectors'] ?? '' ),
58
+ 'gutenberg_panel' => isset( $_POST['fancoolo_fx_gutenberg_panel'] ) ? '1' : '0',
59
+ 'tag_map' => $tag_map,
60
+ );
61
+
62
+ Settings::save( $settings );
63
+
64
+ add_settings_error( 'fancoolo_fx', 'fancoolo_fx_saved', 'Settings saved.', 'success' );
65
+ }
66
+ }
@@ -0,0 +1,41 @@
1
+ <?php
2
+
3
+ namespace FancooloFX;
4
+
5
+ if ( ! defined( 'ABSPATH' ) ) {
6
+ exit;
7
+ }
8
+
9
+ class Settings {
10
+
11
+ private static $defaults = array(
12
+ 'scroll_start' => 'top 85%',
13
+ 'scroll_once' => '1',
14
+ 'section_selector' => 'section',
15
+ 'debug_markers' => '0',
16
+ 'disable_mobile' => '0',
17
+ 'mobile_breakpoint' => '768',
18
+ 'speed_multiplier' => '1',
19
+ 'respect_reduced_motion' => '1',
20
+ 'exclude_selectors' => '',
21
+ 'gutenberg_panel' => '1',
22
+ 'tag_map' => array(),
23
+ );
24
+
25
+ public static function get_defaults() {
26
+ return self::$defaults;
27
+ }
28
+
29
+ public static function get() {
30
+ return wp_parse_args( get_option( 'fancoolo_fx_settings', array() ), self::$defaults );
31
+ }
32
+
33
+ public static function save( $settings ) {
34
+ update_option( 'fancoolo_fx_settings', $settings );
35
+ }
36
+
37
+ public static function get_custom_file_path() {
38
+ $upload_dir = wp_upload_dir();
39
+ return $upload_dir['basedir'] . '/fancoolo-fx/custom.js';
40
+ }
41
+ }
package/inc/update.php ADDED
@@ -0,0 +1,154 @@
1
+ <?php
2
+ // ************************************************************************************************
3
+ // Updater Version: 1.0.2
4
+ // ************************************************************************************************
5
+
6
+ if (!defined('ABSPATH')) exit; // Exit if accessed directly
7
+
8
+ if (!class_exists('FANCOOLOFX_DPUpdateChecker')) {
9
+
10
+ class FANCOOLOFX_DPUpdateChecker {
11
+
12
+ public $slug;
13
+ public $version;
14
+ public $cache_key;
15
+ public $remote_url;
16
+ public $type;
17
+
18
+ public function __construct($slug, $version, $cache_key, $remote_url, $type = 'plugin') {
19
+ $this->slug = $slug;
20
+ $this->version = $version;
21
+ $this->cache_key = $cache_key;
22
+ $this->remote_url = $remote_url;
23
+ $this->type = $type;
24
+ add_action('admin_init', array($this, 'check_update_conditions'));
25
+ add_action('plugin_row_meta', array($this, 'add_check_updates_button'), 10, 2);add_action('admin_post_check_for_updates', array($this, 'check_for_updates'));
26
+ }
27
+
28
+ public function check_update_conditions() {
29
+ add_filter('plugins_api', array($this, 'info'), 20, 3);add_filter('site_transient_update_plugins', array($this, 'update_plugins'));
30
+ add_action('upgrader_process_complete', array($this, 'purge'), 10, 2);
31
+ }
32
+
33
+ public function request() {
34
+ $remote = get_transient($this->cache_key);
35
+
36
+ if (false === $remote) {
37
+ $response = wp_remote_get(
38
+ $this->remote_url,
39
+ array(
40
+ 'timeout' => 10,
41
+ 'headers' => array('Accept' => 'application/json')
42
+ )
43
+ );
44
+
45
+ if (
46
+ is_wp_error($response)
47
+ || 200 !== wp_remote_retrieve_response_code($response)
48
+ || empty(wp_remote_retrieve_body($response))
49
+ ) {
50
+ error_log('Hoster update check error: ' . wp_remote_retrieve_body($response) ?: $response->get_error_message());
51
+ return false;
52
+ }
53
+
54
+ $remote = wp_remote_retrieve_body($response);
55
+ set_transient($this->cache_key, $remote, 6 * HOUR_IN_SECONDS);
56
+ }
57
+
58
+ if(!is_array($remote)){
59
+ $remote = json_decode($remote);
60
+ }
61
+ return $remote;
62
+ }
63
+
64
+
65
+ public function info($res, $action, $args) {
66
+ if ('plugin_information' !== $action || $this->slug !== $args->slug) {
67
+ return $res;
68
+ }
69
+
70
+ $remote = $this->request();
71
+ if (!$remote) return $res;
72
+
73
+ $res = new stdClass();
74
+ $res->name = $remote->name;
75
+ $res->slug = $remote->slug;
76
+ $res->version = $remote->version;
77
+ $res->tested = $remote->tested;
78
+ $res->requires = $remote->requires;
79
+ $res->author = $remote->author;
80
+ $res->author_profile = $remote->author_profile;
81
+ $res->download_link = $remote->download_url;
82
+ $res->trunk = $remote->download_url;
83
+ $res->requires_php = $remote->requires_php;
84
+ $res->last_updated = $remote->last_updated;
85
+ $res->sections = (array)$remote->sections;
86
+
87
+ if (!empty($remote->banners)) {
88
+ $res->banners = (array)$remote->banners;
89
+ }
90
+
91
+ if (!empty($remote->icons)) {
92
+ $res->icons = (array)$remote->icons;
93
+ }
94
+
95
+ return $res;
96
+ }
97
+
98
+ public function update_plugins($transient) {
99
+ if (empty($transient->checked)) return $transient;
100
+
101
+ $remote = $this->request();
102
+ if (!$remote) return $transient;
103
+
104
+ if (
105
+ version_compare($this->version, $remote->version, '<')
106
+ && version_compare($remote->requires, get_bloginfo('version'), '<=')
107
+ && version_compare($remote->requires_php, PHP_VERSION, '<=')
108
+ ) {
109
+ $res = new stdClass();
110
+ $res->slug = $this->slug;
111
+ $res->new_version = $remote->version;
112
+ $res->tested = $remote->tested;
113
+ $res->package = $remote->download_url;
114
+
115
+ if (!empty($remote->icons)) {
116
+ $res->icons = (array)$remote->icons;
117
+ }
118
+
119
+ $res->plugin = $this->slug;
120
+ $transient->response[$res->plugin] = $res;
121
+ }
122
+
123
+ return $transient;
124
+ }
125
+
126
+ public function add_check_updates_button($plugin_meta, $plugin_file) {
127
+ if ($plugin_file === $this->slug) {
128
+ $url = wp_nonce_url(admin_url('admin-post.php?action=check_for_updates&plugin=' . $this->slug), 'check_for_updates');
129
+ $plugin_meta[] = '<a href="' . esc_url($url) . '">Check for updates</a>';
130
+ }
131
+ return $plugin_meta;
132
+ }
133
+
134
+ public function check_for_updates() {
135
+ if (!current_user_can('update_plugins')) {
136
+ wp_die(__('You do not have sufficient permissions to update plugins.'));
137
+ }
138
+
139
+ check_admin_referer('check_for_updates');
140
+ delete_transient($this->cache_key);
141
+ $this->request();
142
+ wp_redirect(admin_url('plugins.php'));
143
+ exit;
144
+ }
145
+
146
+
147
+
148
+ public function purge($upgrader, $options) {
149
+ if ('update' === $options['action'] && $this->type === $options['type']) {
150
+ delete_transient($this->cache_key);
151
+ }
152
+ }
153
+ }
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fancoolo-fx",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "A class-driven GSAP animation wrapper for WordPress and static sites.",
5
5
  "main": "src/fx.js",
6
6
  "homepage": "https://krstivoja.github.io/fancoolo-fx/",
@@ -14,10 +14,15 @@
14
14
  "keywords": ["gsap", "animation", "scrolltrigger", "splittext", "wordpress", "gutenberg"],
15
15
  "author": "Fancoolo",
16
16
  "scripts": {
17
- "sync": "cp src/fx.js assets/fx.js && cp src/fx.js docs/vendor/fx.js"
17
+ "sync": "cp src/fx.js assets/fx.js && cp src/fx.js docs/vendor/fx.js && cp node_modules/gsap/dist/gsap.min.js assets/gsap.min.js && cp node_modules/gsap/dist/ScrollTrigger.min.js assets/ScrollTrigger.min.js && cp node_modules/gsap/dist/SplitText.min.js assets/SplitText.min.js && cp node_modules/gsap/dist/gsap.min.js docs/vendor/gsap.min.js && cp node_modules/gsap/dist/ScrollTrigger.min.js docs/vendor/ScrollTrigger.min.js && cp node_modules/gsap/dist/SplitText.min.js docs/vendor/SplitText.min.js",
18
+ "build": "wp-scripts build src/editor/index.js --output-path=assets/editor && npm run sync",
19
+ "start": "wp-scripts start src/editor/index.js --output-path=assets/editor"
18
20
  },
19
21
  "license": "ISC",
20
22
  "dependencies": {
21
23
  "gsap": "^3.14.2"
24
+ },
25
+ "devDependencies": {
26
+ "@wordpress/scripts": "^30.0.0"
22
27
  }
23
28
  }