@stfrigerio/sito-template 0.1.80 → 0.1.81

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.
@@ -18,44 +18,10 @@ export interface ModalProps {
18
18
  actions?: ReactNode;
19
19
  /** When true, the body is padded; when false, the body is flush to the edges */
20
20
  padding?: boolean;
21
+ /** How the modal renders on narrow viewports. `sheet` slides from the bottom; `center` stays centered. Defaults to `sheet`. */
22
+ mobileVariant?: 'sheet' | 'center';
23
+ /** When the modal is in sheet mode on mobile, allow the user to swipe the header down to dismiss. Defaults to true. */
24
+ draggable?: boolean;
21
25
  }
22
- /**
23
- * Modal Component
24
- *
25
- * @component
26
- * @description
27
- * An accessible overlay dialog with a backdrop, title header, optional
28
- * header actions, and a flexible body. Renders into a React portal on
29
- * `document.body`, animates in/out with Framer Motion, closes on Escape
30
- * key or backdrop click, and clicks inside the dialog don't propagate
31
- * to the backdrop.
32
- *
33
- * @example
34
- * // Basic usage
35
- * <Modal open={isOpen} title="Confirm" onClose={() => setIsOpen(false)}>
36
- * <p>Are you sure?</p>
37
- * </Modal>
38
- *
39
- * @example
40
- * // With header actions and wide size
41
- * <Modal
42
- * open={isOpen}
43
- * title="Edit profile"
44
- * size="wide"
45
- * actions={<button onClick={save}>Save</button>}
46
- * onClose={handleClose}
47
- * >
48
- * <ProfileForm />
49
- * </Modal>
50
- *
51
- * @example
52
- * // Flush body (no padding) for editors and full-bleed content
53
- * <Modal open={isOpen} title="Document" padding={false} onClose={handleClose}>
54
- * <DocEditor content={content} onSave={save} />
55
- * </Modal>
56
- *
57
- * @param {ModalProps} props - The props for the Modal component
58
- * @returns {React.ReactPortal | null} A portal rendering the modal, or null on the server
59
- */
60
26
  export declare const Modal: React.FC<ModalProps>;
61
27
  //# sourceMappingURL=Modal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../../src/components/atoms/Modal/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAa,MAAM,OAAO,CAAC;AAMpD;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,6CAA6C;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,qGAAqG;IACrG,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,yBAAyB;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,gGAAgG;IAChG,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACtC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,CAsEtC,CAAC"}
1
+ {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../../src/components/atoms/Modal/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAA+B,MAAM,OAAO,CAAC;AAMtE;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,6CAA6C;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,qGAAqG;IACrG,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,yBAAyB;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,gGAAgG;IAChG,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACtC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+HAA+H;IAC/H,aAAa,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACnC,uHAAuH;IACvH,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAqBD,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,CA+GtC,CAAC"}
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { motion, AnimatePresence, useMotionValue, useTransform, animate, LayoutGroup } from 'framer-motion';
2
+ import { motion, AnimatePresence, useDragControls, useMotionValue, useTransform, animate, LayoutGroup } from 'framer-motion';
3
3
  import React, { useRef, useEffect, useCallback, useState, useMemo, createContext, useContext, Fragment as Fragment$1, memo } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { X, Calendar as Calendar$1, ChevronDown, Search, Check, Edit, Folder, Users, Book, MessageSquare, UserPlus, Sun, Moon, Info, Github, SquareKanban, ChevronRight, Plus, Loader2, AlertTriangle, Play, Brain, FolderSearch, FilePlus, Pencil, FileText, Terminal, Trash2, GripVertical, Download, Menu, ChevronLeft, Maximize, Share2, Pause } from 'lucide-react';
@@ -720,47 +720,29 @@ const EmptyState = ({ icon, title, message, action, size = 'default', }) => {
720
720
  return (jsxs("div", { className: wrapperClass, children: [icon && jsx("div", { className: styles$z.icon, children: icon }), title && jsx("h3", { className: styles$z.title, children: title }), jsx("p", { className: styles$z.message, children: message }), action && jsx("div", { className: styles$z.action, children: action })] }));
721
721
  };
722
722
 
723
- var styles$y = {"backdrop":"Modal-module_backdrop__yOx-a","dialog":"Modal-module_dialog__yEXTq","dialogCompact":"Modal-module_dialogCompact__z1Wxp","dialogWide":"Modal-module_dialogWide__9PTXK","header":"Modal-module_header__NHHKd","title":"Modal-module_title__i3R0x","headerActions":"Modal-module_headerActions__g28UN","closeButton":"Modal-module_closeButton__siC-1","body":"Modal-module_body__U7jvM","bodyFlush":"Modal-module_bodyFlush__wtk3q"};
723
+ var styles$y = {"backdrop":"Modal-module_backdrop__yOx-a","dialog":"Modal-module_dialog__yEXTq","dialogCompact":"Modal-module_dialogCompact__z1Wxp","dialogWide":"Modal-module_dialogWide__9PTXK","header":"Modal-module_header__NHHKd","title":"Modal-module_title__i3R0x","headerActions":"Modal-module_headerActions__g28UN","closeButton":"Modal-module_closeButton__siC-1","body":"Modal-module_body__U7jvM","bodyFlush":"Modal-module_bodyFlush__wtk3q","backdropSheet":"Modal-module_backdropSheet__o9kW3","dialogSheet":"Modal-module_dialogSheet__EjpwP","grabBar":"Modal-module_grabBar__qhl9f"};
724
724
 
725
- /**
726
- * Modal Component
727
- *
728
- * @component
729
- * @description
730
- * An accessible overlay dialog with a backdrop, title header, optional
731
- * header actions, and a flexible body. Renders into a React portal on
732
- * `document.body`, animates in/out with Framer Motion, closes on Escape
733
- * key or backdrop click, and clicks inside the dialog don't propagate
734
- * to the backdrop.
735
- *
736
- * @example
737
- * // Basic usage
738
- * <Modal open={isOpen} title="Confirm" onClose={() => setIsOpen(false)}>
739
- * <p>Are you sure?</p>
740
- * </Modal>
741
- *
742
- * @example
743
- * // With header actions and wide size
744
- * <Modal
745
- * open={isOpen}
746
- * title="Edit profile"
747
- * size="wide"
748
- * actions={<button onClick={save}>Save</button>}
749
- * onClose={handleClose}
750
- * >
751
- * <ProfileForm />
752
- * </Modal>
753
- *
754
- * @example
755
- * // Flush body (no padding) for editors and full-bleed content
756
- * <Modal open={isOpen} title="Document" padding={false} onClose={handleClose}>
757
- * <DocEditor content={content} onSave={save} />
758
- * </Modal>
759
- *
760
- * @param {ModalProps} props - The props for the Modal component
761
- * @returns {React.ReactPortal | null} A portal rendering the modal, or null on the server
762
- */
763
- const Modal = ({ open, title, onClose, children, size = 'default', actions, padding = true, }) => {
725
+ const MOBILE_BREAKPOINT = '(max-width: 640px)';
726
+ const useIsMobile = () => {
727
+ const [isMobile, setIsMobile] = useState(() => {
728
+ if (typeof window === 'undefined')
729
+ return false;
730
+ return window.matchMedia(MOBILE_BREAKPOINT).matches;
731
+ });
732
+ useEffect(() => {
733
+ if (typeof window === 'undefined')
734
+ return;
735
+ const mq = window.matchMedia(MOBILE_BREAKPOINT);
736
+ const onChange = (e) => setIsMobile(e.matches);
737
+ mq.addEventListener('change', onChange);
738
+ return () => mq.removeEventListener('change', onChange);
739
+ }, []);
740
+ return isMobile;
741
+ };
742
+ const Modal = ({ open, title, onClose, children, size = 'default', actions, padding = true, mobileVariant = 'sheet', draggable = true, }) => {
743
+ const isMobile = useIsMobile();
744
+ const dragControls = useDragControls();
745
+ const headerRef = useRef(null);
764
746
  useEffect(() => {
765
747
  if (!open)
766
748
  return;
@@ -773,14 +755,38 @@ const Modal = ({ open, title, onClose, children, size = 'default', actions, padd
773
755
  }, [open, onClose]);
774
756
  if (typeof document === 'undefined')
775
757
  return null;
758
+ const isSheet = mobileVariant === 'sheet' && isMobile;
759
+ const isDraggable = isSheet && draggable;
760
+ const backdropClass = [styles$y.backdrop, isSheet && styles$y.backdropSheet]
761
+ .filter(Boolean)
762
+ .join(' ');
776
763
  const dialogClass = [
777
764
  styles$y.dialog,
778
765
  size === 'compact' && styles$y.dialogCompact,
779
766
  size === 'wide' && styles$y.dialogWide,
767
+ isSheet && styles$y.dialogSheet,
780
768
  ]
781
769
  .filter(Boolean)
782
770
  .join(' ');
783
- return createPortal(jsx(AnimatePresence, { children: open && (jsx(motion.div, { className: styles$y.backdrop, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15 }, onClick: onClose, "aria-hidden": "true", children: jsxs(motion.div, { className: dialogClass, role: "dialog", "aria-modal": "true", "aria-label": title, initial: { opacity: 0, scale: 0.96, y: 8 }, animate: { opacity: 1, scale: 1, y: 0 }, exit: { opacity: 0, scale: 0.96, y: 8 }, transition: { duration: 0.15, ease: 'easeOut' }, onClick: (e) => e.stopPropagation(), children: [jsxs("div", { className: styles$y.header, children: [jsx("span", { className: styles$y.title, children: title }), actions && jsx("div", { className: styles$y.headerActions, children: actions }), jsx("button", { className: styles$y.closeButton, onClick: onClose, "aria-label": "Close modal", type: "button", children: jsx(X, { size: 16 }) })] }), jsx("div", { className: padding ? styles$y.body : styles$y.bodyFlush, children: children })] }) })) }), document.body);
771
+ const enteringAnimation = isSheet
772
+ ? { initial: { y: '100%' }, animate: { y: 0 }, exit: { y: '100%' } }
773
+ : {
774
+ initial: { opacity: 0, scale: 0.96, y: 8 },
775
+ animate: { opacity: 1, scale: 1, y: 0 },
776
+ exit: { opacity: 0, scale: 0.96, y: 8 },
777
+ };
778
+ return createPortal(jsx(AnimatePresence, { children: open && (jsx(motion.div, { className: backdropClass, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15 }, onClick: onClose, "aria-hidden": "true", children: jsxs(motion.div, { className: dialogClass, role: "dialog", "aria-modal": "true", "aria-label": title, ...enteringAnimation, transition: isSheet
779
+ ? { type: 'spring', stiffness: 320, damping: 32 }
780
+ : { duration: 0.15, ease: 'easeOut' }, onClick: (e) => e.stopPropagation(), drag: isDraggable ? 'y' : false, dragControls: isDraggable ? dragControls : undefined, dragConstraints: { top: 0 }, dragElastic: { top: 0, bottom: 0.3 }, dragListener: false, onDragEnd: (_, info) => {
781
+ if (info.offset.y > 80)
782
+ onClose();
783
+ }, children: [isSheet && jsx("div", { className: styles$y.grabBar }), jsxs("div", { ref: headerRef, className: styles$y.header, onPointerDown: (e) => {
784
+ if (!isDraggable)
785
+ return;
786
+ if (e.target.closest('button'))
787
+ return;
788
+ dragControls.start(e);
789
+ }, children: [jsx("span", { className: styles$y.title, children: title }), actions && jsx("div", { className: styles$y.headerActions, children: actions }), jsx("button", { className: styles$y.closeButton, onClick: onClose, "aria-label": "Close modal", type: "button", children: jsx(X, { size: 16 }) })] }), jsx("div", { className: padding ? styles$y.body : styles$y.bodyFlush, children: children })] }) })) }), document.body);
784
790
  };
785
791
 
786
792
  var styles$x = {"checkboxLabel":"Checkbox-module_checkboxLabel__4tBVg","checkbox":"Checkbox-module_checkbox__BbJul","checkboxText":"Checkbox-module_checkboxText__oJsc9"};