well-petal 0.0.21 → 0.0.23

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,76 @@
1
+ export function animateScaleUp(): GSAPTweenVars {
2
+ return {
3
+ from: { scale: 0, opacity: 0 },
4
+ to: { scale: 1, opacity: 1, ease: "power3.inOut" },
5
+ };
6
+ }
7
+ export function animateScaleDown(): GSAPTweenVars {
8
+ return {
9
+ from: { scale: 1, opacity: 0 },
10
+ to: { scale: 0, opacity: 1, ease: "power3.inOut" },
11
+ };
12
+ }
13
+ export function animateOpenSlideUp(): GSAPTweenVars {
14
+ return {
15
+ from: { y: "100%", x: "0%", opacity: 0 }, // From bottom
16
+ to: { y: "0%", x: "0%", opacity: 1, ease: "power3.inOut" }, // To top
17
+ };
18
+ }
19
+ export function animateOpenSlideDown(): GSAPTweenVars {
20
+ return {
21
+ from: { y: "-100%", x: "0%", opacity: 0 }, // From top
22
+ to: { y: "0%", x: "0%", opacity: 1, ease: "power3.inOut" }, // To bottom
23
+ };
24
+ }
25
+ export function animateOpenSlideRight(): GSAPTweenVars {
26
+ return {
27
+ from: { x: "-100%", y: "0%", opacity: 0 }, // From left
28
+ to: { x: "0%", y: "0%", opacity: 1, ease: "power3.inOut" }, // To center
29
+ };
30
+ }
31
+ export function animateOpenSlideLeft(): GSAPTweenVars {
32
+ return {
33
+ from: { x: "100%", y: "0%", opacity: 0 }, // From right
34
+ to: { x: "0%", y: "0%", opacity: 1, ease: "power3.inOut" }, // To center
35
+ };
36
+ }
37
+ export function animateCloseSlideUp(): GSAPTweenVars {
38
+ return {
39
+ from: { y: "0%", x: "0%", opacity: 1 }, // From bottom
40
+ to: { y: "-100%", x: "0%", opacity: 0, ease: "power3.inOut" }, // To top
41
+ };
42
+ }
43
+ export function animateCloseSlideDown(): GSAPTweenVars {
44
+ return {
45
+ from: { y: "0%", x: "0%", opacity: 1 }, // From top
46
+ to: { y: "100%", x: "0%", opacity: 0, ease: "power3.inOut" }, // To bottom
47
+ };
48
+ }
49
+ export function animateCloseSlideRight(): GSAPTweenVars {
50
+ return {
51
+ from: { x: "0%", y: "0%", opacity: 1 }, // From left
52
+ to: { x: "100%", y: "0%", opacity: 0, ease: "power3.inOut" }, // To center
53
+ };
54
+ }
55
+ export function animateCloseSlideLeft(): GSAPTweenVars {
56
+ return {
57
+ from: { x: "0%", y: "0%", opacity: 1 }, // From right
58
+ to: { x: "-100%", y: "0%", opacity: 0, ease: "power3.inOut" }, // To center
59
+ };
60
+ }
61
+
62
+ // Mask animations
63
+
64
+ export function animateMaskOpen(opacity: number): GSAPTweenVars {
65
+ return {
66
+ from: { opacity: 0 },
67
+ to: { opacity: opacity, duration: 0.5 },
68
+ };
69
+ }
70
+
71
+ export function animateMaskClosed(opacity: number): GSAPTweenVars {
72
+ return {
73
+ from: { opacity: opacity },
74
+ to: { opacity: 0, duration: 0.5 },
75
+ };
76
+ }
@@ -0,0 +1,48 @@
1
+ export interface PetalElements {
2
+ name: string;
3
+ trigger: Element;
4
+ popup: HTMLElement;
5
+ mask: Element;
6
+ slot: Element;
7
+ }
8
+
9
+ // BASE
10
+ export const ATTR_PETAL_NAME = "petal";
11
+ export const ATTR_PETAL_ELEMENT = "petal-el";
12
+
13
+ // BEHAVIOR
14
+ export const ATTR_PETAL_SHOW_ONCE = "petal-show-once"; // Regardless of other settings, only show the popup once per user session
15
+ export const ATTR_PETAL_SHOW_DELAY = "petal-show-delay"; // Time to wait before showing popup (in seconds)
16
+ export const ATTR_PETAL_SESSION_TTL = "petal-session-ttl"; // Time to keep user session (in hours)
17
+
18
+ /**-------------------------*
19
+ * POPUP
20
+ *--------------------------*/
21
+
22
+ // ELEMENTS
23
+ export const ATTR_PETAL_POPUP = "popup";
24
+ export const ATTR_PETAL_OPEN = "open";
25
+ export const ATTR_PETAL_CLOSE = "close";
26
+ export const ATTR_PETAL_MASK = "mask";
27
+ export const ATTR_PETAL_SLOT = "slot";
28
+
29
+ // MASK
30
+ export const ATTR_PETAL_MASK_OPACITY = "petal-mask-opacity";
31
+ export const ATTR_PETAL_MASK_CLOSE = "petal-mask-close";
32
+
33
+ // ANIMATIONS
34
+ export const ATTR_PETAL_ANIM_OPEN = "petal-anim-open";
35
+ export const ATTR_PETAL_ANIM_CLOSE = "petal-anim-close";
36
+ export const ATTR_PETAL_ANIM_OPEN_MOBILE = "petal-anim-open-mobile";
37
+ export const ATTR_PETAL_ANIM_CLOSE_MOBILE = "petal-anim-close-mobile";
38
+ export const ATTR_PETAL_DURATION = "petal-duration";
39
+
40
+ /**-------------------------*
41
+ * BANNER
42
+ *--------------------------*/
43
+
44
+ // ELEMENTS
45
+ export const ATTR_PETAL_BANNER = "banner";
46
+ export const ATTR_PETAL_BANNER_CLOSE = "banner-close";
47
+
48
+ export const ATTR_PETAL_BANNER_CLOSED_CLASS = "petal-hide-nav-banner";
@@ -0,0 +1,4 @@
1
+ // Device check
2
+ export function isMobile(): boolean {
3
+ return window.innerWidth <= 768;
4
+ }
@@ -0,0 +1,51 @@
1
+ // Custom Error classes for Petal
2
+ export class PetalError extends Error {
3
+ constructor(
4
+ message: string,
5
+ public readonly element?: Element,
6
+ public readonly petalName?: string
7
+ ) {
8
+ super(message);
9
+ this.name = "PetalError";
10
+ }
11
+ }
12
+
13
+ export class MissingNameError extends PetalError {
14
+ constructor(trigger: Element) {
15
+ super('Trigger is missing the "petal" attribute', trigger);
16
+ this.name = "MissingNameError";
17
+ }
18
+ }
19
+
20
+ export class MissingPopupError extends PetalError {
21
+ constructor(popupName: string, trigger: Element) {
22
+ super(`Popup with name "${popupName}" not found`, trigger, popupName);
23
+ this.name = "MissingPopupError";
24
+ }
25
+ }
26
+
27
+ export class MissingTriggerError extends PetalError {
28
+ constructor(popupName: string, trigger: Element) {
29
+ super(`Trigger error for "${popupName}"`, trigger, popupName);
30
+ this.name = "MissingTriggerError";
31
+ }
32
+ }
33
+
34
+ export class MissingMaskError extends PetalError {
35
+ constructor(popupName: string, trigger: Element) {
36
+ super(`Mask not found for "${popupName}"`, trigger, popupName);
37
+ this.name = "MissingMaskError";
38
+ }
39
+ }
40
+
41
+ export class MissingSlotError extends PetalError {
42
+ constructor(popupName: string, trigger: Element) {
43
+ super(`Slot not found for "${popupName}"`, trigger, popupName);
44
+ this.name = "MissingSlotError";
45
+ }
46
+ }
47
+
48
+ // Helper function to log errors (for backward compatibility or simple logging)
49
+ export function logPetalError(error: PetalError): void {
50
+ console.error(`[${error.name}]:`, error.message, error.element);
51
+ }
File without changes
@@ -0,0 +1,5 @@
1
+ import { ATTR_PETAL_ELEMENT } from "./attributes";
2
+
3
+ export function getAllPetalElementsOfType(el: string): NodeListOf<Element> {
4
+ return document.querySelectorAll(`[${ATTR_PETAL_ELEMENT}='${el}']`);
5
+ }
@@ -0,0 +1,54 @@
1
+ import { PetalElements } from "./attributes";
2
+
3
+ // MEMORY
4
+ type MemoryItem = "popup" | "banner";
5
+
6
+ export function storeClosedState(type: MemoryItem, petal: PetalElements) {
7
+ const { name } = petal;
8
+ const now = new Date();
9
+ sessionStorage.setItem(getMemoryKey(type, name), now.getTime().toString());
10
+ }
11
+
12
+ /**
13
+ * Check Closed State in Session Storage
14
+ * @param type The type of element (popup or banner)
15
+ * @param petal The Petal to check
16
+ * @param sessionTTLMinutes The length that the session is valid for (in minutes)
17
+ * @returns True if the element has been closed in this session and the session is still valid, false otherwise
18
+ */
19
+ export function checkClosedState(type: MemoryItem, petal: PetalElements, sessionTTLMinutes: number): boolean {
20
+ const { name } = petal;
21
+ const timestampStr = sessionStorage.getItem(getMemoryKey(type, name));
22
+ if (!timestampStr) return false;
23
+
24
+ const timestamp = parseInt(timestampStr, 10);
25
+ if (isNaN(timestamp)) {
26
+ sessionStorage.removeItem(getMemoryKey(type, name));
27
+ return false;
28
+ }
29
+
30
+ const now = new Date();
31
+ const itemTime = new Date(timestamp);
32
+ const diffMinutes = (now.getTime() - itemTime.getTime()) / (1000 * 60);
33
+
34
+ if (diffMinutes > sessionTTLMinutes) {
35
+ // Session expired
36
+ sessionStorage.removeItem(getMemoryKey(type, name));
37
+ return false;
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ // Legacy function names for backward compatibility
44
+ export function storePopupClosedState(petal: PetalElements) {
45
+ storeClosedState("popup", petal);
46
+ }
47
+
48
+ export function checkPopupClosedState(petal: PetalElements, sessionTTLMinutes: number): boolean {
49
+ return checkClosedState("popup", petal, sessionTTLMinutes);
50
+ }
51
+
52
+ function getMemoryKey(key: MemoryItem, name: string) {
53
+ return `petal_memory_${key}_${name}`;
54
+ }
package/src/petal.css ADDED
@@ -0,0 +1,11 @@
1
+ /** BANNER **/
2
+ .petal-hide-nav-banner [petal-el="banner"] {
3
+ display: none;
4
+ }
5
+
6
+ /** NAV **/
7
+
8
+ /* Disable scrolling when mobile nav is open */
9
+ body:has([petal-el="nav"] .w-nav-button.w--open):not(:has([petal-el="nav-desktop"]:not(.w-condition-invisible))) {
10
+ overflow: hidden;
11
+ }
package/src/petal.ts CHANGED
@@ -1,92 +1,5 @@
1
- import gsap from "gsap";
2
- import { animateMaskClosed, animateMaskOpen, getAnimation, getAnimationNew, isPopupAnimation, PopupAnimation } from "./animation";
3
- import { pauseVideo } from "./video";
1
+ import { initializeAllPopups } from "./popup";
2
+ import { initializeBanner } from "./banner";
4
3
 
5
- // Error types for debug logging
6
- export type ErrorType = "NO_NAME" | "NO_TRIGGER" | "NO_POPUP" | "NO_MASK" | "NO_SLOT";
7
-
8
- // Initialize popup triggers
9
- document.querySelectorAll("[petal-el='trigger'], [petal-el='mask']").forEach((trigger) => {
10
- const popupName = trigger.getAttribute("petal");
11
- if (!popupName) {
12
- logError("NO_NAME", "", trigger);
13
- return;
14
- }
15
-
16
- const popup = document.querySelector(`[petal='${popupName}'][petal-el='popup']`) as HTMLElement | null;
17
- if (!popup) {
18
- logError("NO_POPUP", popupName, trigger);
19
- return;
20
- }
21
-
22
- // Popup Elements
23
- const mask = popup.querySelector("[petal-el='mask']");
24
- const slot = popup.querySelector("[petal-el='slot']");
25
-
26
- // Mask Settings
27
- const maskOpacity = parseFloat(mask?.getAttribute("petal-mask-opacity") || "0.15");
28
-
29
- // Initialize GSAP timeline
30
- const tl = gsap.timeline();
31
-
32
- // Initialize Popup
33
- gsap.set(slot, { opacity: 0 });
34
-
35
- trigger.addEventListener("click", () => animate(popup));
36
-
37
- // Animate open/close
38
- function animate(popup: HTMLElement): void {
39
- if (!slot) console.warn("A slot was not found for this popup.");
40
-
41
- console.log(`[Petal Popup]`, {
42
- popupName,
43
- popup,
44
- slot,
45
- maskOpacity,
46
- open,
47
- });
48
-
49
- // If the popup is closed, open it
50
- if (!open) {
51
- // Animate Popup
52
- tl.set(popup, { display: "flex" });
53
- // Animate Mask
54
- if (mask) {
55
- tl.fromTo(mask, animateMaskOpen(maskOpacity).from, animateMaskOpen(maskOpacity).to, "<");
56
- }
57
- // Animate the popup
58
- const anim = getAnimationNew(popup);
59
- console.log(anim);
60
- tl.fromTo(slot, anim.from, anim.to);
61
- }
62
-
63
- // If the popup is open, close it
64
- else {
65
- // Pause any videos in the popup
66
- pauseVideo(popup);
67
- // Animate the popup
68
- const anim = getAnimationNew(popup);
69
- console.log(anim);
70
- tl.fromTo(slot, anim.from, anim.to);
71
- // Animate Mask Closed
72
- if (mask) {
73
- tl.fromTo(mask, animateMaskClosed(maskOpacity).from, animateMaskClosed(maskOpacity).to, "<");
74
- }
75
- tl.set(popup, { display: "none" });
76
- }
77
- }
78
- });
79
-
80
- // Centralized error logging
81
- function logError(error: ErrorType, popupName: string, trigger: Element): void {
82
- switch (error) {
83
- case "NO_NAME":
84
- console.error(`Trigger is missing the "petal" attribute:`, trigger);
85
- break;
86
- case "NO_POPUP":
87
- console.error(`Popup with name "${popupName}" not found for trigger:`, trigger);
88
- break;
89
- default:
90
- console.error(`Popup error [${error}] for "${popupName}":`, trigger);
91
- }
92
- }
4
+ initializeAllPopups();
5
+ initializeBanner();
package/src/popup.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { gsap } from "gsap";
2
+ import { getPopupGSAPAnimation } from "./animation";
3
+ import { MissingMaskError, MissingNameError, MissingPopupError, MissingSlotError, PetalError } from "./lib/console";
4
+ import { animateMaskOpen, animateMaskClosed } from "./lib/animations";
5
+ import { PetalElements, ATTR_PETAL_MASK_OPACITY, ATTR_PETAL_OPEN, ATTR_PETAL_CLOSE, ATTR_PETAL_MASK_CLOSE, ATTR_PETAL_ELEMENT, ATTR_PETAL_POPUP, ATTR_PETAL_NAME } from "./lib/attributes";
6
+ import { getAllPetalElementsOfType } from "./lib/helpers";
7
+ import { storePopupClosedState } from "./lib/memory";
8
+ import { pauseVideo } from "./video";
9
+
10
+ export function initializeAllPopups(): void {
11
+ const popups = getAllPetalElementsOfType(ATTR_PETAL_POPUP);
12
+
13
+ popups.forEach((popup) => {
14
+ const popupElement = popup as HTMLElement;
15
+ const mask = findPopupElement(popup, "mask") as HTMLElement | null;
16
+ const slot = findPopupElement(popup, "slot") as HTMLElement | null;
17
+
18
+ // Set slot opacity to 0
19
+ if (slot) slot.style.opacity = "0";
20
+
21
+ // Set mask opacity to 0
22
+ if (mask) mask.style.opacity = "0";
23
+
24
+ // Set popup display to none
25
+ popupElement.style.display = "none";
26
+ });
27
+ }
28
+
29
+ export function openPopup(petal: PetalElements): void {
30
+ const { name, popup, slot, mask } = petal;
31
+ const tl = gsap.timeline();
32
+
33
+ // Set Popup display to flex
34
+ tl.set(popup, { display: "flex" });
35
+ // Animate Mask open
36
+ const maskOpacity = parseFloat(mask?.getAttribute(ATTR_PETAL_MASK_OPACITY) || "0.15");
37
+ tl.fromTo(mask, animateMaskOpen(maskOpacity).from, animateMaskOpen(maskOpacity).to, "<");
38
+ // Animate Slot Open
39
+ const anim = getPopupGSAPAnimation(popup, "open");
40
+ tl.set(slot, { clearProps: "x,y,scale,transform" });
41
+ tl.fromTo(slot, anim.from, anim.to);
42
+ }
43
+
44
+ export function closePopup(petal: PetalElements): void {
45
+ const { name, popup, slot, mask } = petal;
46
+ const tl = gsap.timeline();
47
+
48
+ // VIDEO: Pause any videos inside the popup
49
+ pauseVideo(popup);
50
+ // SESSION: Store popup closed state
51
+ storePopupClosedState(petal);
52
+ // Animate the Slot Closed
53
+ const anim = getPopupGSAPAnimation(popup, "close");
54
+ tl.fromTo(slot, anim.from, anim.to);
55
+ // Animate Mask Closed
56
+ if (mask) tl.to(mask, animateMaskClosed(0).to, "<");
57
+
58
+ // Hide the Popup and clear transforms so they don't persist
59
+ tl.set(popup, { display: "none" });
60
+ tl.set(slot, { clearProps: "x,y,scale,transform" });
61
+ }
62
+
63
+ export function initializePopupTriggers(): void {
64
+ // Initialize Popup Open Triggers
65
+ const openTriggers = getAllPetalElementsOfType(ATTR_PETAL_OPEN);
66
+ forEachPopupTrigger(openTriggers, (petal) => {
67
+ const { trigger } = petal;
68
+ trigger.addEventListener("click", () => openPopup(petal));
69
+ });
70
+
71
+ // Initialize Popup Close Triggers
72
+ const closeTriggers = getAllPetalElementsOfType(ATTR_PETAL_CLOSE);
73
+ forEachPopupTrigger(closeTriggers, (petal) => {
74
+ const { trigger } = petal;
75
+ trigger.addEventListener("click", () => closePopup(petal));
76
+ });
77
+
78
+ // Initialize Mask Close Triggers (masks with petal-mask-close="true")
79
+ const popups = getAllPetalElementsOfType(ATTR_PETAL_POPUP);
80
+ popups.forEach((popup) => {
81
+ const mask = findPopupElement(popup, "mask") as HTMLElement | null;
82
+ const maskClose = popup.getAttribute(ATTR_PETAL_MASK_CLOSE) === "true";
83
+ if (mask && maskClose) {
84
+ mask.addEventListener("click", () => {
85
+ const slot = findPopupElement(popup, "slot") as HTMLElement | null;
86
+ const name = popup.getAttribute(ATTR_PETAL_NAME) || popup.id;
87
+ if (slot) {
88
+ closePopup({ name, popup: popup as HTMLElement, slot, mask, trigger: mask });
89
+ }
90
+ });
91
+ }
92
+ });
93
+ }
94
+
95
+ export function findPetal(el: Element): HTMLElement {
96
+ const popupName = el.getAttribute(ATTR_PETAL_NAME);
97
+ if (!popupName) throw new MissingNameError(el);
98
+
99
+ const popup = document.querySelector(`[${ATTR_PETAL_NAME}='${popupName}'][${ATTR_PETAL_ELEMENT}='${ATTR_PETAL_POPUP}']`) as HTMLElement | null;
100
+ if (!popup) throw new MissingPopupError(popupName, el);
101
+
102
+ return popup;
103
+ }
104
+
105
+ export function findPopupElement(popup: Element, attr: string): HTMLElement | null {
106
+ return popup.querySelector(`[${ATTR_PETAL_ELEMENT}='${attr}']`);
107
+ }
108
+
109
+ // Helper to iterate over triggers with error handling
110
+ export function forEachPopupTrigger(triggers: NodeListOf<Element>, callback: (petal: PetalElements) => void): void {
111
+ triggers.forEach((trigger) => {
112
+ try {
113
+ const popup = findPetal(trigger);
114
+ const name = trigger.getAttribute(ATTR_PETAL_NAME) || "unknown";
115
+
116
+ const mask = popup.querySelector(`[${ATTR_PETAL_ELEMENT}='mask']`) as HTMLElement;
117
+ if (!mask) throw new MissingMaskError(name, trigger);
118
+
119
+ const slot = popup.querySelector(`[${ATTR_PETAL_ELEMENT}='slot']`) as HTMLElement;
120
+ if (!slot) throw new MissingSlotError(name, trigger);
121
+
122
+ callback({ name, trigger: trigger as HTMLElement, popup, mask, slot });
123
+ } catch (error) {
124
+ if (error instanceof PetalError) {
125
+ // Log and skip invalid triggers
126
+ console.error(`[${error.name}]:`, error.message, error.element);
127
+ } else {
128
+ throw error; // Re-throw unexpected errors
129
+ }
130
+ }
131
+ });
132
+ }
package/webpack.config.js CHANGED
@@ -40,4 +40,19 @@ module.exports = {
40
40
  APP_VERSION: JSON.stringify(require('./package.json').version),
41
41
  }),
42
42
  ],
43
+ devServer: {
44
+ static: [
45
+ {
46
+ directory: path.join(__dirname, 'demo'),
47
+ },
48
+ {
49
+ directory: path.join(__dirname, 'dist'),
50
+ }
51
+ ],
52
+ compress: true,
53
+ port: 8080,
54
+ hot: true,
55
+ open: true,
56
+ watchFiles: ['src/**/*.ts', 'demo/**/*'],
57
+ },
43
58
  };