@tidynav/core 0.1.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 (2) hide show
  1. package/dist/tidynav.core.js +767 -0
  2. package/package.json +20 -0
@@ -0,0 +1,767 @@
1
+ (function (window, document) {
2
+ "use strict";
3
+
4
+ var api = window.TidyNav || {};
5
+ var runtime =
6
+ api.__runtime ||
7
+ (api.__runtime = {
8
+ states: [],
9
+ listenersReady: false,
10
+ readyMotionKeys: new WeakMap(),
11
+ });
12
+
13
+ var focusSelector =
14
+ 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
15
+
16
+ var defaultSelectors = {
17
+ button: '[data-tidynav-role="menu-btn"]',
18
+ menu: '[data-tidynav-role="menu"]',
19
+ backdrop: "[data-tidynav-menu-backdrop]",
20
+ lineTop: '[data-tidynav-menu-line="top"]',
21
+ lineMiddle: '[data-tidynav-menu-line="middle"]',
22
+ lineBottom: '[data-tidynav-menu-line="bottom"]',
23
+ };
24
+
25
+ var defaultBreakpoints = {
26
+ tiny: "(max-width: 479px)",
27
+ small: "(min-width: 480px) and (max-width: 767px)",
28
+ medium: "(min-width: 768px) and (max-width: 991px)",
29
+ main: "(min-width: 992px) and (max-width: 1279px)",
30
+ large: "(min-width: 1280px) and (max-width: 1439px)",
31
+ xl: "(min-width: 1440px) and (max-width: 1919px)",
32
+ xxl: "(min-width: 1920px)",
33
+ };
34
+
35
+ function init(config) {
36
+ var options = config || api.config || {};
37
+ if (config) api.config = config;
38
+ ensureListeners();
39
+ initializeConfig(buildConfig(options));
40
+ }
41
+
42
+ function destroy(id) {
43
+ var i = runtime.states.length;
44
+ while (i--) {
45
+ var state = runtime.states[i];
46
+ if (state.id !== id) continue;
47
+
48
+ if (state.open) {
49
+ setMenuOpen(state, false, { skipMotion: true, returnFocus: false });
50
+ }
51
+
52
+ resetMotionMedia(state);
53
+
54
+ if (state._toggleTimeline) {
55
+ state._toggleTimeline.kill();
56
+ state._toggleTimeline = null;
57
+ state._toggleTargets = null;
58
+ }
59
+
60
+ if (window.gsap && window.gsap.killTweensOf) {
61
+ if (state.button) window.gsap.killTweensOf(state.button);
62
+ if (state.menu) window.gsap.killTweensOf(state.menu);
63
+ if (state.backdrop) window.gsap.killTweensOf(state.backdrop);
64
+ }
65
+
66
+ runtime.states.splice(i, 1);
67
+ }
68
+
69
+ if (runtime.states.length === 0) {
70
+ runtime.listenersReady = false;
71
+ }
72
+ }
73
+
74
+ function buildConfig(options) {
75
+ var id = options.id || "";
76
+ var motion = options.motion || {};
77
+ var a11y = options.a11y || {};
78
+
79
+ return {
80
+ enabled: options.enabled !== false,
81
+ id: id,
82
+ debug: options.debug === true,
83
+ a11y: {
84
+ mobileQuery: a11y.mobileQuery || "(max-width: 767px)",
85
+ selectors: defaultSelectors,
86
+ labels: merge({ open: "Open menu", close: "Close menu" }, a11y.labels),
87
+ },
88
+ motion: {
89
+ breakpoints: merge(defaultBreakpoints, motion.breakpoints),
90
+ themes: motion.themes || { base: { animations: [] } },
91
+ },
92
+ };
93
+ }
94
+
95
+ function merge(base, overrides) {
96
+ var output = {};
97
+ var key;
98
+ for (key in base) {
99
+ if (Object.prototype.hasOwnProperty.call(base, key)) output[key] = base[key];
100
+ }
101
+ overrides = overrides || {};
102
+ for (key in overrides) {
103
+ if (Object.prototype.hasOwnProperty.call(overrides, key)) {
104
+ output[key] = overrides[key];
105
+ }
106
+ }
107
+ return output;
108
+ }
109
+
110
+ function initializeConfig(config) {
111
+ if (!config.id) return;
112
+ var selector = '[data-tidynav-id="' + config.id + '"]';
113
+ queryAll(document, selector).forEach(function (root, index) {
114
+ initializeRoot(config, root, index);
115
+ });
116
+ }
117
+
118
+ function initializeRoot(config, root, index) {
119
+ if (!config.enabled) return;
120
+
121
+ var state = findState(root, config.id);
122
+ if (!state) {
123
+ state = { root: root, id: config.id };
124
+ runtime.states.push(state);
125
+ }
126
+
127
+ state.config = config;
128
+ initializeMenu(state, index);
129
+ runReadyMotion(state);
130
+ }
131
+
132
+ function initializeMenu(state, index) {
133
+ var config = state.config;
134
+ var selectors = config.a11y.selectors;
135
+ var button = state.root.querySelector(selectors.button);
136
+ var menu = state.root.querySelector(selectors.menu);
137
+ var backdrop = state.root.querySelector(selectors.backdrop);
138
+
139
+ if (!button || !menu) {
140
+ warnMissing(config, state.root, !button ? "button" : "menu");
141
+ return;
142
+ }
143
+
144
+ state.button = button;
145
+ state.menu = menu;
146
+ state.backdrop = backdrop;
147
+
148
+ var menuId = makeMenuId(config.id, index);
149
+ menu.setAttribute("id", menuId);
150
+ button.setAttribute("aria-controls", menuId);
151
+ setMenuOpen(state, false, { skipMotion: true });
152
+ }
153
+
154
+ function findState(root, id) {
155
+ for (var i = 0; i < runtime.states.length; i += 1) {
156
+ if (runtime.states[i].root === root && runtime.states[i].id === id) {
157
+ return runtime.states[i];
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+
163
+ function makeMenuId(id, index) {
164
+ var prefix = id || "tidynav-menu";
165
+ return prefix.replace(/[^a-zA-Z0-9_-]/g, "-") + "-menu-" + (index + 1);
166
+ }
167
+
168
+ function warnMissing(config, root, name) {
169
+ if (!window.console || !window.console.warn) return;
170
+ window.console.warn(
171
+ "[TidyNav] Missing " +
172
+ name +
173
+ " for " +
174
+ (config.id || "navigation") +
175
+ ".",
176
+ root
177
+ );
178
+ }
179
+
180
+ function ensureListeners() {
181
+ if (runtime.listenersReady) return;
182
+ runtime.listenersReady = true;
183
+
184
+ document.addEventListener("click", handleClick);
185
+ document.addEventListener("keydown", handleKeydown);
186
+
187
+ window.addEventListener("resize", function () {
188
+ runtime.states.slice().forEach(function (state) {
189
+ if (state.button && state.menu) {
190
+ setMenuOpen(state, false, { skipMotion: true });
191
+ }
192
+ });
193
+ });
194
+ }
195
+
196
+ function handleClick(event) {
197
+ var target = event.target;
198
+ if (!target || !target.closest) return;
199
+
200
+ var buttonState = getStateForTarget(target, "button");
201
+ if (buttonState) {
202
+ event.preventDefault();
203
+ setMenuOpen(buttonState, !buttonState.open);
204
+ return;
205
+ }
206
+
207
+ var linkState = getOpenStateForMenuLink(target);
208
+ if (linkState && isMobileLayout(linkState.config)) {
209
+ setMenuOpen(linkState, false);
210
+ return;
211
+ }
212
+
213
+ var backdropState = getStateForTarget(target, "backdrop");
214
+ if (backdropState && backdropState.open) {
215
+ setMenuOpen(backdropState, false, { returnFocus: true });
216
+ }
217
+ }
218
+
219
+ function handleKeydown(event) {
220
+ var active = document.activeElement;
221
+ var activeButtonState = active && active.closest ? getStateForTarget(active, "button") : null;
222
+
223
+ if (activeButtonState && (event.key === " " || event.key === "Spacebar")) {
224
+ event.preventDefault();
225
+ setMenuOpen(activeButtonState, !activeButtonState.open);
226
+ return;
227
+ }
228
+
229
+ if (event.key === "Escape") {
230
+ getOpenStates().forEach(function (state) {
231
+ setMenuOpen(state, false, { returnFocus: true });
232
+ });
233
+ return;
234
+ }
235
+
236
+ if (event.key !== "Tab") return;
237
+
238
+ var trapState = getOpenStates().filter(function (state) {
239
+ return (
240
+ isMobileLayout(state.config) &&
241
+ (state.menu.contains(active) || active === state.button)
242
+ );
243
+ })[0];
244
+ if (!trapState) return;
245
+
246
+ trapFocus(event, trapState);
247
+ }
248
+
249
+ function getStateForTarget(target, role) {
250
+ for (var i = 0; i < runtime.states.length; i += 1) {
251
+ var state = runtime.states[i];
252
+ var selector = state.config.a11y.selectors[role];
253
+ var match = selector && target.closest(selector);
254
+ if (match && state.root.contains(match)) return state;
255
+ }
256
+ return null;
257
+ }
258
+
259
+ function getOpenStateForMenuLink(target) {
260
+ var link = target.closest && target.closest("a[href]");
261
+ if (!link) return null;
262
+
263
+ return getOpenStates().filter(function (state) {
264
+ return state.menu.contains(link);
265
+ })[0];
266
+ }
267
+
268
+ function getOpenStates() {
269
+ return runtime.states.filter(function (state) {
270
+ return state.open === true;
271
+ });
272
+ }
273
+
274
+ function setMenuOpen(state, open, options) {
275
+ var config = state.config;
276
+ var a11y = config.a11y;
277
+ resetMotionMedia(state);
278
+ state.open = open;
279
+
280
+ state.root.setAttribute("data-tidynav-state", open ? "open" : "closed");
281
+ state.button.setAttribute("data-tidynav-state", open ? "open" : "closed");
282
+ state.menu.setAttribute("data-tidynav-state", open ? "open" : "closed");
283
+ if (state.backdrop) {
284
+ state.backdrop.setAttribute("data-tidynav-state", open ? "open" : "closed");
285
+ }
286
+
287
+ state.button.setAttribute("aria-expanded", String(open));
288
+ state.button.setAttribute("aria-label", open ? a11y.labels.close : a11y.labels.open);
289
+ setMenuAvailable(state.menu, open || !isMobileLayout(config), state.button);
290
+
291
+ if (!options || !options.skipMotion) {
292
+ runMotionTrigger(state, open ? "menuOpen" : "menuClose");
293
+ } else {
294
+ runMotionTrigger(state, open ? "menuOpen" : "menuClose", { instant: true });
295
+ }
296
+
297
+ if (!open && options && options.returnFocus && state.button.focus) {
298
+ state.button.focus();
299
+ }
300
+ }
301
+
302
+ function setMenuAvailable(menu, available, button) {
303
+ menu.setAttribute("aria-hidden", String(!available));
304
+ queryAll(menu, "a[href], button, input, select, textarea").forEach(function (item) {
305
+ if (item === button) return;
306
+ if (available) {
307
+ item.removeAttribute("tabindex");
308
+ } else {
309
+ item.setAttribute("tabindex", "-1");
310
+ }
311
+ });
312
+ }
313
+
314
+ function trapFocus(event, state) {
315
+ var focusItems = [state.button].concat(queryAll(state.menu, focusSelector)).filter(Boolean);
316
+ var first = focusItems[0];
317
+ var last = focusItems[focusItems.length - 1];
318
+
319
+ if (event.shiftKey && document.activeElement === first) {
320
+ event.preventDefault();
321
+ last.focus();
322
+ } else if (!event.shiftKey && document.activeElement === last) {
323
+ event.preventDefault();
324
+ first.focus();
325
+ }
326
+ }
327
+
328
+ function runReadyMotion(state) {
329
+ runMotionTrigger(state, "ready", { once: true });
330
+ }
331
+
332
+ function resetMotionMedia(state) {
333
+ if (state.motionMedia && state.motionMedia.revert) {
334
+ state.motionMedia.revert();
335
+ }
336
+ state.motionMedia = null;
337
+ }
338
+
339
+ function runMotionTrigger(state, trigger, options) {
340
+ var motion = state.config.motion;
341
+ var groups = {};
342
+ var toggleGroup = null;
343
+ if (!window.gsap) return;
344
+
345
+ if (state.config.debug) console.log('[TidyNav] trigger:', trigger, 'open:', state.open);
346
+
347
+ getActiveMotionEntries(state).forEach(function (entry) {
348
+ entry.theme.animations.forEach(function (animation, index) {
349
+ var query;
350
+
351
+ // menuToggle fires for both menuOpen and menuClose — the runtime
352
+ // creates a GSAP timeline and plays/reverses it automatically.
353
+ var matchesTrigger = animation.on === trigger;
354
+ if (animation.on === 'menuToggle' && (trigger === 'menuOpen' || trigger === 'menuClose')) {
355
+ matchesTrigger = true;
356
+ }
357
+
358
+ if (!matchesTrigger || !matchesAnimationBreakpoints(state, animation)) {
359
+ return;
360
+ }
361
+
362
+ if (options && options.once && hasReadyMotionRun(state, entry.id, index)) {
363
+ return;
364
+ }
365
+
366
+ // menuToggle animations go into a separate timeline group —
367
+ // they play forward on menuOpen and reverse on menuClose.
368
+ if (animation.on === 'menuToggle') {
369
+ toggleGroup = toggleGroup || [];
370
+ toggleGroup.push({
371
+ animation: animation,
372
+ instant: shouldUseInstantMotion(animation, options),
373
+ });
374
+ } else {
375
+ query = getAnimationMediaQuery(state, animation) || "__all__";
376
+ groups[query] = groups[query] || [];
377
+ groups[query].push({
378
+ animation: animation,
379
+ instant: shouldUseInstantMotion(animation, options),
380
+ });
381
+ }
382
+
383
+ if (options && options.once) {
384
+ markReadyMotionRun(state, entry.id, index);
385
+ }
386
+ });
387
+ });
388
+
389
+ if (toggleGroup) {
390
+ runToggleTimeline(state, toggleGroup, trigger, options);
391
+ }
392
+ runResponsiveGsapGroups(state, groups);
393
+ }
394
+
395
+ function shouldUseInstantMotion(animation, options) {
396
+ if (options && options.instant) return true;
397
+ if (animation.instant === true) return true;
398
+ return window.matchMedia &&
399
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
400
+ }
401
+
402
+ function getActiveMotionEntries(state) {
403
+ var themes = state.config.motion.themes;
404
+ var id;
405
+
406
+ // Variant themes take priority. Only ONE theme's animations run.
407
+ // Base (no className) is the fallback — it only fires when no
408
+ // variant theme's className matches the root element.
409
+ for (id in themes) {
410
+ if (!Object.prototype.hasOwnProperty.call(themes, id)) continue;
411
+ if (state.config.debug) console.log('[TidyNav] getActiveMotionEntries: checking', id, 'className:', themes[id].className);
412
+ if (!themes[id].className) continue; // skip base — checked last
413
+ if (isThemeActive(state, themes[id])) {
414
+ if (state.config.debug) console.log('[TidyNav] active theme:', id, 'className:', themes[id].className);
415
+ return [{ id: id, theme: themes[id] }];
416
+ }
417
+ }
418
+
419
+ // No variant matched — fall back to base
420
+ if (themes.base) {
421
+ if (state.config.debug) console.log('[TidyNav] active theme: base (no variant matched)');
422
+ return [{ id: 'base', theme: themes.base }];
423
+ }
424
+
425
+ return [];
426
+ }
427
+
428
+ function isThemeActive(state, theme) {
429
+ var className = theme.className;
430
+ if (!className) return true;
431
+
432
+ if (state.config.debug) console.log('[TidyNav] isThemeActive: checking className:', className);
433
+
434
+ // Resolve preview variant tokens at runtime. In the preview tool,
435
+ // {{variantClass:X}} tokens are not replaced before the script runs.
436
+ // We resolve them against the preview theme data attribute instead.
437
+ if (className.indexOf('{{variantClass:') === 0) {
438
+ var previewTheme = resolvePreviewTheme();
439
+ if (!previewTheme) {
440
+ if (state.config.debug) console.log('[TidyNav] isThemeActive: token', className, '→ no preview theme, skipping');
441
+ return false;
442
+ }
443
+ className = 'tidynav-preview-variant-' + previewTheme;
444
+ if (state.config.debug) console.log('[TidyNav] isThemeActive: resolved token →', className);
445
+ }
446
+
447
+ if (hasClass(state.root, className)) {
448
+ if (state.config.debug) console.log('[TidyNav] isThemeActive:', className, 'found on root');
449
+ return true;
450
+ }
451
+ if (state.menu && hasClass(state.menu, className)) {
452
+ if (state.config.debug) console.log('[TidyNav] isThemeActive:', className, 'found on menu');
453
+ return true;
454
+ }
455
+
456
+ return queryAll(state.root, "." + cssIdentifier(className)).length > 0;
457
+ }
458
+
459
+ function resolvePreviewTheme() {
460
+ try {
461
+ var theme = document.documentElement.dataset.previewTheme;
462
+ if (theme && theme !== 'wireframe') return theme;
463
+ } catch (_) { /* not in a browser */ }
464
+ return null;
465
+ }
466
+
467
+ function matchesAnimationBreakpoints(state, animation) {
468
+ var breakpoints = animation.breakpoints || animation.breakpoint;
469
+ var queries = state.config.motion.breakpoints;
470
+
471
+ if (!breakpoints) return true;
472
+ if (typeof breakpoints === "string") breakpoints = [breakpoints];
473
+ if (!Array.isArray(breakpoints) || breakpoints.length === 0) return true;
474
+
475
+ return breakpoints.some(function (breakpoint) {
476
+ var query = queries[breakpoint] || breakpoint;
477
+ return window.matchMedia(query).matches;
478
+ });
479
+ }
480
+
481
+ function hasReadyMotionRun(state, variantId, animationIndex) {
482
+ var keys = runtime.readyMotionKeys.get(state.root);
483
+ var key = variantId + ":" + animationIndex;
484
+ return Boolean(keys && keys[key]);
485
+ }
486
+
487
+ function markReadyMotionRun(state, variantId, animationIndex) {
488
+ var keys = runtime.readyMotionKeys.get(state.root);
489
+ var key = variantId + ":" + animationIndex;
490
+
491
+ if (!keys) {
492
+ keys = {};
493
+ runtime.readyMotionKeys.set(state.root, keys);
494
+ }
495
+
496
+ keys[key] = true;
497
+ }
498
+
499
+ function runResponsiveGsapGroups(state, groups) {
500
+ var query;
501
+
502
+ for (query in groups) {
503
+ if (!Object.prototype.hasOwnProperty.call(groups, query)) continue;
504
+ runResponsiveGsapGroup(state, query, groups[query]);
505
+ }
506
+ }
507
+
508
+ function runResponsiveGsapGroup(state, query, animations) {
509
+ var gsap = window.gsap;
510
+
511
+ if (query === "__all__") {
512
+ runGsapAnimationGroup(state, animations);
513
+ return;
514
+ }
515
+
516
+ if (!gsap.matchMedia) {
517
+ runGsapAnimationGroup(state, animations);
518
+ return;
519
+ }
520
+
521
+ state.motionMedia = state.motionMedia || gsap.matchMedia();
522
+ state.motionMedia.add(query, function () {
523
+ runGsapAnimationGroup(state, animations);
524
+ });
525
+ }
526
+
527
+ function runToggleTimeline(state, animations, trigger, options) {
528
+ var gsap = window.gsap;
529
+ var instant = options && options.instant;
530
+
531
+ if (state.config.debug) console.log('[TidyNav] toggleTimeline:', trigger, 'entries:', animations.length, 'instant:', !!instant);
532
+
533
+ // Build the timeline once on first open and reuse it across toggles.
534
+ if (!state._toggleTimeline) {
535
+ var targets = [];
536
+ var tl = gsap.timeline({ paused: true });
537
+ animations.forEach(function (entry) {
538
+ addToTimeline(tl, state, entry.animation, targets);
539
+ });
540
+
541
+ // After reverse completes, clear inline styles so the element
542
+ // returns to pure CSS. This is the key — no leftover GSAP cruft.
543
+ tl.eventCallback('onReverseComplete', function () {
544
+ if (state.config.debug) console.log('[TidyNav] reverse complete, clearing inline styles');
545
+ clearToggleTargets(gsap, targets);
546
+ clearButtonMotionState(state);
547
+ });
548
+ tl.eventCallback('onComplete', function () {
549
+ clearButtonMotionState(state);
550
+ });
551
+
552
+ state._toggleTimeline = tl;
553
+ state._toggleTargets = targets;
554
+ }
555
+
556
+ var tl = state._toggleTimeline;
557
+ setButtonMotionState(state, trigger === 'menuOpen' ? 'opening' : 'closing');
558
+
559
+ if (trigger === 'menuOpen') {
560
+ if (instant) {
561
+ tl.progress(1);
562
+ clearButtonMotionState(state);
563
+ } else {
564
+ tl.play();
565
+ }
566
+ } else {
567
+ if (instant) {
568
+ tl.progress(0);
569
+ if (state._toggleTargets) clearToggleTargets(gsap, state._toggleTargets);
570
+ clearButtonMotionState(state);
571
+ } else {
572
+ tl.reverse();
573
+ }
574
+ }
575
+ }
576
+
577
+ function setButtonMotionState(state, value) {
578
+ if (!state.button) return;
579
+ state.button.setAttribute('data-tidynav-motion', value);
580
+ }
581
+
582
+ function clearButtonMotionState(state) {
583
+ if (!state.button) return;
584
+ state.button.removeAttribute('data-tidynav-motion');
585
+ }
586
+
587
+ function isToggleTarget(toggleTargets, target) {
588
+ if (!toggleTargets || !toggleTargets.length) return false;
589
+ if (Array.isArray(target)) {
590
+ return target.some(function (t) { return toggleTargets.indexOf(t) !== -1; });
591
+ }
592
+ return toggleTargets.indexOf(target) !== -1;
593
+ }
594
+
595
+ function clearToggleTargets(gsap, targets) {
596
+ targets.forEach(function (target) {
597
+ gsap.set(target, { clearProps: 'transform,opacity,visibility,pointerEvents,color,backgroundColor,borderColor,right' });
598
+ });
599
+ }
600
+
601
+ function addToTimeline(tl, state, animation, targets) {
602
+ var gsap = window.gsap;
603
+ var target;
604
+
605
+ var pos = typeof animation.position === 'number' ? animation.position : 0;
606
+
607
+ if (Array.isArray(animation.fromTo)) {
608
+ target = resolveGsapTargets(state.root, animation.fromTo[0]);
609
+ if (!target || (Array.isArray(target) && target.length === 0)) return;
610
+ tl.fromTo(target, animation.fromTo[1], animation.fromTo[2], pos);
611
+ } else if (Array.isArray(animation.to)) {
612
+ target = resolveGsapTargets(state.root, animation.to[0]);
613
+ if (!target || (Array.isArray(target) && target.length === 0)) return;
614
+ tl.to(target, animation.to[1], pos);
615
+ } else if (Array.isArray(animation.from)) {
616
+ target = resolveGsapTargets(state.root, animation.from[0]);
617
+ if (!target || (Array.isArray(target) && target.length === 0)) return;
618
+ tl.from(target, animation.from[1], pos);
619
+ } else {
620
+ return;
621
+ }
622
+
623
+ // Collect targets for cleanup after reverse completes
624
+ if (targets) {
625
+ if (Array.isArray(target)) {
626
+ targets.push.apply(targets, target);
627
+ } else {
628
+ targets.push(target);
629
+ }
630
+ }
631
+ }
632
+
633
+ function runGsapAnimationGroup(state, animations) {
634
+ animations.forEach(function (entry) {
635
+ runGsapAnimation(state, entry.animation, entry.instant);
636
+ });
637
+ }
638
+
639
+ function getAnimationMediaQuery(state, animation) {
640
+ var breakpoints = animation.breakpoints || animation.breakpoint;
641
+ var queries = state.config.motion.breakpoints;
642
+
643
+ if (!breakpoints) return "";
644
+ if (typeof breakpoints === "string") breakpoints = [breakpoints];
645
+ if (!Array.isArray(breakpoints) || breakpoints.length === 0) return "";
646
+
647
+ return breakpoints.map(function (breakpoint) {
648
+ return queries[breakpoint] || breakpoint;
649
+ }).join(", ");
650
+ }
651
+
652
+ function runGsapAnimation(state, animation, instant) {
653
+ var gsap = window.gsap;
654
+ var method;
655
+ var args;
656
+
657
+ if (Array.isArray(animation.fromTo)) {
658
+ method = "fromTo";
659
+ args = animation.fromTo;
660
+ } else if (Array.isArray(animation.to)) {
661
+ method = "to";
662
+ args = animation.to;
663
+ } else if (Array.isArray(animation.from)) {
664
+ method = "from";
665
+ args = animation.from;
666
+ } else if (Array.isArray(animation.set)) {
667
+ method = "set";
668
+ args = animation.set;
669
+ } else {
670
+ return;
671
+ }
672
+
673
+ args = args.slice();
674
+ args[0] = resolveGsapTargets(state.root, args[0]);
675
+ if (!args[0] || (Array.isArray(args[0]) && args[0].length === 0)) {
676
+ if (state.config.debug) console.log('[TidyNav] skipped (no target):', animation.on, animation[method] && animation[method][0]);
677
+ return;
678
+ }
679
+
680
+ if (state.config.debug) {
681
+ var targetCount = Array.isArray(args[0]) ? args[0].length : 1;
682
+ console.log('[TidyNav] ' + method, '→', targetCount, 'target(s), on:', animation.on);
683
+ }
684
+
685
+ // Don't kill tweens on targets owned by a toggle timeline —
686
+ // killTweensOf would destroy the timeline's tweens.
687
+ var skipKill = state._toggleTimeline && isToggleTarget(state._toggleTargets, args[0]);
688
+ if (gsap.killTweensOf && !skipKill) gsap.killTweensOf(args[0]);
689
+ if (instant) {
690
+ if (state.config.debug) console.log('[TidyNav] instant ' + method, JSON.stringify(args[1]));
691
+ runInstantGsapAnimation(gsap, method, args);
692
+ return;
693
+ }
694
+
695
+ if (state.config.debug) console.log('[TidyNav] tween ' + method, JSON.stringify(args[1]));
696
+ gsap[method].apply(gsap, args);
697
+ }
698
+
699
+ function runInstantGsapAnimation(gsap, method, args) {
700
+ var target = args[0];
701
+ var finalVars;
702
+
703
+ if (method === "fromTo") {
704
+ finalVars = args[2] || {};
705
+ } else {
706
+ finalVars = args[1] || {};
707
+ }
708
+
709
+ gsap.set(target, toImmediateVars(finalVars));
710
+ }
711
+
712
+ function toImmediateVars(vars) {
713
+ var output = {};
714
+ var key;
715
+
716
+ vars = vars || {};
717
+ for (key in vars) {
718
+ if (!Object.prototype.hasOwnProperty.call(vars, key)) continue;
719
+ if (isGsapTimingKey(key)) continue;
720
+ output[key] = vars[key];
721
+ }
722
+
723
+ return output;
724
+ }
725
+
726
+ function isGsapTimingKey(key) {
727
+ return (
728
+ key === "duration" ||
729
+ key === "delay" ||
730
+ key === "stagger" ||
731
+ key === "ease" ||
732
+ key === "overwrite" ||
733
+ key === "onComplete" ||
734
+ key === "onStart" ||
735
+ key === "onUpdate"
736
+ );
737
+ }
738
+
739
+ function resolveGsapTargets(root, target) {
740
+ if (typeof target === "string") return queryAll(root, target);
741
+ if (target && target.nodeType) return target;
742
+ if (Array.isArray(target)) return target;
743
+ return target;
744
+ }
745
+
746
+ function isMobileLayout(config) {
747
+ return window.matchMedia(config.a11y.mobileQuery).matches;
748
+ }
749
+
750
+ function queryAll(root, selector) {
751
+ if (!root || !selector) return [];
752
+ return Array.prototype.slice.call(root.querySelectorAll(selector));
753
+ }
754
+
755
+ function hasClass(element, className) {
756
+ return Boolean(element && className && element.classList.contains(className));
757
+ }
758
+
759
+ function cssIdentifier(value) {
760
+ if (window.CSS && window.CSS.escape) return window.CSS.escape(String(value));
761
+ return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
762
+ }
763
+
764
+ api.init = init;
765
+ api.destroy = destroy;
766
+ window.TidyNav = api;
767
+ })(window, document);
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@tidynav/core",
3
+ "version": "0.1.0",
4
+ "description": "Accessible animated navigation helpers for TidyNav.",
5
+ "main": "dist/tidynav.core.js",
6
+ "files": [
7
+ "dist/tidynav.core.js"
8
+ ],
9
+ "keywords": [
10
+ "tidynav",
11
+ "webflow",
12
+ "navigation",
13
+ "navbar",
14
+ "accessibility",
15
+ "gsap"
16
+ ],
17
+ "author": "Champion Labs",
18
+ "license": "MIT",
19
+ "homepage": "https://tidynav.com"
20
+ }