prettier-modals 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 srdavo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Prettier Modals
2
+
3
+ A tiny JavaScript class that brings beautiful open/close animations to native `<dialog>` elements using [GSAP](https://gsap.com/) and its [Flip plugin](https://gsap.com/docs/v3/Plugins/Flip/).
4
+
5
+ The modal morphs **from** the trigger button and collapses **back into it** when closed — with elastic easing, blur, and fade effects. No frameworks, no dependencies beyond GSAP.
6
+
7
+ ## Features
8
+
9
+ - Uses the native HTML `<dialog>` element (accessible by default)
10
+ - Smooth elastic open animation with blur fade-in
11
+ - Closing animation with border-radius morph, blur, and fade-out
12
+ - Automatic style injection (no extra CSS file needed)
13
+ - Respects `prefers-reduced-motion`
14
+ - SSR-safe, ships ESM + CJS + TypeScript types
15
+ - Lightweight — GSAP is the only (peer) dependency
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install prettier-modals gsap
21
+ ```
22
+
23
+ GSAP is a **peer dependency** (`>=3.12`) — install it alongside Prettier Modals. The package ships ESM, CJS, and TypeScript types.
24
+
25
+ ## Usage
26
+
27
+ ### 1. Register the GSAP plugins
28
+
29
+ Prettier Modals imports GSAP, `Flip`, and `CustomEase` internally, but GSAP requires the plugins to be registered once in your app:
30
+
31
+ ```js
32
+ import gsap from 'gsap'
33
+ import { Flip } from 'gsap/Flip'
34
+ import { CustomEase } from 'gsap/CustomEase'
35
+
36
+ gsap.registerPlugin(Flip, CustomEase)
37
+ ```
38
+
39
+ > With a bundler (Vite, webpack, Angular CLI…) this resolves GSAP to a single instance automatically. See [Running without a bundler](#running-without-a-bundler) for the CDN setup.
40
+
41
+ ### 2. Create the instance
42
+
43
+ ```js
44
+ import { PrettyModal } from 'prettier-modals'
45
+
46
+ const prettyModal = new PrettyModal()
47
+ ```
48
+
49
+ ### 3. Add a `<dialog>` and a trigger
50
+
51
+ The trigger element is **explicit** — pass the element the modal should morph from. From an inline handler, use `this`:
52
+
53
+ ```html
54
+ <button onclick="prettyModal.open('my-modal', { trigger: this })">Open</button>
55
+
56
+ <dialog id="my-modal">
57
+ <h1>Hello world!</h1>
58
+ <button onclick="prettyModal.close('my-modal')">Close</button>
59
+ </dialog>
60
+ ```
61
+
62
+ You can pass either an **id string** or an **`HTMLElement`** for both the dialog and the trigger — handy from frameworks where you hold element references:
63
+
64
+ ```js
65
+ prettyModal.open(dialogEl, { trigger: buttonEl, anchor: 'origin' })
66
+ ```
67
+
68
+ ## API
69
+
70
+ ### `new PrettyModal(options?)`
71
+
72
+ | Option | Type | Default | Description |
73
+ |---|---|---|---|
74
+ | `anchor` | `'center' \| 'origin'` | `'center'` | Where the modal opens from. `origin` positions it near the trigger. |
75
+ | `duration` | `number` | `0.4` | Flip animation duration (seconds). |
76
+ | `ease` | `string` | elastic | `CustomEase` SVG path for the Flip tween. |
77
+ | `respectReducedMotion` | `boolean` | `true` | Skip animation when the user prefers reduced motion. |
78
+ | `onOpen` | `(dialog) => void` | — | Called after the open animation completes. |
79
+ | `onClose` | `(dialog) => void` | — | Called after the close animation completes. |
80
+
81
+ These act as defaults; any of them (plus `trigger`) can be overridden per call via the second argument of `open`/`close`.
82
+
83
+ ### Methods
84
+
85
+ | Method | Description |
86
+ |---|---|
87
+ | `open(dialogRef, options?)` | Opens the `<dialog>` (id or element), morphing from `options.trigger`. |
88
+ | `close(dialogRef, options?)` | Closes the `<dialog>`, morphing back into its trigger. |
89
+ | `destroy()` | Removes the injected `<style>` tag. Call when tearing down. |
90
+
91
+ ## How it works
92
+
93
+ 1. **Open** — Pairs the trigger and dialog with a shared `data-flip-id`, captures the trigger's position with `Flip.getState()`, calls `dialog.showModal()`, then uses `Flip.from()` to morph the dialog out of the trigger with an elastic ease.
94
+ 2. **Close** — Uses `Flip.to()` to morph the dialog back into its trigger with blur and fade, then calls `dialog.close()`.
95
+
96
+ CSS keyframe animations handle the blur/fade/border-radius effects during transitions. Styles are auto-injected on instantiation (once per page) so you don't need to import any CSS. The library is SSR-safe — it touches `document`/`window` only in the browser.
97
+
98
+ ## Demo
99
+
100
+ The demo lives in `demo/` and loads the source directly via an [import map](demo/index.html) (no build step). Because ES modules and import maps require HTTP (not `file://`), serve it with any static server:
101
+
102
+ ```bash
103
+ git clone https://github.com/antuuanyf/prettier-modals.git
104
+ cd prettier-modals
105
+
106
+ # any static server works, e.g.:
107
+ npx serve .
108
+ # or
109
+ python3 -m http.server 8000
110
+ ```
111
+
112
+ Then open `http://localhost:8000/demo/` (adjust the port) and click the buttons. The demo shows both `anchor: 'origin'` and `anchor: 'center'`.
113
+
114
+ ## Running without a bundler
115
+
116
+ In a plain `<script type="module">` setup, point an import map at GSAP's combined ESM bundle so `gsap`, `Flip`, and `CustomEase` share **one instance** (separate per-plugin CDN bundles each ship their own GSAP copy and break Flip):
117
+
118
+ ```html
119
+ <script type="importmap">
120
+ {
121
+ "imports": {
122
+ "gsap": "https://cdn.jsdelivr.net/npm/gsap@3.14.1/all.js/+esm",
123
+ "gsap/Flip": "https://cdn.jsdelivr.net/npm/gsap@3.14.1/all.js/+esm",
124
+ "gsap/CustomEase": "https://cdn.jsdelivr.net/npm/gsap@3.14.1/all.js/+esm"
125
+ }
126
+ }
127
+ </script>
128
+ ```
129
+
130
+ ## Customization
131
+
132
+ Style your `<dialog>` however you want with regular CSS. Prettier Modals only handles the animation — layout, colors, and sizing are up to you.
133
+
134
+ ```css
135
+ dialog {
136
+ border: none;
137
+ border-radius: 24px;
138
+ width: 100%;
139
+ height: 100%;
140
+ max-width: 400px;
141
+ max-height: 400px;
142
+ }
143
+ ```
144
+
145
+ ## Browser Support
146
+
147
+ Works in all modern browsers that support `<dialog>` and ES modules (Chrome, Firefox, Safari, Edge).
148
+
149
+ ## License
150
+
151
+ MIT License
152
+
153
+ Copyright (c) 2026 srdavo
154
+
155
+ Permission is hereby granted, free of charge, to any person obtaining a copy
156
+ of this software and associated documentation files (the "Software"), to deal
157
+ in the Software without restriction, including without limitation the rights
158
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
159
+ copies of the Software, and to permit persons to whom the Software is
160
+ furnished to do so, subject to the following conditions:
161
+
162
+ The above copyright notice and this permission notice shall be included in all
163
+ copies or substantial portions of the Software.
164
+
165
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
166
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
167
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
168
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
169
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
170
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
171
+ SOFTWARE.
172
+
173
+ ## Contributing
174
+
175
+ Contributions are welcome! Feel free to open an issue or submit a pull request.
176
+
177
+ ## Credits
178
+
179
+ Maintained by [Antonio Monreal Diaz](https://github.com/antuuanyf), based on the original work by [srdavo](https://github.com/srdavo). Powered by [GSAP](https://gsap.com/).
@@ -0,0 +1,62 @@
1
+ 'use strict';var gsap=require('gsap'),Flip=require('gsap/Flip'),CustomEase=require('gsap/CustomEase');var y=s=>{throw TypeError(s)};var S=(s,t,o)=>t.has(s)||y("Cannot "+o);var b=(s,t,o)=>t.has(s)?y("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(s):t.set(s,o);var l=(s,t,o)=>(S(s,t,"access private method"),o);gsap.gsap.registerPlugin(Flip.Flip,CustomEase.CustomEase);var m="pretty-modal-styles",I="M0,0 C0.305,0.206 0.116,0.567 0.3,0.8 0.394,0.921 0.491,1 1,1",i,g,x,f,w,h=class{constructor(t={}){b(this,i);this.defaults={anchor:"center",duration:.4,ease:I,respectReducedMotion:true,onOpen:null,onClose:null,...t},this.state=new WeakMap,typeof document<"u"&&(this.ease=CustomEase.CustomEase.create("pretty-modal-ease",this.defaults.ease),this.injectStyles());}open(t,o={}){let e=l(this,i,g).call(this,t);if(!e||e.open)return;let n={...this.defaults,...o},r=l(this,i,x).call(this,o.trigger);if(!r){console.warn("[PrettyModal] No trigger element found. Pass { trigger } explicitly.");return}if(this.state.get(e)?.animating)return;if(this.state.set(e,{trigger:r,anchor:n.anchor,animating:true}),l(this,i,f).call(this,n)){e.showModal(),this.state.set(e,{trigger:r,anchor:n.anchor,animating:false}),n.onOpen?.(e);return}let a=this.state.get(e).flipId||Math.random().toString(16).slice(2);r.dataset.flipId=a,e.dataset.flipId=a,e.dataset.anchor=n.anchor,this.state.get(e).flipId=a;let d=Flip.Flip.getState(r);e.showModal(),n.anchor==="origin"&&l(this,i,w).call(this,e,r),Flip.Flip.from(d,{targets:e,scale:true,ease:this.ease,toggleClass:"pretty-modal-opening",duration:n.duration,onComplete:()=>{let u=this.state.get(e);u&&(u.animating=false),n.onOpen?.(e);}});}close(t,o={}){let e=l(this,i,g).call(this,t);if(!e||!e.open)return;let n={...this.defaults,...o},r=this.state.get(e),c=r?.trigger;if(l(this,i,f).call(this,n)||!c){e.close(),e.setAttribute("style",""),r&&(r.animating=false),n.onClose?.(e);return}r&&(r.animating=true);let a=Flip.Flip.getState(c);Flip.Flip.to(a,{targets:e,scale:true,ease:this.ease,toggleClass:"pretty-modal-closing",duration:n.duration,onComplete:()=>{e.setAttribute("style",""),e.close(),r&&(r.animating=false),n.onClose?.(e);}});}destroy(){typeof document>"u"||document.getElementById(m)?.remove();}injectStyles(){if(document.getElementById(m))return;let t=`
2
+ .pretty-modal-opening {
3
+ animation: pretty-modal-opening 500ms cubic-bezier(.56,.27,0,1);
4
+ }
5
+
6
+ @keyframes pretty-modal-opening {
7
+ from { opacity: 0; filter: blur(8px); } to { opacity: 1; filter: blur(0px); }
8
+ }
9
+
10
+ .pretty-modal-closing {
11
+ animation:
12
+ pretty-modal-closing-border-radius 500ms cubic-bezier(.56,.27,0,1),
13
+ pretty-modal-closing-blur 500ms cubic-bezier(.37,.35,0,1),
14
+ pretty-modal-closing-fade 700ms cubic-bezier(.56,.27,0,1)
15
+ ;
16
+ }
17
+
18
+ @keyframes pretty-modal-closing-border-radius {
19
+ to { border-radius: 400px; }
20
+ }
21
+
22
+ @keyframes pretty-modal-closing-blur {
23
+ 0% { filter: blur(0); } 100% { filter: blur(32px); }
24
+ }
25
+
26
+ @keyframes pretty-modal-closing-fade {
27
+ from { opacity: 1; } to { opacity: 0; }
28
+ }
29
+
30
+ dialog[open]::backdrop {
31
+ background: rgba(0,0,0,0.2);
32
+ backdrop-filter: blur(2px);
33
+ }
34
+
35
+ dialog.pretty-modal-opening::backdrop {
36
+ animation: pretty-modal-backdrop-in 400ms cubic-bezier(.56,.27,0,1);
37
+ }
38
+
39
+ dialog.pretty-modal-closing::backdrop {
40
+ animation: pretty-modal-backdrop-out 400ms cubic-bezier(.56,.27,0,1) forwards;
41
+ }
42
+
43
+ @keyframes pretty-modal-backdrop-in {
44
+ from { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }
45
+ to { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }
46
+ }
47
+
48
+ @keyframes pretty-modal-backdrop-out {
49
+ from { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }
50
+ to { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ .pretty-modal-opening,
55
+ .pretty-modal-closing,
56
+ dialog.pretty-modal-opening::backdrop,
57
+ dialog.pretty-modal-closing::backdrop {
58
+ animation: none;
59
+ }
60
+ }
61
+ `,o=document.createElement("style");o.id=m,o.textContent=t,document.head.appendChild(o);}};i=new WeakSet,g=function(t){return t?typeof t=="string"?document.getElementById(t):t:null},x=function(t){let o=l(this,i,g).call(this,t);return o||(typeof event<"u"&&event?.currentTarget?event.currentTarget:null)},f=function(t){return !t.respectReducedMotion||typeof window>"u"||!window.matchMedia?false:window.matchMedia("(prefers-reduced-motion: reduce)").matches},w=function(t,o){let e=o.getBoundingClientRect(),n=window.innerWidth,r=window.innerHeight;t.style.margin="0",t.style.position="fixed",t.style.inset="auto";let c=t.getBoundingClientRect(),a=c.width,d=c.height,u=n-e.left,C=e.right,M=r-e.top,v=e.bottom;a<=u?t.style.left=`${e.left}px`:a<=C?t.style.right=`${n-e.right}px`:t.style.left=`${Math.max(0,(n-a)/2)}px`,d<=M?t.style.top=`${e.top}px`:d<=v?t.style.bottom=`${r-e.bottom}px`:t.style.top=`${Math.max(0,(r-d)/2)}px`;};exports.PrettyModal=h;//# sourceMappingURL=PrettyModal.cjs.map
62
+ //# sourceMappingURL=PrettyModal.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/PrettyModal.js"],"names":["gsap","Flip","CustomEase","STYLE_ID","EASE","_PrettyModal_instances","resolveEl_fn","resolveTrigger_fn","reducedMotion_fn","positionAtOrigin_fn","PrettyModal","options","__privateAdd","dialogRef","dialog","__privateMethod","opts","trigger","flipId","originState","e","entry","styles","styleSheet","ref","resolved","origin","originRect","vw","vh","dialogRect","dialogW","dialogH","spaceRight","spaceLeft","spaceBelow","spaceAbove"],"mappings":"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","file":"PrettyModal.cjs","sourcesContent":["import { gsap } from 'gsap'\nimport { Flip } from 'gsap/Flip'\nimport { CustomEase } from 'gsap/CustomEase'\n\ngsap.registerPlugin(Flip, CustomEase)\n\nconst STYLE_ID = 'pretty-modal-styles'\nconst EASE = 'M0,0 C0.305,0.206 0.116,0.567 0.3,0.8 0.394,0.921 0.491,1 1,1'\n\nexport class PrettyModal {\n /**\n * @param {Object} [options]\n * @param {'center'|'origin'} [options.anchor='center'] Where the modal opens from.\n * @param {number} [options.duration=0.4] Animation duration in seconds.\n * @param {string} [options.ease] CustomEase SVG path used for the Flip tween.\n * @param {boolean} [options.respectReducedMotion=true] Skip animation when the user prefers reduced motion.\n * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]\n * @param {(dialog: HTMLDialogElement) => void} [options.onClose]\n */\n constructor(options = {}) {\n this.defaults = {\n anchor: 'center',\n duration: 0.4,\n ease: EASE,\n respectReducedMotion: true,\n onOpen: null,\n onClose: null,\n ...options,\n }\n\n // Per-dialog state: trigger element + whether it is mid-animation.\n this.state = new WeakMap()\n\n if (typeof document !== 'undefined') {\n this.ease = CustomEase.create('pretty-modal-ease', this.defaults.ease)\n this.injectStyles()\n }\n }\n\n /**\n * Open a dialog, morphing from the trigger element.\n * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.\n * @param {Object} [options]\n * @param {string|HTMLElement} [options.trigger] Element to animate from. Defaults to `event.currentTarget` when called from an inline handler.\n * @param {'center'|'origin'} [options.anchor]\n * @param {number} [options.duration]\n * @param {boolean} [options.respectReducedMotion]\n * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]\n * @param {(dialog: HTMLDialogElement) => void} [options.onClose]\n */\n open(dialogRef, options = {}) {\n const dialog = this.#resolveEl(dialogRef)\n if (!dialog || dialog.open) return\n\n const opts = { ...this.defaults, ...options }\n const trigger = this.#resolveTrigger(options.trigger)\n if (!trigger) {\n console.warn('[PrettyModal] No trigger element found. Pass { trigger } explicitly.')\n return\n }\n\n const entry = this.state.get(dialog)\n if (entry?.animating) return\n\n this.state.set(dialog, { trigger, anchor: opts.anchor, animating: true })\n\n if (this.#reducedMotion(opts)) {\n dialog.showModal()\n this.state.set(dialog, { trigger, anchor: opts.anchor, animating: false })\n opts.onOpen?.(dialog)\n return\n }\n\n // Flip morphs elements that share a data-flip-id, so pair trigger and dialog.\n const flipId = this.state.get(dialog).flipId || Math.random().toString(16).slice(2)\n trigger.dataset.flipId = flipId\n dialog.dataset.flipId = flipId\n dialog.dataset.anchor = opts.anchor\n this.state.get(dialog).flipId = flipId\n\n const originState = Flip.getState(trigger)\n dialog.showModal()\n\n if (opts.anchor === 'origin') {\n this.#positionAtOrigin(dialog, trigger)\n }\n\n Flip.from(originState, {\n targets: dialog,\n scale: true,\n ease: this.ease,\n toggleClass: 'pretty-modal-opening',\n duration: opts.duration,\n onComplete: () => {\n const e = this.state.get(dialog)\n if (e) e.animating = false\n opts.onOpen?.(dialog)\n },\n })\n }\n\n /**\n * Close a dialog, morphing back into its trigger element.\n * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.\n * @param {Object} [options]\n * @param {number} [options.duration]\n * @param {boolean} [options.respectReducedMotion]\n * @param {(dialog: HTMLDialogElement) => void} [options.onClose]\n */\n close(dialogRef, options = {}) {\n const dialog = this.#resolveEl(dialogRef)\n if (!dialog || !dialog.open) return\n\n const opts = { ...this.defaults, ...options }\n const entry = this.state.get(dialog)\n const trigger = entry?.trigger\n\n if (this.#reducedMotion(opts) || !trigger) {\n dialog.close()\n dialog.setAttribute('style', '')\n if (entry) entry.animating = false\n opts.onClose?.(dialog)\n return\n }\n\n if (entry) entry.animating = true\n\n const originState = Flip.getState(trigger)\n\n Flip.to(originState, {\n targets: dialog,\n scale: true,\n ease: this.ease,\n toggleClass: 'pretty-modal-closing',\n duration: opts.duration,\n onComplete: () => {\n dialog.setAttribute('style', '')\n dialog.close()\n if (entry) entry.animating = false\n opts.onClose?.(dialog)\n },\n })\n }\n\n /** Remove injected styles. Call when tearing down. */\n destroy() {\n if (typeof document === 'undefined') return\n document.getElementById(STYLE_ID)?.remove()\n }\n\n // --- internals ---------------------------------------------------------\n\n #resolveEl(ref) {\n if (!ref) return null\n if (typeof ref === 'string') return document.getElementById(ref)\n return ref\n }\n\n #resolveTrigger(trigger) {\n const resolved = this.#resolveEl(trigger)\n if (resolved) return resolved\n // Fallback for inline onclick handlers (Chromium exposes a global event).\n if (typeof event !== 'undefined' && event?.currentTarget) return event.currentTarget\n return null\n }\n\n #reducedMotion(opts) {\n if (!opts.respectReducedMotion) return false\n if (typeof window === 'undefined' || !window.matchMedia) return false\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches\n }\n\n #positionAtOrigin(dialog, origin) {\n const originRect = origin.getBoundingClientRect()\n const vw = window.innerWidth\n const vh = window.innerHeight\n\n dialog.style.margin = '0'\n dialog.style.position = 'fixed'\n dialog.style.inset = 'auto'\n\n const dialogRect = dialog.getBoundingClientRect()\n const dialogW = dialogRect.width\n const dialogH = dialogRect.height\n\n const spaceRight = vw - originRect.left\n const spaceLeft = originRect.right\n const spaceBelow = vh - originRect.top\n const spaceAbove = originRect.bottom\n\n if (dialogW <= spaceRight) {\n dialog.style.left = `${originRect.left}px`\n } else if (dialogW <= spaceLeft) {\n dialog.style.right = `${vw - originRect.right}px`\n } else {\n dialog.style.left = `${Math.max(0, (vw - dialogW) / 2)}px`\n }\n\n if (dialogH <= spaceBelow) {\n dialog.style.top = `${originRect.top}px`\n } else if (dialogH <= spaceAbove) {\n dialog.style.bottom = `${vh - originRect.bottom}px`\n } else {\n dialog.style.top = `${Math.max(0, (vh - dialogH) / 2)}px`\n }\n }\n\n injectStyles() {\n if (document.getElementById(STYLE_ID)) return\n\n const styles = `\n .pretty-modal-opening {\n animation: pretty-modal-opening 500ms cubic-bezier(.56,.27,0,1);\n }\n\n @keyframes pretty-modal-opening {\n from { opacity: 0; filter: blur(8px); } to { opacity: 1; filter: blur(0px); }\n }\n\n .pretty-modal-closing {\n animation:\n pretty-modal-closing-border-radius 500ms cubic-bezier(.56,.27,0,1),\n pretty-modal-closing-blur 500ms cubic-bezier(.37,.35,0,1),\n pretty-modal-closing-fade 700ms cubic-bezier(.56,.27,0,1)\n ;\n }\n\n @keyframes pretty-modal-closing-border-radius {\n to { border-radius: 400px; }\n }\n\n @keyframes pretty-modal-closing-blur {\n 0% { filter: blur(0); } 100% { filter: blur(32px); }\n }\n\n @keyframes pretty-modal-closing-fade {\n from { opacity: 1; } to { opacity: 0; }\n }\n\n dialog[open]::backdrop {\n background: rgba(0,0,0,0.2);\n backdrop-filter: blur(2px);\n }\n\n dialog.pretty-modal-opening::backdrop {\n animation: pretty-modal-backdrop-in 400ms cubic-bezier(.56,.27,0,1);\n }\n\n dialog.pretty-modal-closing::backdrop {\n animation: pretty-modal-backdrop-out 400ms cubic-bezier(.56,.27,0,1) forwards;\n }\n\n @keyframes pretty-modal-backdrop-in {\n from { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }\n to { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }\n }\n\n @keyframes pretty-modal-backdrop-out {\n from { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }\n to { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pretty-modal-opening,\n .pretty-modal-closing,\n dialog.pretty-modal-opening::backdrop,\n dialog.pretty-modal-closing::backdrop {\n animation: none;\n }\n }\n `\n\n const styleSheet = document.createElement('style')\n styleSheet.id = STYLE_ID\n styleSheet.textContent = styles\n document.head.appendChild(styleSheet)\n }\n}\n"]}
@@ -0,0 +1,67 @@
1
+ declare class PrettyModal {
2
+ /**
3
+ * @param {Object} [options]
4
+ * @param {'center'|'origin'} [options.anchor='center'] Where the modal opens from.
5
+ * @param {number} [options.duration=0.4] Animation duration in seconds.
6
+ * @param {string} [options.ease] CustomEase SVG path used for the Flip tween.
7
+ * @param {boolean} [options.respectReducedMotion=true] Skip animation when the user prefers reduced motion.
8
+ * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]
9
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
10
+ */
11
+ constructor(options?: {
12
+ anchor?: "center" | "origin" | undefined;
13
+ duration?: number | undefined;
14
+ ease?: string | undefined;
15
+ respectReducedMotion?: boolean | undefined;
16
+ onOpen?: ((dialog: HTMLDialogElement) => void) | undefined;
17
+ onClose?: ((dialog: HTMLDialogElement) => void) | undefined;
18
+ });
19
+ defaults: {
20
+ anchor: string;
21
+ duration: number;
22
+ ease: string;
23
+ respectReducedMotion: boolean;
24
+ onOpen: ((dialog: HTMLDialogElement) => void) | null;
25
+ onClose: ((dialog: HTMLDialogElement) => void) | null;
26
+ };
27
+ state: WeakMap<WeakKey, any>;
28
+ ease: EaseFunction | undefined;
29
+ /**
30
+ * Open a dialog, morphing from the trigger element.
31
+ * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.
32
+ * @param {Object} [options]
33
+ * @param {string|HTMLElement} [options.trigger] Element to animate from. Defaults to `event.currentTarget` when called from an inline handler.
34
+ * @param {'center'|'origin'} [options.anchor]
35
+ * @param {number} [options.duration]
36
+ * @param {boolean} [options.respectReducedMotion]
37
+ * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]
38
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
39
+ */
40
+ open(dialogRef: string | HTMLDialogElement, options?: {
41
+ trigger?: string | HTMLElement | undefined;
42
+ anchor?: "center" | "origin" | undefined;
43
+ duration?: number | undefined;
44
+ respectReducedMotion?: boolean | undefined;
45
+ onOpen?: ((dialog: HTMLDialogElement) => void) | undefined;
46
+ onClose?: ((dialog: HTMLDialogElement) => void) | undefined;
47
+ }): void;
48
+ /**
49
+ * Close a dialog, morphing back into its trigger element.
50
+ * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.
51
+ * @param {Object} [options]
52
+ * @param {number} [options.duration]
53
+ * @param {boolean} [options.respectReducedMotion]
54
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
55
+ */
56
+ close(dialogRef: string | HTMLDialogElement, options?: {
57
+ duration?: number | undefined;
58
+ respectReducedMotion?: boolean | undefined;
59
+ onClose?: ((dialog: HTMLDialogElement) => void) | undefined;
60
+ }): void;
61
+ /** Remove injected styles. Call when tearing down. */
62
+ destroy(): void;
63
+ injectStyles(): void;
64
+ #private;
65
+ }
66
+
67
+ export { PrettyModal };
@@ -0,0 +1,67 @@
1
+ declare class PrettyModal {
2
+ /**
3
+ * @param {Object} [options]
4
+ * @param {'center'|'origin'} [options.anchor='center'] Where the modal opens from.
5
+ * @param {number} [options.duration=0.4] Animation duration in seconds.
6
+ * @param {string} [options.ease] CustomEase SVG path used for the Flip tween.
7
+ * @param {boolean} [options.respectReducedMotion=true] Skip animation when the user prefers reduced motion.
8
+ * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]
9
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
10
+ */
11
+ constructor(options?: {
12
+ anchor?: "center" | "origin" | undefined;
13
+ duration?: number | undefined;
14
+ ease?: string | undefined;
15
+ respectReducedMotion?: boolean | undefined;
16
+ onOpen?: ((dialog: HTMLDialogElement) => void) | undefined;
17
+ onClose?: ((dialog: HTMLDialogElement) => void) | undefined;
18
+ });
19
+ defaults: {
20
+ anchor: string;
21
+ duration: number;
22
+ ease: string;
23
+ respectReducedMotion: boolean;
24
+ onOpen: ((dialog: HTMLDialogElement) => void) | null;
25
+ onClose: ((dialog: HTMLDialogElement) => void) | null;
26
+ };
27
+ state: WeakMap<WeakKey, any>;
28
+ ease: EaseFunction | undefined;
29
+ /**
30
+ * Open a dialog, morphing from the trigger element.
31
+ * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.
32
+ * @param {Object} [options]
33
+ * @param {string|HTMLElement} [options.trigger] Element to animate from. Defaults to `event.currentTarget` when called from an inline handler.
34
+ * @param {'center'|'origin'} [options.anchor]
35
+ * @param {number} [options.duration]
36
+ * @param {boolean} [options.respectReducedMotion]
37
+ * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]
38
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
39
+ */
40
+ open(dialogRef: string | HTMLDialogElement, options?: {
41
+ trigger?: string | HTMLElement | undefined;
42
+ anchor?: "center" | "origin" | undefined;
43
+ duration?: number | undefined;
44
+ respectReducedMotion?: boolean | undefined;
45
+ onOpen?: ((dialog: HTMLDialogElement) => void) | undefined;
46
+ onClose?: ((dialog: HTMLDialogElement) => void) | undefined;
47
+ }): void;
48
+ /**
49
+ * Close a dialog, morphing back into its trigger element.
50
+ * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.
51
+ * @param {Object} [options]
52
+ * @param {number} [options.duration]
53
+ * @param {boolean} [options.respectReducedMotion]
54
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
55
+ */
56
+ close(dialogRef: string | HTMLDialogElement, options?: {
57
+ duration?: number | undefined;
58
+ respectReducedMotion?: boolean | undefined;
59
+ onClose?: ((dialog: HTMLDialogElement) => void) | undefined;
60
+ }): void;
61
+ /** Remove injected styles. Call when tearing down. */
62
+ destroy(): void;
63
+ injectStyles(): void;
64
+ #private;
65
+ }
66
+
67
+ export { PrettyModal };
@@ -0,0 +1,62 @@
1
+ import {gsap}from'gsap';import {Flip}from'gsap/Flip';import {CustomEase}from'gsap/CustomEase';var y=s=>{throw TypeError(s)};var S=(s,t,o)=>t.has(s)||y("Cannot "+o);var b=(s,t,o)=>t.has(s)?y("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(s):t.set(s,o);var l=(s,t,o)=>(S(s,t,"access private method"),o);gsap.registerPlugin(Flip,CustomEase);var m="pretty-modal-styles",I="M0,0 C0.305,0.206 0.116,0.567 0.3,0.8 0.394,0.921 0.491,1 1,1",i,g,x,f,w,h=class{constructor(t={}){b(this,i);this.defaults={anchor:"center",duration:.4,ease:I,respectReducedMotion:true,onOpen:null,onClose:null,...t},this.state=new WeakMap,typeof document<"u"&&(this.ease=CustomEase.create("pretty-modal-ease",this.defaults.ease),this.injectStyles());}open(t,o={}){let e=l(this,i,g).call(this,t);if(!e||e.open)return;let n={...this.defaults,...o},r=l(this,i,x).call(this,o.trigger);if(!r){console.warn("[PrettyModal] No trigger element found. Pass { trigger } explicitly.");return}if(this.state.get(e)?.animating)return;if(this.state.set(e,{trigger:r,anchor:n.anchor,animating:true}),l(this,i,f).call(this,n)){e.showModal(),this.state.set(e,{trigger:r,anchor:n.anchor,animating:false}),n.onOpen?.(e);return}let a=this.state.get(e).flipId||Math.random().toString(16).slice(2);r.dataset.flipId=a,e.dataset.flipId=a,e.dataset.anchor=n.anchor,this.state.get(e).flipId=a;let d=Flip.getState(r);e.showModal(),n.anchor==="origin"&&l(this,i,w).call(this,e,r),Flip.from(d,{targets:e,scale:true,ease:this.ease,toggleClass:"pretty-modal-opening",duration:n.duration,onComplete:()=>{let u=this.state.get(e);u&&(u.animating=false),n.onOpen?.(e);}});}close(t,o={}){let e=l(this,i,g).call(this,t);if(!e||!e.open)return;let n={...this.defaults,...o},r=this.state.get(e),c=r?.trigger;if(l(this,i,f).call(this,n)||!c){e.close(),e.setAttribute("style",""),r&&(r.animating=false),n.onClose?.(e);return}r&&(r.animating=true);let a=Flip.getState(c);Flip.to(a,{targets:e,scale:true,ease:this.ease,toggleClass:"pretty-modal-closing",duration:n.duration,onComplete:()=>{e.setAttribute("style",""),e.close(),r&&(r.animating=false),n.onClose?.(e);}});}destroy(){typeof document>"u"||document.getElementById(m)?.remove();}injectStyles(){if(document.getElementById(m))return;let t=`
2
+ .pretty-modal-opening {
3
+ animation: pretty-modal-opening 500ms cubic-bezier(.56,.27,0,1);
4
+ }
5
+
6
+ @keyframes pretty-modal-opening {
7
+ from { opacity: 0; filter: blur(8px); } to { opacity: 1; filter: blur(0px); }
8
+ }
9
+
10
+ .pretty-modal-closing {
11
+ animation:
12
+ pretty-modal-closing-border-radius 500ms cubic-bezier(.56,.27,0,1),
13
+ pretty-modal-closing-blur 500ms cubic-bezier(.37,.35,0,1),
14
+ pretty-modal-closing-fade 700ms cubic-bezier(.56,.27,0,1)
15
+ ;
16
+ }
17
+
18
+ @keyframes pretty-modal-closing-border-radius {
19
+ to { border-radius: 400px; }
20
+ }
21
+
22
+ @keyframes pretty-modal-closing-blur {
23
+ 0% { filter: blur(0); } 100% { filter: blur(32px); }
24
+ }
25
+
26
+ @keyframes pretty-modal-closing-fade {
27
+ from { opacity: 1; } to { opacity: 0; }
28
+ }
29
+
30
+ dialog[open]::backdrop {
31
+ background: rgba(0,0,0,0.2);
32
+ backdrop-filter: blur(2px);
33
+ }
34
+
35
+ dialog.pretty-modal-opening::backdrop {
36
+ animation: pretty-modal-backdrop-in 400ms cubic-bezier(.56,.27,0,1);
37
+ }
38
+
39
+ dialog.pretty-modal-closing::backdrop {
40
+ animation: pretty-modal-backdrop-out 400ms cubic-bezier(.56,.27,0,1) forwards;
41
+ }
42
+
43
+ @keyframes pretty-modal-backdrop-in {
44
+ from { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }
45
+ to { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }
46
+ }
47
+
48
+ @keyframes pretty-modal-backdrop-out {
49
+ from { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }
50
+ to { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ .pretty-modal-opening,
55
+ .pretty-modal-closing,
56
+ dialog.pretty-modal-opening::backdrop,
57
+ dialog.pretty-modal-closing::backdrop {
58
+ animation: none;
59
+ }
60
+ }
61
+ `,o=document.createElement("style");o.id=m,o.textContent=t,document.head.appendChild(o);}};i=new WeakSet,g=function(t){return t?typeof t=="string"?document.getElementById(t):t:null},x=function(t){let o=l(this,i,g).call(this,t);return o||(typeof event<"u"&&event?.currentTarget?event.currentTarget:null)},f=function(t){return !t.respectReducedMotion||typeof window>"u"||!window.matchMedia?false:window.matchMedia("(prefers-reduced-motion: reduce)").matches},w=function(t,o){let e=o.getBoundingClientRect(),n=window.innerWidth,r=window.innerHeight;t.style.margin="0",t.style.position="fixed",t.style.inset="auto";let c=t.getBoundingClientRect(),a=c.width,d=c.height,u=n-e.left,C=e.right,M=r-e.top,v=e.bottom;a<=u?t.style.left=`${e.left}px`:a<=C?t.style.right=`${n-e.right}px`:t.style.left=`${Math.max(0,(n-a)/2)}px`,d<=M?t.style.top=`${e.top}px`:d<=v?t.style.bottom=`${r-e.bottom}px`:t.style.top=`${Math.max(0,(r-d)/2)}px`;};export{h as PrettyModal};//# sourceMappingURL=PrettyModal.js.map
62
+ //# sourceMappingURL=PrettyModal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/PrettyModal.js"],"names":["gsap","Flip","CustomEase","STYLE_ID","EASE","_PrettyModal_instances","resolveEl_fn","resolveTrigger_fn","reducedMotion_fn","positionAtOrigin_fn","PrettyModal","options","__privateAdd","dialogRef","dialog","__privateMethod","opts","trigger","flipId","originState","e","entry","styles","styleSheet","ref","resolved","origin","originRect","vw","vh","dialogRect","dialogW","dialogH","spaceRight","spaceLeft","spaceBelow","spaceAbove"],"mappings":"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","file":"PrettyModal.js","sourcesContent":["import { gsap } from 'gsap'\nimport { Flip } from 'gsap/Flip'\nimport { CustomEase } from 'gsap/CustomEase'\n\ngsap.registerPlugin(Flip, CustomEase)\n\nconst STYLE_ID = 'pretty-modal-styles'\nconst EASE = 'M0,0 C0.305,0.206 0.116,0.567 0.3,0.8 0.394,0.921 0.491,1 1,1'\n\nexport class PrettyModal {\n /**\n * @param {Object} [options]\n * @param {'center'|'origin'} [options.anchor='center'] Where the modal opens from.\n * @param {number} [options.duration=0.4] Animation duration in seconds.\n * @param {string} [options.ease] CustomEase SVG path used for the Flip tween.\n * @param {boolean} [options.respectReducedMotion=true] Skip animation when the user prefers reduced motion.\n * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]\n * @param {(dialog: HTMLDialogElement) => void} [options.onClose]\n */\n constructor(options = {}) {\n this.defaults = {\n anchor: 'center',\n duration: 0.4,\n ease: EASE,\n respectReducedMotion: true,\n onOpen: null,\n onClose: null,\n ...options,\n }\n\n // Per-dialog state: trigger element + whether it is mid-animation.\n this.state = new WeakMap()\n\n if (typeof document !== 'undefined') {\n this.ease = CustomEase.create('pretty-modal-ease', this.defaults.ease)\n this.injectStyles()\n }\n }\n\n /**\n * Open a dialog, morphing from the trigger element.\n * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.\n * @param {Object} [options]\n * @param {string|HTMLElement} [options.trigger] Element to animate from. Defaults to `event.currentTarget` when called from an inline handler.\n * @param {'center'|'origin'} [options.anchor]\n * @param {number} [options.duration]\n * @param {boolean} [options.respectReducedMotion]\n * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]\n * @param {(dialog: HTMLDialogElement) => void} [options.onClose]\n */\n open(dialogRef, options = {}) {\n const dialog = this.#resolveEl(dialogRef)\n if (!dialog || dialog.open) return\n\n const opts = { ...this.defaults, ...options }\n const trigger = this.#resolveTrigger(options.trigger)\n if (!trigger) {\n console.warn('[PrettyModal] No trigger element found. Pass { trigger } explicitly.')\n return\n }\n\n const entry = this.state.get(dialog)\n if (entry?.animating) return\n\n this.state.set(dialog, { trigger, anchor: opts.anchor, animating: true })\n\n if (this.#reducedMotion(opts)) {\n dialog.showModal()\n this.state.set(dialog, { trigger, anchor: opts.anchor, animating: false })\n opts.onOpen?.(dialog)\n return\n }\n\n // Flip morphs elements that share a data-flip-id, so pair trigger and dialog.\n const flipId = this.state.get(dialog).flipId || Math.random().toString(16).slice(2)\n trigger.dataset.flipId = flipId\n dialog.dataset.flipId = flipId\n dialog.dataset.anchor = opts.anchor\n this.state.get(dialog).flipId = flipId\n\n const originState = Flip.getState(trigger)\n dialog.showModal()\n\n if (opts.anchor === 'origin') {\n this.#positionAtOrigin(dialog, trigger)\n }\n\n Flip.from(originState, {\n targets: dialog,\n scale: true,\n ease: this.ease,\n toggleClass: 'pretty-modal-opening',\n duration: opts.duration,\n onComplete: () => {\n const e = this.state.get(dialog)\n if (e) e.animating = false\n opts.onOpen?.(dialog)\n },\n })\n }\n\n /**\n * Close a dialog, morphing back into its trigger element.\n * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.\n * @param {Object} [options]\n * @param {number} [options.duration]\n * @param {boolean} [options.respectReducedMotion]\n * @param {(dialog: HTMLDialogElement) => void} [options.onClose]\n */\n close(dialogRef, options = {}) {\n const dialog = this.#resolveEl(dialogRef)\n if (!dialog || !dialog.open) return\n\n const opts = { ...this.defaults, ...options }\n const entry = this.state.get(dialog)\n const trigger = entry?.trigger\n\n if (this.#reducedMotion(opts) || !trigger) {\n dialog.close()\n dialog.setAttribute('style', '')\n if (entry) entry.animating = false\n opts.onClose?.(dialog)\n return\n }\n\n if (entry) entry.animating = true\n\n const originState = Flip.getState(trigger)\n\n Flip.to(originState, {\n targets: dialog,\n scale: true,\n ease: this.ease,\n toggleClass: 'pretty-modal-closing',\n duration: opts.duration,\n onComplete: () => {\n dialog.setAttribute('style', '')\n dialog.close()\n if (entry) entry.animating = false\n opts.onClose?.(dialog)\n },\n })\n }\n\n /** Remove injected styles. Call when tearing down. */\n destroy() {\n if (typeof document === 'undefined') return\n document.getElementById(STYLE_ID)?.remove()\n }\n\n // --- internals ---------------------------------------------------------\n\n #resolveEl(ref) {\n if (!ref) return null\n if (typeof ref === 'string') return document.getElementById(ref)\n return ref\n }\n\n #resolveTrigger(trigger) {\n const resolved = this.#resolveEl(trigger)\n if (resolved) return resolved\n // Fallback for inline onclick handlers (Chromium exposes a global event).\n if (typeof event !== 'undefined' && event?.currentTarget) return event.currentTarget\n return null\n }\n\n #reducedMotion(opts) {\n if (!opts.respectReducedMotion) return false\n if (typeof window === 'undefined' || !window.matchMedia) return false\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches\n }\n\n #positionAtOrigin(dialog, origin) {\n const originRect = origin.getBoundingClientRect()\n const vw = window.innerWidth\n const vh = window.innerHeight\n\n dialog.style.margin = '0'\n dialog.style.position = 'fixed'\n dialog.style.inset = 'auto'\n\n const dialogRect = dialog.getBoundingClientRect()\n const dialogW = dialogRect.width\n const dialogH = dialogRect.height\n\n const spaceRight = vw - originRect.left\n const spaceLeft = originRect.right\n const spaceBelow = vh - originRect.top\n const spaceAbove = originRect.bottom\n\n if (dialogW <= spaceRight) {\n dialog.style.left = `${originRect.left}px`\n } else if (dialogW <= spaceLeft) {\n dialog.style.right = `${vw - originRect.right}px`\n } else {\n dialog.style.left = `${Math.max(0, (vw - dialogW) / 2)}px`\n }\n\n if (dialogH <= spaceBelow) {\n dialog.style.top = `${originRect.top}px`\n } else if (dialogH <= spaceAbove) {\n dialog.style.bottom = `${vh - originRect.bottom}px`\n } else {\n dialog.style.top = `${Math.max(0, (vh - dialogH) / 2)}px`\n }\n }\n\n injectStyles() {\n if (document.getElementById(STYLE_ID)) return\n\n const styles = `\n .pretty-modal-opening {\n animation: pretty-modal-opening 500ms cubic-bezier(.56,.27,0,1);\n }\n\n @keyframes pretty-modal-opening {\n from { opacity: 0; filter: blur(8px); } to { opacity: 1; filter: blur(0px); }\n }\n\n .pretty-modal-closing {\n animation:\n pretty-modal-closing-border-radius 500ms cubic-bezier(.56,.27,0,1),\n pretty-modal-closing-blur 500ms cubic-bezier(.37,.35,0,1),\n pretty-modal-closing-fade 700ms cubic-bezier(.56,.27,0,1)\n ;\n }\n\n @keyframes pretty-modal-closing-border-radius {\n to { border-radius: 400px; }\n }\n\n @keyframes pretty-modal-closing-blur {\n 0% { filter: blur(0); } 100% { filter: blur(32px); }\n }\n\n @keyframes pretty-modal-closing-fade {\n from { opacity: 1; } to { opacity: 0; }\n }\n\n dialog[open]::backdrop {\n background: rgba(0,0,0,0.2);\n backdrop-filter: blur(2px);\n }\n\n dialog.pretty-modal-opening::backdrop {\n animation: pretty-modal-backdrop-in 400ms cubic-bezier(.56,.27,0,1);\n }\n\n dialog.pretty-modal-closing::backdrop {\n animation: pretty-modal-backdrop-out 400ms cubic-bezier(.56,.27,0,1) forwards;\n }\n\n @keyframes pretty-modal-backdrop-in {\n from { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }\n to { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }\n }\n\n @keyframes pretty-modal-backdrop-out {\n from { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }\n to { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pretty-modal-opening,\n .pretty-modal-closing,\n dialog.pretty-modal-opening::backdrop,\n dialog.pretty-modal-closing::backdrop {\n animation: none;\n }\n }\n `\n\n const styleSheet = document.createElement('style')\n styleSheet.id = STYLE_ID\n styleSheet.textContent = styles\n document.head.appendChild(styleSheet)\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "prettier-modals",
3
+ "version": "0.1.0",
4
+ "description": "Beautiful open/close animations for native <dialog> elements, powered by GSAP Flip.",
5
+ "type": "module",
6
+ "main": "./dist/PrettyModal.cjs",
7
+ "module": "./dist/PrettyModal.js",
8
+ "types": "./dist/PrettyModal.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/PrettyModal.d.ts",
12
+ "import": "./dist/PrettyModal.js",
13
+ "require": "./dist/PrettyModal.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "sideEffects": false,
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "modal",
31
+ "dialog",
32
+ "gsap",
33
+ "flip",
34
+ "animation",
35
+ "vanilla"
36
+ ],
37
+ "author": "Antonio Monreal Diaz",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/antuuanyf/prettier-modals.git"
42
+ },
43
+ "peerDependencies": {
44
+ "gsap": ">=3.12"
45
+ },
46
+ "devDependencies": {
47
+ "gsap": "^3.15.0",
48
+ "happy-dom": "^20.9.0",
49
+ "tsup": "^8.5.0",
50
+ "typescript": "^5.7.0",
51
+ "vitest": "^4.1.7"
52
+ }
53
+ }
@@ -0,0 +1,278 @@
1
+ import { gsap } from 'gsap'
2
+ import { Flip } from 'gsap/Flip'
3
+ import { CustomEase } from 'gsap/CustomEase'
4
+
5
+ gsap.registerPlugin(Flip, CustomEase)
6
+
7
+ const STYLE_ID = 'pretty-modal-styles'
8
+ const EASE = 'M0,0 C0.305,0.206 0.116,0.567 0.3,0.8 0.394,0.921 0.491,1 1,1'
9
+
10
+ export class PrettyModal {
11
+ /**
12
+ * @param {Object} [options]
13
+ * @param {'center'|'origin'} [options.anchor='center'] Where the modal opens from.
14
+ * @param {number} [options.duration=0.4] Animation duration in seconds.
15
+ * @param {string} [options.ease] CustomEase SVG path used for the Flip tween.
16
+ * @param {boolean} [options.respectReducedMotion=true] Skip animation when the user prefers reduced motion.
17
+ * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]
18
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
19
+ */
20
+ constructor(options = {}) {
21
+ this.defaults = {
22
+ anchor: 'center',
23
+ duration: 0.4,
24
+ ease: EASE,
25
+ respectReducedMotion: true,
26
+ onOpen: null,
27
+ onClose: null,
28
+ ...options,
29
+ }
30
+
31
+ // Per-dialog state: trigger element + whether it is mid-animation.
32
+ this.state = new WeakMap()
33
+
34
+ if (typeof document !== 'undefined') {
35
+ this.ease = CustomEase.create('pretty-modal-ease', this.defaults.ease)
36
+ this.injectStyles()
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Open a dialog, morphing from the trigger element.
42
+ * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.
43
+ * @param {Object} [options]
44
+ * @param {string|HTMLElement} [options.trigger] Element to animate from. Defaults to `event.currentTarget` when called from an inline handler.
45
+ * @param {'center'|'origin'} [options.anchor]
46
+ * @param {number} [options.duration]
47
+ * @param {boolean} [options.respectReducedMotion]
48
+ * @param {(dialog: HTMLDialogElement) => void} [options.onOpen]
49
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
50
+ */
51
+ open(dialogRef, options = {}) {
52
+ const dialog = this.#resolveEl(dialogRef)
53
+ if (!dialog || dialog.open) return
54
+
55
+ const opts = { ...this.defaults, ...options }
56
+ const trigger = this.#resolveTrigger(options.trigger)
57
+ if (!trigger) {
58
+ console.warn('[PrettyModal] No trigger element found. Pass { trigger } explicitly.')
59
+ return
60
+ }
61
+
62
+ const entry = this.state.get(dialog)
63
+ if (entry?.animating) return
64
+
65
+ this.state.set(dialog, { trigger, anchor: opts.anchor, animating: true })
66
+
67
+ if (this.#reducedMotion(opts)) {
68
+ dialog.showModal()
69
+ this.state.set(dialog, { trigger, anchor: opts.anchor, animating: false })
70
+ opts.onOpen?.(dialog)
71
+ return
72
+ }
73
+
74
+ // Flip morphs elements that share a data-flip-id, so pair trigger and dialog.
75
+ const flipId = this.state.get(dialog).flipId || Math.random().toString(16).slice(2)
76
+ trigger.dataset.flipId = flipId
77
+ dialog.dataset.flipId = flipId
78
+ dialog.dataset.anchor = opts.anchor
79
+ this.state.get(dialog).flipId = flipId
80
+
81
+ const originState = Flip.getState(trigger)
82
+ dialog.showModal()
83
+
84
+ if (opts.anchor === 'origin') {
85
+ this.#positionAtOrigin(dialog, trigger)
86
+ }
87
+
88
+ Flip.from(originState, {
89
+ targets: dialog,
90
+ scale: true,
91
+ ease: this.ease,
92
+ toggleClass: 'pretty-modal-opening',
93
+ duration: opts.duration,
94
+ onComplete: () => {
95
+ const e = this.state.get(dialog)
96
+ if (e) e.animating = false
97
+ opts.onOpen?.(dialog)
98
+ },
99
+ })
100
+ }
101
+
102
+ /**
103
+ * Close a dialog, morphing back into its trigger element.
104
+ * @param {string|HTMLDialogElement} dialogRef Dialog element or its id.
105
+ * @param {Object} [options]
106
+ * @param {number} [options.duration]
107
+ * @param {boolean} [options.respectReducedMotion]
108
+ * @param {(dialog: HTMLDialogElement) => void} [options.onClose]
109
+ */
110
+ close(dialogRef, options = {}) {
111
+ const dialog = this.#resolveEl(dialogRef)
112
+ if (!dialog || !dialog.open) return
113
+
114
+ const opts = { ...this.defaults, ...options }
115
+ const entry = this.state.get(dialog)
116
+ const trigger = entry?.trigger
117
+
118
+ if (this.#reducedMotion(opts) || !trigger) {
119
+ dialog.close()
120
+ dialog.setAttribute('style', '')
121
+ if (entry) entry.animating = false
122
+ opts.onClose?.(dialog)
123
+ return
124
+ }
125
+
126
+ if (entry) entry.animating = true
127
+
128
+ const originState = Flip.getState(trigger)
129
+
130
+ Flip.to(originState, {
131
+ targets: dialog,
132
+ scale: true,
133
+ ease: this.ease,
134
+ toggleClass: 'pretty-modal-closing',
135
+ duration: opts.duration,
136
+ onComplete: () => {
137
+ dialog.setAttribute('style', '')
138
+ dialog.close()
139
+ if (entry) entry.animating = false
140
+ opts.onClose?.(dialog)
141
+ },
142
+ })
143
+ }
144
+
145
+ /** Remove injected styles. Call when tearing down. */
146
+ destroy() {
147
+ if (typeof document === 'undefined') return
148
+ document.getElementById(STYLE_ID)?.remove()
149
+ }
150
+
151
+ // --- internals ---------------------------------------------------------
152
+
153
+ #resolveEl(ref) {
154
+ if (!ref) return null
155
+ if (typeof ref === 'string') return document.getElementById(ref)
156
+ return ref
157
+ }
158
+
159
+ #resolveTrigger(trigger) {
160
+ const resolved = this.#resolveEl(trigger)
161
+ if (resolved) return resolved
162
+ // Fallback for inline onclick handlers (Chromium exposes a global event).
163
+ if (typeof event !== 'undefined' && event?.currentTarget) return event.currentTarget
164
+ return null
165
+ }
166
+
167
+ #reducedMotion(opts) {
168
+ if (!opts.respectReducedMotion) return false
169
+ if (typeof window === 'undefined' || !window.matchMedia) return false
170
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches
171
+ }
172
+
173
+ #positionAtOrigin(dialog, origin) {
174
+ const originRect = origin.getBoundingClientRect()
175
+ const vw = window.innerWidth
176
+ const vh = window.innerHeight
177
+
178
+ dialog.style.margin = '0'
179
+ dialog.style.position = 'fixed'
180
+ dialog.style.inset = 'auto'
181
+
182
+ const dialogRect = dialog.getBoundingClientRect()
183
+ const dialogW = dialogRect.width
184
+ const dialogH = dialogRect.height
185
+
186
+ const spaceRight = vw - originRect.left
187
+ const spaceLeft = originRect.right
188
+ const spaceBelow = vh - originRect.top
189
+ const spaceAbove = originRect.bottom
190
+
191
+ if (dialogW <= spaceRight) {
192
+ dialog.style.left = `${originRect.left}px`
193
+ } else if (dialogW <= spaceLeft) {
194
+ dialog.style.right = `${vw - originRect.right}px`
195
+ } else {
196
+ dialog.style.left = `${Math.max(0, (vw - dialogW) / 2)}px`
197
+ }
198
+
199
+ if (dialogH <= spaceBelow) {
200
+ dialog.style.top = `${originRect.top}px`
201
+ } else if (dialogH <= spaceAbove) {
202
+ dialog.style.bottom = `${vh - originRect.bottom}px`
203
+ } else {
204
+ dialog.style.top = `${Math.max(0, (vh - dialogH) / 2)}px`
205
+ }
206
+ }
207
+
208
+ injectStyles() {
209
+ if (document.getElementById(STYLE_ID)) return
210
+
211
+ const styles = `
212
+ .pretty-modal-opening {
213
+ animation: pretty-modal-opening 500ms cubic-bezier(.56,.27,0,1);
214
+ }
215
+
216
+ @keyframes pretty-modal-opening {
217
+ from { opacity: 0; filter: blur(8px); } to { opacity: 1; filter: blur(0px); }
218
+ }
219
+
220
+ .pretty-modal-closing {
221
+ animation:
222
+ pretty-modal-closing-border-radius 500ms cubic-bezier(.56,.27,0,1),
223
+ pretty-modal-closing-blur 500ms cubic-bezier(.37,.35,0,1),
224
+ pretty-modal-closing-fade 700ms cubic-bezier(.56,.27,0,1)
225
+ ;
226
+ }
227
+
228
+ @keyframes pretty-modal-closing-border-radius {
229
+ to { border-radius: 400px; }
230
+ }
231
+
232
+ @keyframes pretty-modal-closing-blur {
233
+ 0% { filter: blur(0); } 100% { filter: blur(32px); }
234
+ }
235
+
236
+ @keyframes pretty-modal-closing-fade {
237
+ from { opacity: 1; } to { opacity: 0; }
238
+ }
239
+
240
+ dialog[open]::backdrop {
241
+ background: rgba(0,0,0,0.2);
242
+ backdrop-filter: blur(2px);
243
+ }
244
+
245
+ dialog.pretty-modal-opening::backdrop {
246
+ animation: pretty-modal-backdrop-in 400ms cubic-bezier(.56,.27,0,1);
247
+ }
248
+
249
+ dialog.pretty-modal-closing::backdrop {
250
+ animation: pretty-modal-backdrop-out 400ms cubic-bezier(.56,.27,0,1) forwards;
251
+ }
252
+
253
+ @keyframes pretty-modal-backdrop-in {
254
+ from { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }
255
+ to { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }
256
+ }
257
+
258
+ @keyframes pretty-modal-backdrop-out {
259
+ from { background: rgba(0,0,0,0.2); backdrop-filter: blur(2px); }
260
+ to { background: rgba(0,0,0,0); backdrop-filter: blur(0px); }
261
+ }
262
+
263
+ @media (prefers-reduced-motion: reduce) {
264
+ .pretty-modal-opening,
265
+ .pretty-modal-closing,
266
+ dialog.pretty-modal-opening::backdrop,
267
+ dialog.pretty-modal-closing::backdrop {
268
+ animation: none;
269
+ }
270
+ }
271
+ `
272
+
273
+ const styleSheet = document.createElement('style')
274
+ styleSheet.id = STYLE_ID
275
+ styleSheet.textContent = styles
276
+ document.head.appendChild(styleSheet)
277
+ }
278
+ }