well-petal 0.0.22 → 0.0.24

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,95 +1,8 @@
1
- import gsap from "gsap";
2
- import { animateMaskClosed, animateMaskOpen, getAnimationNew } from "./animation";
3
- import { pauseVideo } from "./video";
1
+ import { initializeAllPopups, initializePopupTriggers } 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";
4
+ console.log(`🌸 Hello from Wellflow Petal v${APP_VERSION}`);
7
5
 
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
- // Get the animation
42
- const open = getComputedStyle(popup).display !== "none";
43
- const anim = getAnimationNew(popup, open);
44
- console.log(anim);
45
-
46
- console.log(`[Petal Popup]`, {
47
- popupName,
48
- popup,
49
- slot,
50
- maskOpacity,
51
- open,
52
- anim,
53
- });
54
-
55
- // If the popup is closed, open it
56
- if (!open) {
57
- // Show Popup
58
- tl.set(popup, { display: "flex" });
59
- // Animate Mask Open
60
- if (mask) {
61
- tl.fromTo(mask, animateMaskOpen(maskOpacity).from, animateMaskOpen(maskOpacity).to, "<");
62
- }
63
- // Animate the Slot Open
64
- tl.fromTo(slot, anim.from, anim.to);
65
- }
66
-
67
- // If the popup is open, close it
68
- else {
69
- // Pause any videos in the popup
70
- pauseVideo(popup);
71
- // Animate the Slot Closed
72
- tl.fromTo(slot, anim.from, anim.to);
73
- // Animate Mask Closed
74
- if (mask) {
75
- tl.fromTo(mask, animateMaskClosed(maskOpacity).from, animateMaskClosed(maskOpacity).to, "<");
76
- }
77
- // Hide the Popup
78
- tl.set(popup, { display: "none" });
79
- }
80
- }
81
- });
82
-
83
- // Centralized error logging
84
- function logError(error: ErrorType, popupName: string, trigger: Element): void {
85
- switch (error) {
86
- case "NO_NAME":
87
- console.error(`Trigger is missing the "petal" attribute:`, trigger);
88
- break;
89
- case "NO_POPUP":
90
- console.error(`Popup with name "${popupName}" not found for trigger:`, trigger);
91
- break;
92
- default:
93
- console.error(`Popup error [${error}] for "${popupName}":`, trigger);
94
- }
95
- }
6
+ initializeAllPopups();
7
+ initializePopupTriggers();
8
+ initializeBanner();
package/src/popup.ts ADDED
@@ -0,0 +1,141 @@
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
+ console.log(`🌸 Detected ${popups.length} popup(s)`);
14
+
15
+ popups.forEach((popup) => {
16
+ const popupElement = popup as HTMLElement;
17
+ const name = popup.getAttribute(ATTR_PETAL_NAME) || "unknown";
18
+ console.log(` → Popup: "${name}"`);
19
+
20
+ const mask = findPopupElement(popup, "mask") as HTMLElement | null;
21
+ const slot = findPopupElement(popup, "slot") as HTMLElement | null;
22
+
23
+ // Set slot opacity to 0
24
+ if (slot) slot.style.opacity = "0";
25
+
26
+ // Set mask opacity to 0
27
+ if (mask) mask.style.opacity = "0";
28
+
29
+ // Set popup display to none
30
+ popupElement.style.display = "none";
31
+ });
32
+ }
33
+
34
+ export function openPopup(petal: PetalElements): void {
35
+ const { name, popup, slot, mask } = petal;
36
+ const tl = gsap.timeline();
37
+
38
+ // Set Popup display to flex
39
+ tl.set(popup, { display: "flex" });
40
+ // Animate Mask open
41
+ const maskOpacity = parseFloat(mask?.getAttribute(ATTR_PETAL_MASK_OPACITY) || "0.15");
42
+ tl.fromTo(mask, animateMaskOpen(maskOpacity).from, animateMaskOpen(maskOpacity).to, "<");
43
+ // Animate Slot Open
44
+ const anim = getPopupGSAPAnimation(popup, "open");
45
+ tl.set(slot, { clearProps: "x,y,scale,transform" });
46
+ tl.fromTo(slot, anim.from, anim.to);
47
+ }
48
+
49
+ export function closePopup(petal: PetalElements): void {
50
+ const { name, popup, slot, mask } = petal;
51
+ const tl = gsap.timeline();
52
+
53
+ // VIDEO: Pause any videos inside the popup
54
+ pauseVideo(popup);
55
+ // SESSION: Store popup closed state
56
+ storePopupClosedState(petal);
57
+ // Animate the Slot Closed
58
+ const anim = getPopupGSAPAnimation(popup, "close");
59
+ tl.fromTo(slot, anim.from, anim.to);
60
+ // Animate Mask Closed
61
+ if (mask) tl.to(mask, animateMaskClosed(0).to, "<");
62
+
63
+ // Hide the Popup and clear transforms so they don't persist
64
+ tl.set(popup, { display: "none" });
65
+ tl.set(slot, { clearProps: "x,y,scale,transform" });
66
+ }
67
+
68
+ export function initializePopupTriggers(): void {
69
+ // Initialize Popup Open Triggers
70
+ const openTriggers = getAllPetalElementsOfType(ATTR_PETAL_OPEN);
71
+ console.log(`🌸 Detected ${openTriggers.length} open trigger(s)`);
72
+ forEachPopupTrigger(openTriggers, (petal) => {
73
+ const { trigger } = petal;
74
+ console.log(` → Open trigger for: "${petal.name}"`);
75
+ trigger.addEventListener("click", () => openPopup(petal));
76
+ });
77
+
78
+ // Initialize Popup Close Triggers
79
+ const closeTriggers = getAllPetalElementsOfType(ATTR_PETAL_CLOSE);
80
+ console.log(`🌸 Detected ${closeTriggers.length} close trigger(s)`);
81
+ forEachPopupTrigger(closeTriggers, (petal) => {
82
+ const { trigger } = petal;
83
+ console.log(` → Close trigger for: "${petal.name}"`);
84
+ trigger.addEventListener("click", () => closePopup(petal));
85
+ });
86
+
87
+ // Initialize Mask Close Triggers (masks with petal-mask-close="true")
88
+ const popups = getAllPetalElementsOfType(ATTR_PETAL_POPUP);
89
+ popups.forEach((popup) => {
90
+ const mask = findPopupElement(popup, "mask") as HTMLElement | null;
91
+ const maskClose = popup.getAttribute(ATTR_PETAL_MASK_CLOSE) === "true";
92
+ if (mask && maskClose) {
93
+ mask.addEventListener("click", () => {
94
+ const slot = findPopupElement(popup, "slot") as HTMLElement | null;
95
+ const name = popup.getAttribute(ATTR_PETAL_NAME) || popup.id;
96
+ if (slot) {
97
+ closePopup({ name, popup: popup as HTMLElement, slot, mask, trigger: mask });
98
+ }
99
+ });
100
+ }
101
+ });
102
+ }
103
+
104
+ export function findPetal(el: Element): HTMLElement {
105
+ const popupName = el.getAttribute(ATTR_PETAL_NAME);
106
+ if (!popupName) throw new MissingNameError(el);
107
+
108
+ const popup = document.querySelector(`[${ATTR_PETAL_NAME}='${popupName}'][${ATTR_PETAL_ELEMENT}='${ATTR_PETAL_POPUP}']`) as HTMLElement | null;
109
+ if (!popup) throw new MissingPopupError(popupName, el);
110
+
111
+ return popup;
112
+ }
113
+
114
+ export function findPopupElement(popup: Element, attr: string): HTMLElement | null {
115
+ return popup.querySelector(`[${ATTR_PETAL_ELEMENT}='${attr}']`);
116
+ }
117
+
118
+ // Helper to iterate over triggers with error handling
119
+ export function forEachPopupTrigger(triggers: NodeListOf<Element>, callback: (petal: PetalElements) => void): void {
120
+ triggers.forEach((trigger) => {
121
+ try {
122
+ const popup = findPetal(trigger);
123
+ const name = trigger.getAttribute(ATTR_PETAL_NAME) || "unknown";
124
+
125
+ const mask = popup.querySelector(`[${ATTR_PETAL_ELEMENT}='mask']`) as HTMLElement;
126
+ if (!mask) throw new MissingMaskError(name, trigger);
127
+
128
+ const slot = popup.querySelector(`[${ATTR_PETAL_ELEMENT}='slot']`) as HTMLElement;
129
+ if (!slot) throw new MissingSlotError(name, trigger);
130
+
131
+ callback({ name, trigger: trigger as HTMLElement, popup, mask, slot });
132
+ } catch (error) {
133
+ if (error instanceof PetalError) {
134
+ // Log and skip invalid triggers
135
+ console.error(`[${error.name}]:`, error.message, error.element);
136
+ } else {
137
+ throw error; // Re-throw unexpected errors
138
+ }
139
+ }
140
+ });
141
+ }
@@ -0,0 +1 @@
1
+ declare const APP_VERSION: string;
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
  };