analytica-frontend-lib 1.1.52 → 1.1.54

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.
@@ -12,19 +12,29 @@ type ModalProps = {
12
12
  /** Modal title */
13
13
  title: string;
14
14
  /** Modal description/content */
15
- children: ReactNode;
15
+ children?: ReactNode;
16
16
  /** Size of the modal */
17
17
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
18
18
  /** Additional CSS classes for the modal content */
19
19
  className?: string;
20
- /** Whether clicking the backdrop should close the modal */
21
- closeOnBackdropClick?: boolean;
22
20
  /** Whether pressing Escape should close the modal */
23
21
  closeOnEscape?: boolean;
24
22
  /** Footer content (typically buttons) */
25
23
  footer?: ReactNode;
26
24
  /** Hide the close button */
27
25
  hideCloseButton?: boolean;
26
+ /** Modal variant */
27
+ variant?: 'default' | 'activity';
28
+ /** Description for activity variant */
29
+ description?: string;
30
+ /** Image URL for activity variant */
31
+ image?: string;
32
+ /** Alt text for activity image (leave empty for decorative images) */
33
+ imageAlt?: string;
34
+ /** Action link for activity variant */
35
+ actionLink?: string;
36
+ /** Action button label for activity variant */
37
+ actionLabel?: string;
28
38
  };
29
39
  /**
30
40
  * Modal component for Analytica Ensino platforms
@@ -37,7 +47,6 @@ type ModalProps = {
37
47
  * @param children - The main content of the modal
38
48
  * @param size - The size variant (xs, sm, md, lg, xl)
39
49
  * @param className - Additional CSS classes for the modal content
40
- * @param closeOnBackdropClick - Whether clicking the backdrop closes the modal (default: true)
41
50
  * @param closeOnEscape - Whether pressing Escape closes the modal (default: true)
42
51
  * @param footer - Footer content, typically action buttons
43
52
  * @param hideCloseButton - Whether to hide the X close button (default: false)
@@ -61,6 +70,6 @@ type ModalProps = {
61
70
  * </Modal>
62
71
  * ```
63
72
  */
64
- declare const Modal: ({ isOpen, onClose, title, children, size, className, closeOnBackdropClick, closeOnEscape, footer, hideCloseButton, }: ModalProps) => react_jsx_runtime.JSX.Element | null;
73
+ declare const Modal: ({ isOpen, onClose, title, children, size, className, closeOnEscape, footer, hideCloseButton, variant, description, image, imageAlt, actionLink, actionLabel, }: ModalProps) => react_jsx_runtime.JSX.Element | null;
65
74
 
66
75
  export { Modal as default };
@@ -12,19 +12,29 @@ type ModalProps = {
12
12
  /** Modal title */
13
13
  title: string;
14
14
  /** Modal description/content */
15
- children: ReactNode;
15
+ children?: ReactNode;
16
16
  /** Size of the modal */
17
17
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
18
18
  /** Additional CSS classes for the modal content */
19
19
  className?: string;
20
- /** Whether clicking the backdrop should close the modal */
21
- closeOnBackdropClick?: boolean;
22
20
  /** Whether pressing Escape should close the modal */
23
21
  closeOnEscape?: boolean;
24
22
  /** Footer content (typically buttons) */
25
23
  footer?: ReactNode;
26
24
  /** Hide the close button */
27
25
  hideCloseButton?: boolean;
26
+ /** Modal variant */
27
+ variant?: 'default' | 'activity';
28
+ /** Description for activity variant */
29
+ description?: string;
30
+ /** Image URL for activity variant */
31
+ image?: string;
32
+ /** Alt text for activity image (leave empty for decorative images) */
33
+ imageAlt?: string;
34
+ /** Action link for activity variant */
35
+ actionLink?: string;
36
+ /** Action button label for activity variant */
37
+ actionLabel?: string;
28
38
  };
29
39
  /**
30
40
  * Modal component for Analytica Ensino platforms
@@ -37,7 +47,6 @@ type ModalProps = {
37
47
  * @param children - The main content of the modal
38
48
  * @param size - The size variant (xs, sm, md, lg, xl)
39
49
  * @param className - Additional CSS classes for the modal content
40
- * @param closeOnBackdropClick - Whether clicking the backdrop closes the modal (default: true)
41
50
  * @param closeOnEscape - Whether pressing Escape closes the modal (default: true)
42
51
  * @param footer - Footer content, typically action buttons
43
52
  * @param hideCloseButton - Whether to hide the X close button (default: false)
@@ -61,6 +70,6 @@ type ModalProps = {
61
70
  * </Modal>
62
71
  * ```
63
72
  */
64
- declare const Modal: ({ isOpen, onClose, title, children, size, className, closeOnBackdropClick, closeOnEscape, footer, hideCloseButton, }: ModalProps) => react_jsx_runtime.JSX.Element | null;
73
+ declare const Modal: ({ isOpen, onClose, title, children, size, className, closeOnEscape, footer, hideCloseButton, variant, description, image, imageAlt, actionLink, actionLabel, }: ModalProps) => react_jsx_runtime.JSX.Element | null;
65
74
 
66
75
  export { Modal as default };
@@ -33,9 +33,111 @@ function cn(...inputs) {
33
33
  return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
34
34
  }
35
35
 
36
- // src/components/Modal/Modal.tsx
36
+ // src/components/Button/Button.tsx
37
37
  var import_jsx_runtime = require("react/jsx-runtime");
38
+ var VARIANT_ACTION_CLASSES = {
39
+ solid: {
40
+ primary: "bg-primary-950 text-text border border-primary-950 hover:bg-primary-800 hover:border-primary-800 focus-visible:outline-none focus-visible:bg-primary-950 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-primary-700 active:border-primary-700 disabled:bg-primary-500 disabled:border-primary-500 disabled:opacity-40 disabled:cursor-not-allowed",
41
+ positive: "bg-success-500 text-text border border-success-500 hover:bg-success-600 hover:border-success-600 focus-visible:outline-none focus-visible:bg-success-500 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-success-700 active:border-success-700 disabled:bg-success-500 disabled:border-success-500 disabled:opacity-40 disabled:cursor-not-allowed",
42
+ negative: "bg-error-500 text-text border border-error-500 hover:bg-error-600 hover:border-error-600 focus-visible:outline-none focus-visible:bg-error-500 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-error-700 active:border-error-700 disabled:bg-error-500 disabled:border-error-500 disabled:opacity-40 disabled:cursor-not-allowed"
43
+ },
44
+ outline: {
45
+ primary: "bg-transparent text-primary-950 border border-primary-950 hover:bg-background-50 hover:text-primary-400 hover:border-primary-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-primary-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-primary-700 active:border-primary-700 disabled:opacity-40 disabled:cursor-not-allowed",
46
+ positive: "bg-transparent text-success-500 border border-success-300 hover:bg-background-50 hover:text-success-400 hover:border-success-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-success-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-success-700 active:border-success-700 disabled:opacity-40 disabled:cursor-not-allowed",
47
+ negative: "bg-transparent text-error-500 border border-error-300 hover:bg-background-50 hover:text-error-400 hover:border-error-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-error-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-error-700 active:border-error-700 disabled:opacity-40 disabled:cursor-not-allowed"
48
+ },
49
+ link: {
50
+ primary: "bg-transparent text-primary-950 hover:text-primary-400 focus-visible:outline-none focus-visible:text-primary-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-primary-700 disabled:opacity-40 disabled:cursor-not-allowed",
51
+ positive: "bg-transparent text-success-500 hover:text-success-400 focus-visible:outline-none focus-visible:text-success-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-success-700 disabled:opacity-40 disabled:cursor-not-allowed",
52
+ negative: "bg-transparent text-error-500 hover:text-error-400 focus-visible:outline-none focus-visible:text-error-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-error-700 disabled:opacity-40 disabled:cursor-not-allowed"
53
+ }
54
+ };
38
55
  var SIZE_CLASSES = {
56
+ "extra-small": "text-xs px-3.5 py-2",
57
+ small: "text-sm px-4 py-2.5",
58
+ medium: "text-md px-5 py-2.5",
59
+ large: "text-lg px-6 py-3",
60
+ "extra-large": "text-lg px-7 py-3.5"
61
+ };
62
+ var Button = ({
63
+ children,
64
+ iconLeft,
65
+ iconRight,
66
+ size = "medium",
67
+ variant = "solid",
68
+ action = "primary",
69
+ className = "",
70
+ disabled,
71
+ type = "button",
72
+ ...props
73
+ }) => {
74
+ const sizeClasses = SIZE_CLASSES[size];
75
+ const variantClasses = VARIANT_ACTION_CLASSES[variant][action];
76
+ const baseClasses = "inline-flex items-center justify-center rounded-full cursor-pointer font-medium";
77
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
78
+ "button",
79
+ {
80
+ className: cn(baseClasses, variantClasses, sizeClasses, className),
81
+ disabled,
82
+ type,
83
+ ...props,
84
+ children: [
85
+ iconLeft && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mr-2 flex items-center", children: iconLeft }),
86
+ children,
87
+ iconRight && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ml-2 flex items-center", children: iconRight })
88
+ ]
89
+ }
90
+ );
91
+ };
92
+ var Button_default = Button;
93
+
94
+ // src/components/Modal/utils/videoUtils.ts
95
+ var isYouTubeUrl = (url) => {
96
+ const youtubeRegex = /^(https?:\/\/)?((www|m|music)\.)?(youtube\.com|youtu\.be|youtube-nocookie\.com)\/.+/i;
97
+ return youtubeRegex.test(url);
98
+ };
99
+ var isValidYouTubeHost = (host) => {
100
+ if (host === "youtu.be") return "youtu.be";
101
+ const isValidYouTubeCom = host === "youtube.com" || host.endsWith(".youtube.com") && /^(www|m|music)\.youtube\.com$/.test(host);
102
+ if (isValidYouTubeCom) return "youtube";
103
+ const isValidNoCookie = host === "youtube-nocookie.com" || host.endsWith(".youtube-nocookie.com") && /^(www|m|music)\.youtube-nocookie\.com$/.test(host);
104
+ if (isValidNoCookie) return "nocookie";
105
+ return null;
106
+ };
107
+ var extractYoutuBeId = (pathname) => {
108
+ const firstSeg = pathname.split("/").filter(Boolean)[0];
109
+ return firstSeg || null;
110
+ };
111
+ var extractYouTubeId = (pathname, searchParams) => {
112
+ const parts = pathname.split("/").filter(Boolean);
113
+ const [first, second] = parts;
114
+ if (first === "embed" && second) return second;
115
+ if (first === "shorts" && second) return second;
116
+ if (first === "live" && second) return second;
117
+ const v = searchParams.get("v");
118
+ if (v) return v;
119
+ return null;
120
+ };
121
+ var getYouTubeVideoId = (url) => {
122
+ try {
123
+ const u = new URL(url);
124
+ const hostType = isValidYouTubeHost(u.hostname.toLowerCase());
125
+ if (!hostType) return null;
126
+ if (hostType === "youtu.be") {
127
+ return extractYoutuBeId(u.pathname);
128
+ }
129
+ return extractYouTubeId(u.pathname, u.searchParams);
130
+ } catch {
131
+ return null;
132
+ }
133
+ };
134
+ var getYouTubeEmbedUrl = (videoId) => {
135
+ return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=0&rel=0&modestbranding=1`;
136
+ };
137
+
138
+ // src/components/Modal/Modal.tsx
139
+ var import_jsx_runtime2 = require("react/jsx-runtime");
140
+ var SIZE_CLASSES2 = {
39
141
  xs: "max-w-[360px]",
40
142
  sm: "max-w-[420px]",
41
143
  md: "max-w-[510px]",
@@ -49,11 +151,17 @@ var Modal = ({
49
151
  children,
50
152
  size = "md",
51
153
  className = "",
52
- closeOnBackdropClick = true,
53
154
  closeOnEscape = true,
54
155
  footer,
55
- hideCloseButton = false
156
+ hideCloseButton = false,
157
+ variant = "default",
158
+ description,
159
+ image,
160
+ imageAlt,
161
+ actionLink,
162
+ actionLabel
56
163
  }) => {
164
+ const titleId = (0, import_react.useId)();
57
165
  (0, import_react.useEffect)(() => {
58
166
  if (!isOpen || !closeOnEscape) return;
59
167
  const handleEscape = (event) => {
@@ -75,18 +183,8 @@ var Modal = ({
75
183
  document.body.style.overflow = originalOverflow;
76
184
  };
77
185
  }, [isOpen]);
78
- const handleBackdropClick = (event) => {
79
- if (closeOnBackdropClick && event.target === event.currentTarget) {
80
- onClose();
81
- }
82
- };
83
- const handleBackdropKeyDown = (event) => {
84
- if (closeOnBackdropClick && (event.key === "Enter" || event.key === " ")) {
85
- onClose();
86
- }
87
- };
88
186
  if (!isOpen) return null;
89
- const sizeClasses = SIZE_CLASSES[size];
187
+ const sizeClasses = SIZE_CLASSES2[size];
90
188
  const baseClasses = "bg-secondary-50 rounded-3xl shadow-hard-shadow-2 border border-border-100 w-full mx-4";
91
189
  const dialogResetClasses = "p-0 m-0 border-none outline-none max-h-none static";
92
190
  const modalClasses = cn(
@@ -95,33 +193,120 @@ var Modal = ({
95
193
  dialogResetClasses,
96
194
  className
97
195
  );
98
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
99
- "div",
196
+ const normalizeUrl = (href) => /^https?:\/\//i.test(href) ? href : `https://${href}`;
197
+ const handleActionClick = () => {
198
+ if (actionLink) {
199
+ window.open(normalizeUrl(actionLink), "_blank", "noopener,noreferrer");
200
+ }
201
+ };
202
+ if (variant === "activity") {
203
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs border-none p-0 m-0 w-full cursor-default", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
204
+ "dialog",
205
+ {
206
+ className: modalClasses,
207
+ "aria-labelledby": titleId,
208
+ "aria-modal": "true",
209
+ open: true,
210
+ children: [
211
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex justify-end p-6 pb-0", children: !hideCloseButton && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
212
+ "button",
213
+ {
214
+ onClick: onClose,
215
+ className: "p-1 text-text-500 hover:text-text-700 hover:bg-background-50 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-indicator-info focus:ring-offset-2",
216
+ "aria-label": "Fechar modal",
217
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_phosphor_react.X, { size: 18 })
218
+ }
219
+ ) }),
220
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center px-6 pb-6 gap-5", children: [
221
+ image && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
222
+ "img",
223
+ {
224
+ src: image,
225
+ alt: imageAlt ?? "",
226
+ className: "w-[122px] h-[122px] object-contain"
227
+ }
228
+ ) }),
229
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
230
+ "h2",
231
+ {
232
+ id: titleId,
233
+ className: "text-lg font-semibold text-text-950 text-center",
234
+ children: title
235
+ }
236
+ ),
237
+ description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm font-normal text-text-400 text-center max-w-md leading-[21px]", children: description }),
238
+ actionLink && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full", children: [
239
+ (() => {
240
+ const normalized = normalizeUrl(actionLink);
241
+ const isYT = isYouTubeUrl(normalized);
242
+ if (!isYT) return null;
243
+ const id = getYouTubeVideoId(normalized);
244
+ if (!id) {
245
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
246
+ Button_default,
247
+ {
248
+ variant: "solid",
249
+ action: "primary",
250
+ size: "large",
251
+ className: "w-full",
252
+ onClick: handleActionClick,
253
+ children: actionLabel || "Iniciar Atividade"
254
+ }
255
+ );
256
+ }
257
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
258
+ "iframe",
259
+ {
260
+ src: getYouTubeEmbedUrl(id),
261
+ className: "w-full aspect-video rounded-lg",
262
+ allowFullScreen: true,
263
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
264
+ title: "V\xEDdeo YouTube"
265
+ }
266
+ );
267
+ })(),
268
+ !isYouTubeUrl(normalizeUrl(actionLink)) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
269
+ Button_default,
270
+ {
271
+ variant: "solid",
272
+ action: "primary",
273
+ size: "large",
274
+ className: "w-full",
275
+ onClick: handleActionClick,
276
+ children: actionLabel || "Iniciar Atividade"
277
+ }
278
+ )
279
+ ] })
280
+ ] })
281
+ ]
282
+ }
283
+ ) });
284
+ }
285
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs border-none p-0 m-0 w-full cursor-default", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
286
+ "dialog",
100
287
  {
101
- className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs",
102
- onClick: handleBackdropClick,
103
- onKeyDown: handleBackdropKeyDown,
104
- role: "button",
105
- tabIndex: closeOnBackdropClick ? 0 : -1,
106
- "aria-label": "Fechar modal clicando no fundo",
107
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dialog", { className: modalClasses, "aria-labelledby": "modal-title", open: true, children: [
108
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between px-6 py-6", children: [
109
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { id: "modal-title", className: "text-lg font-semibold text-text-950", children: title }),
110
- !hideCloseButton && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
288
+ className: modalClasses,
289
+ "aria-labelledby": titleId,
290
+ "aria-modal": "true",
291
+ open: true,
292
+ children: [
293
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between px-6 py-6", children: [
294
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { id: titleId, className: "text-lg font-semibold text-text-950", children: title }),
295
+ !hideCloseButton && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
111
296
  "button",
112
297
  {
113
298
  onClick: onClose,
114
299
  className: "p-1 text-text-500 hover:text-text-700 hover:bg-background-50 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-indicator-info focus:ring-offset-2",
115
300
  "aria-label": "Fechar modal",
116
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_phosphor_react.X, { size: 18 })
301
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_phosphor_react.X, { size: 18 })
117
302
  }
118
303
  )
119
304
  ] }),
120
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "px-6 pb-6", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-text-500 font-normal text-sm leading-6", children }) }),
121
- footer && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex justify-end gap-3 px-6 pb-6", children: footer })
122
- ] })
305
+ children && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "px-6 pb-6", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-text-500 font-normal text-sm leading-6", children }) }),
306
+ footer && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex justify-end gap-3 px-6 pb-6", children: footer })
307
+ ]
123
308
  }
124
- );
309
+ ) });
125
310
  };
126
311
  var Modal_default = Modal;
127
312
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/Modal/Modal.tsx","../../src/utils/utils.ts"],"sourcesContent":["import { ReactNode, MouseEvent, useEffect, KeyboardEvent } from 'react';\nimport { X } from 'phosphor-react';\nimport { cn } from '../../utils/utils';\n\n/**\n * Lookup table for size classes\n */\nconst SIZE_CLASSES = {\n xs: 'max-w-[360px]',\n sm: 'max-w-[420px]',\n md: 'max-w-[510px]',\n lg: 'max-w-[640px]',\n xl: 'max-w-[970px]',\n} as const;\n\n/**\n * Modal component props interface\n */\ntype ModalProps = {\n /** Whether the modal is open */\n isOpen: boolean;\n /** Function to close the modal */\n onClose: () => void;\n /** Modal title */\n title: string;\n /** Modal description/content */\n children: ReactNode;\n /** Size of the modal */\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n /** Additional CSS classes for the modal content */\n className?: string;\n /** Whether clicking the backdrop should close the modal */\n closeOnBackdropClick?: boolean;\n /** Whether pressing Escape should close the modal */\n closeOnEscape?: boolean;\n /** Footer content (typically buttons) */\n footer?: ReactNode;\n /** Hide the close button */\n hideCloseButton?: boolean;\n};\n\n/**\n * Modal component for Analytica Ensino platforms\n *\n * A flexible modal component with multiple size variants and customizable behavior.\n *\n * @param isOpen - Whether the modal is currently open\n * @param onClose - Callback function called when the modal should be closed\n * @param title - The title displayed at the top of the modal\n * @param children - The main content of the modal\n * @param size - The size variant (xs, sm, md, lg, xl)\n * @param className - Additional CSS classes for the modal content\n * @param closeOnBackdropClick - Whether clicking the backdrop closes the modal (default: true)\n * @param closeOnEscape - Whether pressing Escape closes the modal (default: true)\n * @param footer - Footer content, typically action buttons\n * @param hideCloseButton - Whether to hide the X close button (default: false)\n * @returns A modal overlay with content\n *\n * @example\n * ```tsx\n * <Modal\n * isOpen={isModalOpen}\n * onClose={() => setIsModalOpen(false)}\n * title=\"Invite your team\"\n * size=\"md\"\n * footer={\n * <div className=\"flex gap-3\">\n * <Button variant=\"outline\" onClick={() => setIsModalOpen(false)}>Cancel</Button>\n * <Button variant=\"solid\" onClick={handleExplore}>Explore</Button>\n * </div>\n * }\n * >\n * Elevate user interactions with our versatile modals.\n * </Modal>\n * ```\n */\nconst Modal = ({\n isOpen,\n onClose,\n title,\n children,\n size = 'md',\n className = '',\n closeOnBackdropClick = true,\n closeOnEscape = true,\n footer,\n hideCloseButton = false,\n}: ModalProps) => {\n // Handle escape key\n useEffect(() => {\n if (!isOpen || !closeOnEscape) return;\n\n const handleEscape = (event: globalThis.KeyboardEvent) => {\n if (event.key === 'Escape') {\n onClose();\n }\n };\n\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [isOpen, closeOnEscape, onClose]);\n\n // Handle body scroll lock\n useEffect(() => {\n const originalOverflow = document.body.style.overflow;\n if (isOpen) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = originalOverflow;\n }\n\n return () => {\n document.body.style.overflow = originalOverflow;\n };\n }, [isOpen]);\n\n // Handle backdrop click\n const handleBackdropClick = (event: MouseEvent<HTMLDivElement>) => {\n if (closeOnBackdropClick && event.target === event.currentTarget) {\n onClose();\n }\n };\n\n // Handle backdrop keyboard interaction\n const handleBackdropKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (closeOnBackdropClick && (event.key === 'Enter' || event.key === ' ')) {\n onClose();\n }\n };\n\n if (!isOpen) return null;\n\n const sizeClasses = SIZE_CLASSES[size];\n const baseClasses =\n 'bg-secondary-50 rounded-3xl shadow-hard-shadow-2 border border-border-100 w-full mx-4';\n // Reset dialog default styles to prevent positioning issues\n const dialogResetClasses =\n 'p-0 m-0 border-none outline-none max-h-none static';\n const modalClasses = cn(\n baseClasses,\n sizeClasses,\n dialogResetClasses,\n className\n );\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs\"\n onClick={handleBackdropClick}\n onKeyDown={handleBackdropKeyDown}\n role=\"button\"\n tabIndex={closeOnBackdropClick ? 0 : -1}\n aria-label=\"Fechar modal clicando no fundo\"\n >\n <dialog className={modalClasses} aria-labelledby=\"modal-title\" open>\n {/* Header */}\n <div className=\"flex items-center justify-between px-6 py-6\">\n <h2 id=\"modal-title\" className=\"text-lg font-semibold text-text-950\">\n {title}\n </h2>\n {!hideCloseButton && (\n <button\n onClick={onClose}\n className=\"p-1 text-text-500 hover:text-text-700 hover:bg-background-50 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-indicator-info focus:ring-offset-2\"\n aria-label=\"Fechar modal\"\n >\n <X size={18} />\n </button>\n )}\n </div>\n\n {/* Content */}\n <div className=\"px-6 pb-6\">\n <div className=\"text-text-500 font-normal text-sm leading-6\">\n {children}\n </div>\n </div>\n\n {/* Footer */}\n {footer && (\n <div className=\"flex justify-end gap-3 px-6 pb-6\">{footer}</div>\n )}\n </dialog>\n </div>\n );\n};\n\nexport default Modal;\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAgE;AAChE,4BAAkB;;;ACDlB,kBAAsC;AACtC,4BAAwB;AAEjB,SAAS,MAAM,QAAsB;AAC1C,aAAO,mCAAQ,kBAAK,MAAM,CAAC;AAC7B;;;ADuJQ;AArJR,IAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AA+DA,IAAM,QAAQ,CAAC;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,uBAAuB;AAAA,EACvB,gBAAgB;AAAA,EAChB;AAAA,EACA,kBAAkB;AACpB,MAAkB;AAEhB,8BAAU,MAAM;AACd,QAAI,CAAC,UAAU,CAAC,cAAe;AAE/B,UAAM,eAAe,CAAC,UAAoC;AACxD,UAAI,MAAM,QAAQ,UAAU;AAC1B,gBAAQ;AAAA,MACV;AAAA,IACF;AAEA,aAAS,iBAAiB,WAAW,YAAY;AACjD,WAAO,MAAM,SAAS,oBAAoB,WAAW,YAAY;AAAA,EACnE,GAAG,CAAC,QAAQ,eAAe,OAAO,CAAC;AAGnC,8BAAU,MAAM;AACd,UAAM,mBAAmB,SAAS,KAAK,MAAM;AAC7C,QAAI,QAAQ;AACV,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC,OAAO;AACL,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC;AAEA,WAAO,MAAM;AACX,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAGX,QAAM,sBAAsB,CAAC,UAAsC;AACjE,QAAI,wBAAwB,MAAM,WAAW,MAAM,eAAe;AAChE,cAAQ;AAAA,IACV;AAAA,EACF;AAGA,QAAM,wBAAwB,CAAC,UAAyC;AACtE,QAAI,yBAAyB,MAAM,QAAQ,WAAW,MAAM,QAAQ,MAAM;AACxE,cAAQ;AAAA,IACV;AAAA,EACF;AAEA,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,cAAc,aAAa,IAAI;AACrC,QAAM,cACJ;AAEF,QAAM,qBACJ;AACF,QAAM,eAAe;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAW;AAAA,MACX,MAAK;AAAA,MACL,UAAU,uBAAuB,IAAI;AAAA,MACrC,cAAW;AAAA,MAEX,uDAAC,YAAO,WAAW,cAAc,mBAAgB,eAAc,MAAI,MAEjE;AAAA,qDAAC,SAAI,WAAU,+CACb;AAAA,sDAAC,QAAG,IAAG,eAAc,WAAU,uCAC5B,iBACH;AAAA,UACC,CAAC,mBACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS;AAAA,cACT,WAAU;AAAA,cACV,cAAW;AAAA,cAEX,sDAAC,2BAAE,MAAM,IAAI;AAAA;AAAA,UACf;AAAA,WAEJ;AAAA,QAGA,4CAAC,SAAI,WAAU,aACb,sDAAC,SAAI,WAAU,+CACZ,UACH,GACF;AAAA,QAGC,UACC,4CAAC,SAAI,WAAU,oCAAoC,kBAAO;AAAA,SAE9D;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,gBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/Modal/Modal.tsx","../../src/utils/utils.ts","../../src/components/Button/Button.tsx","../../src/components/Modal/utils/videoUtils.ts"],"sourcesContent":["import { ReactNode, useEffect, useId } from 'react';\nimport { X } from 'phosphor-react';\nimport { cn } from '../../utils/utils';\nimport Button from '../Button/Button';\nimport {\n isYouTubeUrl,\n getYouTubeVideoId,\n getYouTubeEmbedUrl,\n} from './utils/videoUtils';\n\n/**\n * Lookup table for size classes\n */\nconst SIZE_CLASSES = {\n xs: 'max-w-[360px]',\n sm: 'max-w-[420px]',\n md: 'max-w-[510px]',\n lg: 'max-w-[640px]',\n xl: 'max-w-[970px]',\n} as const;\n\n/**\n * Modal component props interface\n */\ntype ModalProps = {\n /** Whether the modal is open */\n isOpen: boolean;\n /** Function to close the modal */\n onClose: () => void;\n /** Modal title */\n title: string;\n /** Modal description/content */\n children?: ReactNode;\n /** Size of the modal */\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n /** Additional CSS classes for the modal content */\n className?: string;\n /** Whether pressing Escape should close the modal */\n closeOnEscape?: boolean;\n /** Footer content (typically buttons) */\n footer?: ReactNode;\n /** Hide the close button */\n hideCloseButton?: boolean;\n /** Modal variant */\n variant?: 'default' | 'activity';\n /** Description for activity variant */\n description?: string;\n /** Image URL for activity variant */\n image?: string;\n /** Alt text for activity image (leave empty for decorative images) */\n imageAlt?: string;\n /** Action link for activity variant */\n actionLink?: string;\n /** Action button label for activity variant */\n actionLabel?: string;\n};\n\n/**\n * Modal component for Analytica Ensino platforms\n *\n * A flexible modal component with multiple size variants and customizable behavior.\n *\n * @param isOpen - Whether the modal is currently open\n * @param onClose - Callback function called when the modal should be closed\n * @param title - The title displayed at the top of the modal\n * @param children - The main content of the modal\n * @param size - The size variant (xs, sm, md, lg, xl)\n * @param className - Additional CSS classes for the modal content\n * @param closeOnEscape - Whether pressing Escape closes the modal (default: true)\n * @param footer - Footer content, typically action buttons\n * @param hideCloseButton - Whether to hide the X close button (default: false)\n * @returns A modal overlay with content\n *\n * @example\n * ```tsx\n * <Modal\n * isOpen={isModalOpen}\n * onClose={() => setIsModalOpen(false)}\n * title=\"Invite your team\"\n * size=\"md\"\n * footer={\n * <div className=\"flex gap-3\">\n * <Button variant=\"outline\" onClick={() => setIsModalOpen(false)}>Cancel</Button>\n * <Button variant=\"solid\" onClick={handleExplore}>Explore</Button>\n * </div>\n * }\n * >\n * Elevate user interactions with our versatile modals.\n * </Modal>\n * ```\n */\nconst Modal = ({\n isOpen,\n onClose,\n title,\n children,\n size = 'md',\n className = '',\n closeOnEscape = true,\n footer,\n hideCloseButton = false,\n variant = 'default',\n description,\n image,\n imageAlt,\n actionLink,\n actionLabel,\n}: ModalProps) => {\n const titleId = useId();\n\n // Handle escape key\n useEffect(() => {\n if (!isOpen || !closeOnEscape) return;\n\n const handleEscape = (event: globalThis.KeyboardEvent) => {\n if (event.key === 'Escape') {\n onClose();\n }\n };\n\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [isOpen, closeOnEscape, onClose]);\n\n // Handle body scroll lock\n useEffect(() => {\n const originalOverflow = document.body.style.overflow;\n if (isOpen) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = originalOverflow;\n }\n\n return () => {\n document.body.style.overflow = originalOverflow;\n };\n }, [isOpen]);\n\n if (!isOpen) return null;\n\n const sizeClasses = SIZE_CLASSES[size];\n const baseClasses =\n 'bg-secondary-50 rounded-3xl shadow-hard-shadow-2 border border-border-100 w-full mx-4';\n // Reset dialog default styles to prevent positioning issues\n const dialogResetClasses =\n 'p-0 m-0 border-none outline-none max-h-none static';\n const modalClasses = cn(\n baseClasses,\n sizeClasses,\n dialogResetClasses,\n className\n );\n\n // Normalize URLs missing protocol\n const normalizeUrl = (href: string) =>\n /^https?:\\/\\//i.test(href) ? href : `https://${href}`;\n\n // Handle action link click\n const handleActionClick = () => {\n if (actionLink) {\n window.open(normalizeUrl(actionLink), '_blank', 'noopener,noreferrer');\n }\n };\n\n // Activity variant rendering\n if (variant === 'activity') {\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs border-none p-0 m-0 w-full cursor-default\">\n <dialog\n className={modalClasses}\n aria-labelledby={titleId}\n aria-modal=\"true\"\n open\n >\n {/* Header simples com X */}\n <div className=\"flex justify-end p-6 pb-0\">\n {!hideCloseButton && (\n <button\n onClick={onClose}\n className=\"p-1 text-text-500 hover:text-text-700 hover:bg-background-50 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-indicator-info focus:ring-offset-2\"\n aria-label=\"Fechar modal\"\n >\n <X size={18} />\n </button>\n )}\n </div>\n\n {/* Conteúdo centralizado */}\n <div className=\"flex flex-col items-center px-6 pb-6 gap-5\">\n {/* Imagem ilustrativa */}\n {image && (\n <div className=\"flex justify-center\">\n <img\n src={image}\n alt={imageAlt ?? ''}\n className=\"w-[122px] h-[122px] object-contain\"\n />\n </div>\n )}\n\n {/* Título */}\n <h2\n id={titleId}\n className=\"text-lg font-semibold text-text-950 text-center\"\n >\n {title}\n </h2>\n\n {/* Descrição */}\n {description && (\n <p className=\"text-sm font-normal text-text-400 text-center max-w-md leading-[21px]\">\n {description}\n </p>\n )}\n\n {/* Ação: Botão ou Vídeo Embedado */}\n {actionLink && (\n <div className=\"w-full\">\n {(() => {\n const normalized = normalizeUrl(actionLink);\n const isYT = isYouTubeUrl(normalized);\n if (!isYT) return null;\n const id = getYouTubeVideoId(normalized);\n if (!id) {\n return (\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"large\"\n className=\"w-full\"\n onClick={handleActionClick}\n >\n {actionLabel || 'Iniciar Atividade'}\n </Button>\n );\n }\n return (\n <iframe\n src={getYouTubeEmbedUrl(id)}\n className=\"w-full aspect-video rounded-lg\"\n allowFullScreen\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n title=\"Vídeo YouTube\"\n />\n );\n })()}\n {!isYouTubeUrl(normalizeUrl(actionLink)) && (\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"large\"\n className=\"w-full\"\n onClick={handleActionClick}\n >\n {actionLabel || 'Iniciar Atividade'}\n </Button>\n )}\n </div>\n )}\n </div>\n </dialog>\n </div>\n );\n }\n\n // Default variant rendering\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs border-none p-0 m-0 w-full cursor-default\">\n <dialog\n className={modalClasses}\n aria-labelledby={titleId}\n aria-modal=\"true\"\n open\n >\n {/* Header */}\n <div className=\"flex items-center justify-between px-6 py-6\">\n <h2 id={titleId} className=\"text-lg font-semibold text-text-950\">\n {title}\n </h2>\n {!hideCloseButton && (\n <button\n onClick={onClose}\n className=\"p-1 text-text-500 hover:text-text-700 hover:bg-background-50 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-indicator-info focus:ring-offset-2\"\n aria-label=\"Fechar modal\"\n >\n <X size={18} />\n </button>\n )}\n </div>\n\n {/* Content */}\n {children && (\n <div className=\"px-6 pb-6\">\n <div className=\"text-text-500 font-normal text-sm leading-6\">\n {children}\n </div>\n </div>\n )}\n\n {/* Footer */}\n {footer && (\n <div className=\"flex justify-end gap-3 px-6 pb-6\">{footer}</div>\n )}\n </dialog>\n </div>\n );\n};\n\nexport default Modal;\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import { ButtonHTMLAttributes, ReactNode } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * Lookup table for variant and action class combinations\n */\nconst VARIANT_ACTION_CLASSES = {\n solid: {\n primary:\n 'bg-primary-950 text-text border border-primary-950 hover:bg-primary-800 hover:border-primary-800 focus-visible:outline-none focus-visible:bg-primary-950 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-primary-700 active:border-primary-700 disabled:bg-primary-500 disabled:border-primary-500 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-success-500 text-text border border-success-500 hover:bg-success-600 hover:border-success-600 focus-visible:outline-none focus-visible:bg-success-500 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-success-700 active:border-success-700 disabled:bg-success-500 disabled:border-success-500 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-error-500 text-text border border-error-500 hover:bg-error-600 hover:border-error-600 focus-visible:outline-none focus-visible:bg-error-500 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-error-700 active:border-error-700 disabled:bg-error-500 disabled:border-error-500 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n outline: {\n primary:\n 'bg-transparent text-primary-950 border border-primary-950 hover:bg-background-50 hover:text-primary-400 hover:border-primary-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-primary-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-primary-700 active:border-primary-700 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-transparent text-success-500 border border-success-300 hover:bg-background-50 hover:text-success-400 hover:border-success-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-success-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-success-700 active:border-success-700 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-transparent text-error-500 border border-error-300 hover:bg-background-50 hover:text-error-400 hover:border-error-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-error-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-error-700 active:border-error-700 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n link: {\n primary:\n 'bg-transparent text-primary-950 hover:text-primary-400 focus-visible:outline-none focus-visible:text-primary-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-primary-700 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-transparent text-success-500 hover:text-success-400 focus-visible:outline-none focus-visible:text-success-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-success-700 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-transparent text-error-500 hover:text-error-400 focus-visible:outline-none focus-visible:text-error-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-error-700 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n} as const;\n\n/**\n * Lookup table for size classes\n */\nconst SIZE_CLASSES = {\n 'extra-small': 'text-xs px-3.5 py-2',\n small: 'text-sm px-4 py-2.5',\n medium: 'text-md px-5 py-2.5',\n large: 'text-lg px-6 py-3',\n 'extra-large': 'text-lg px-7 py-3.5',\n} as const;\n\n/**\n * Button component props interface\n */\ntype ButtonProps = {\n /** Content to be displayed inside the button */\n children: ReactNode;\n /** Ícone à esquerda do texto */\n iconLeft?: ReactNode;\n /** Ícone à direita do texto */\n iconRight?: ReactNode;\n /** Size of the button */\n size?: 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large';\n /** Visual variant of the button */\n variant?: 'solid' | 'outline' | 'link';\n /** Action type of the button */\n action?: 'primary' | 'positive' | 'negative';\n /** Additional CSS classes to apply */\n className?: string;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * Button component for Analytica Ensino platforms\n *\n * A flexible button component with multiple variants, sizes and actions.\n *\n * @param children - The content to display inside the button\n * @param size - The size variant (extra-small, small, medium, large, extra-large)\n * @param variant - The visual style variant (solid, outline, link)\n * @param action - The action type (primary, positive, negative)\n * @param className - Additional CSS classes\n * @param props - All other standard button HTML attributes\n * @returns A styled button element\n *\n * @example\n * ```tsx\n * <Button variant=\"solid\" action=\"primary\" size=\"medium\" onClick={() => console.log('clicked')}>\n * Click me\n * </Button>\n * ```\n */\nconst Button = ({\n children,\n iconLeft,\n iconRight,\n size = 'medium',\n variant = 'solid',\n action = 'primary',\n className = '',\n disabled,\n type = 'button',\n ...props\n}: ButtonProps) => {\n // Get classes from lookup tables\n const sizeClasses = SIZE_CLASSES[size];\n const variantClasses = VARIANT_ACTION_CLASSES[variant][action];\n\n const baseClasses =\n 'inline-flex items-center justify-center rounded-full cursor-pointer font-medium';\n\n return (\n <button\n className={cn(baseClasses, variantClasses, sizeClasses, className)}\n disabled={disabled}\n type={type}\n {...props}\n >\n {iconLeft && <span className=\"mr-2 flex items-center\">{iconLeft}</span>}\n {children}\n {iconRight && <span className=\"ml-2 flex items-center\">{iconRight}</span>}\n </button>\n );\n};\n\nexport default Button;\n","/**\n * Video utilities for Modal component\n *\n * Utilities to handle YouTube video embedding and URL detection\n */\n\n/**\n * Check if a given URL is a YouTube URL\n *\n * @param url - The URL to check\n * @returns true if the URL is from YouTube, false otherwise\n */\nexport const isYouTubeUrl = (url: string): boolean => {\n const youtubeRegex =\n /^(https?:\\/\\/)?((www|m|music)\\.)?(youtube\\.com|youtu\\.be|youtube-nocookie\\.com)\\/.+/i;\n return youtubeRegex.test(url);\n};\n\n/**\n * Validate if hostname is a legitimate YouTube host\n *\n * @param host - The hostname to validate\n * @returns The type of YouTube host or null if invalid\n */\nconst isValidYouTubeHost = (\n host: string\n): 'youtu.be' | 'youtube' | 'nocookie' | null => {\n if (host === 'youtu.be') return 'youtu.be';\n\n const isValidYouTubeCom =\n host === 'youtube.com' ||\n (host.endsWith('.youtube.com') &&\n /^(www|m|music)\\.youtube\\.com$/.test(host));\n\n if (isValidYouTubeCom) return 'youtube';\n\n const isValidNoCookie =\n host === 'youtube-nocookie.com' ||\n (host.endsWith('.youtube-nocookie.com') &&\n /^(www|m|music)\\.youtube-nocookie\\.com$/.test(host));\n\n if (isValidNoCookie) return 'nocookie';\n\n return null;\n};\n\n/**\n * Extract video ID from youtu.be path\n *\n * @param pathname - The URL pathname\n * @returns The video ID or null\n */\nconst extractYoutuBeId = (pathname: string): string | null => {\n const firstSeg = pathname.split('/').filter(Boolean)[0];\n return firstSeg || null;\n};\n\n/**\n * Extract video ID from YouTube or nocookie domains\n *\n * @param pathname - The URL pathname\n * @param searchParams - The URL search parameters\n * @returns The video ID or null\n */\nconst extractYouTubeId = (\n pathname: string,\n searchParams: URLSearchParams\n): string | null => {\n const parts = pathname.split('/').filter(Boolean);\n const [first, second] = parts;\n\n if (first === 'embed' && second) return second;\n if (first === 'shorts' && second) return second;\n if (first === 'live' && second) return second;\n\n const v = searchParams.get('v');\n if (v) return v;\n\n return null;\n};\n\n/**\n * Extract YouTube video ID from URL\n *\n * @param url - The YouTube URL\n * @returns The video ID if found, null otherwise\n */\nexport const getYouTubeVideoId = (url: string): string | null => {\n try {\n const u = new URL(url);\n const hostType = isValidYouTubeHost(u.hostname.toLowerCase());\n\n if (!hostType) return null;\n\n if (hostType === 'youtu.be') {\n return extractYoutuBeId(u.pathname);\n }\n\n return extractYouTubeId(u.pathname, u.searchParams);\n } catch {\n return null;\n }\n};\n\n/**\n * Generate YouTube embed URL for iframe\n *\n * @param videoId - The YouTube video ID\n * @returns The embed URL for the video\n */\nexport const getYouTubeEmbedUrl = (videoId: string): string => {\n return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=0&rel=0&modestbranding=1`;\n};\n"],"mappings":"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","names":["import_jsx_runtime","SIZE_CLASSES"]}