@vite-mf-monorepo/ui 0.4.5 → 0.4.6
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/dist/Avatar/Avatar.d.ts +19 -0
- package/dist/Avatar/Avatar.js +62 -0
- package/dist/Avatar/Avatar.js.map +1 -0
- package/dist/Avatar/index.d.ts +3 -0
- package/dist/Avatar/index.js +5 -0
- package/dist/Avatar/index.js.map +1 -0
- package/dist/Badge/Badge.d.ts +23 -0
- package/dist/Badge/Badge.js +48 -0
- package/dist/Badge/Badge.js.map +1 -0
- package/dist/Badge/index.d.ts +4 -0
- package/dist/Badge/index.js +5 -0
- package/dist/Badge/index.js.map +1 -0
- package/dist/Button/Button.utils.d.ts +23 -0
- package/dist/{chunk-IUGKH376.js → Button/Button.utils.js} +8 -7
- package/dist/Button/Button.utils.js.map +1 -0
- package/dist/Button/index.d.ts +6 -26
- package/dist/Button/index.js +4 -5
- package/dist/Button/index.js.map +1 -1
- package/dist/Card/Card.d.ts +11 -0
- package/dist/{chunk-RZU2FFBW.js → Card/Card.js} +7 -8
- package/dist/Card/Card.js.map +1 -0
- package/dist/Card/index.d.ts +3 -11
- package/dist/Card/index.js +4 -2
- package/dist/Card/index.js.map +1 -1
- package/dist/Carousel/Carousel.d.ts +65 -0
- package/dist/Carousel/Carousel.js +268 -0
- package/dist/Carousel/Carousel.js.map +1 -0
- package/dist/Carousel/CarouselCounter.d.ts +17 -0
- package/dist/Carousel/CarouselCounter.js +28 -0
- package/dist/Carousel/CarouselCounter.js.map +1 -0
- package/dist/Carousel/CarouselError.d.ts +8 -0
- package/dist/Carousel/CarouselError.js +18 -0
- package/dist/Carousel/CarouselError.js.map +1 -0
- package/dist/Carousel/CarouselItem.d.ts +16 -0
- package/dist/Carousel/CarouselItem.js +28 -0
- package/dist/Carousel/CarouselItem.js.map +1 -0
- package/dist/Carousel/CarouselLoading.d.ts +25 -0
- package/dist/Carousel/CarouselLoading.js +63 -0
- package/dist/Carousel/CarouselLoading.js.map +1 -0
- package/dist/Carousel/CarouselNavigation.d.ts +31 -0
- package/dist/Carousel/CarouselNavigation.js +97 -0
- package/dist/Carousel/CarouselNavigation.js.map +1 -0
- package/dist/Carousel/CarouselPagination.d.ts +20 -0
- package/dist/Carousel/CarouselPagination.js +29 -0
- package/dist/Carousel/CarouselPagination.js.map +1 -0
- package/dist/Carousel/index.d.ts +8 -0
- package/dist/Carousel/index.js +15 -0
- package/dist/Carousel/index.js.map +1 -0
- package/dist/HeroImage/HeroImage.d.ts +15 -0
- package/dist/HeroImage/HeroImage.js +47 -0
- package/dist/HeroImage/HeroImage.js.map +1 -0
- package/dist/HeroImage/index.d.ts +2 -0
- package/dist/HeroImage/index.js +5 -0
- package/dist/HeroImage/index.js.map +1 -0
- package/dist/Icon/Icon.d.ts +14 -0
- package/dist/Icon/Icon.js +204 -0
- package/dist/Icon/Icon.js.map +1 -0
- package/dist/Icon/index.d.ts +3 -14
- package/dist/Icon/index.js +4 -2
- package/dist/Icon/index.js.map +1 -1
- package/dist/IconButton/IconButton.d.ts +17 -0
- package/dist/IconButton/IconButton.js +44 -0
- package/dist/IconButton/IconButton.js.map +1 -0
- package/dist/IconButton/index.d.ts +4 -0
- package/dist/IconButton/index.js +5 -0
- package/dist/IconButton/index.js.map +1 -0
- package/dist/{MovieCard.utils-D8i4d7qA.d.ts → Image/Image.d.ts} +1 -28
- package/dist/Image/Image.js +125 -0
- package/dist/Image/Image.js.map +1 -0
- package/dist/Image/index.d.ts +3 -0
- package/dist/Image/index.js +5 -0
- package/dist/Image/index.js.map +1 -0
- package/dist/Modal/Modal.d.ts +20 -0
- package/dist/Modal/Modal.js +55 -0
- package/dist/Modal/Modal.js.map +1 -0
- package/dist/Modal/index.d.ts +3 -0
- package/dist/Modal/index.js +5 -0
- package/dist/Modal/index.js.map +1 -0
- package/dist/MovieCard/MovieCard.utils.d.ts +34 -0
- package/dist/MovieCard/MovieCard.utils.js +20 -0
- package/dist/MovieCard/MovieCard.utils.js.map +1 -0
- package/dist/MovieCard/MovieCardContent.d.ts +18 -0
- package/dist/MovieCard/MovieCardContent.js +75 -0
- package/dist/MovieCard/MovieCardContent.js.map +1 -0
- package/dist/MovieCard/index.d.ts +6 -0
- package/dist/MovieCard/index.js +5 -0
- package/dist/MovieCard/index.js.map +1 -0
- package/dist/Rating/CircleRating.d.ts +20 -0
- package/dist/Rating/CircleRating.js +75 -0
- package/dist/Rating/CircleRating.js.map +1 -0
- package/dist/Rating/Rating.d.ts +23 -0
- package/dist/Rating/Rating.js +41 -0
- package/dist/Rating/Rating.js.map +1 -0
- package/dist/Rating/StarsRating.d.ts +18 -0
- package/dist/Rating/StarsRating.js +47 -0
- package/dist/Rating/StarsRating.js.map +1 -0
- package/dist/Rating/index.d.ts +2 -0
- package/dist/Rating/index.js +5 -0
- package/dist/Rating/index.js.map +1 -0
- package/dist/Skeleton/Skeleton.d.ts +23 -0
- package/dist/{chunk-FJZK3PY6.js → Skeleton/Skeleton.js} +6 -7
- package/dist/Skeleton/Skeleton.js.map +1 -0
- package/dist/Skeleton/index.d.ts +3 -0
- package/dist/Skeleton/index.js +5 -0
- package/dist/Skeleton/index.js.map +1 -0
- package/dist/Spinner/Spinner.d.ts +8 -0
- package/dist/Spinner/Spinner.js +20 -0
- package/dist/Spinner/Spinner.js.map +1 -0
- package/dist/Spinner/index.d.ts +2 -0
- package/dist/Spinner/index.js +5 -0
- package/dist/Spinner/index.js.map +1 -0
- package/dist/Tabs/Tabs.d.ts +34 -0
- package/dist/Tabs/Tabs.js +47 -0
- package/dist/Tabs/Tabs.js.map +1 -0
- package/dist/Tabs/TabsContext.d.ts +21 -0
- package/dist/Tabs/TabsContext.js +16 -0
- package/dist/Tabs/TabsContext.js.map +1 -0
- package/dist/Tabs/TabsList.d.ts +7 -0
- package/dist/Tabs/TabsList.js +51 -0
- package/dist/Tabs/TabsList.js.map +1 -0
- package/dist/Tabs/TabsListContext.d.ts +16 -0
- package/dist/Tabs/TabsListContext.js +16 -0
- package/dist/Tabs/TabsListContext.js.map +1 -0
- package/dist/Tabs/TabsPanel.d.ts +17 -0
- package/dist/Tabs/TabsPanel.js +26 -0
- package/dist/Tabs/TabsPanel.js.map +1 -0
- package/dist/Tabs/TabsTrigger.d.ts +12 -0
- package/dist/Tabs/TabsTrigger.js +124 -0
- package/dist/Tabs/TabsTrigger.js.map +1 -0
- package/dist/Tabs/index.d.ts +6 -0
- package/dist/Tabs/index.js +5 -0
- package/dist/Tabs/index.js.map +1 -0
- package/dist/Talent/Talent.d.ts +19 -0
- package/dist/Talent/Talent.js +69 -0
- package/dist/Talent/Talent.js.map +1 -0
- package/dist/Talent/index.d.ts +4 -0
- package/dist/Talent/index.js +5 -0
- package/dist/Talent/index.js.map +1 -0
- package/dist/TrailerCard/TrailerCard.d.ts +15 -0
- package/dist/TrailerCard/TrailerCard.js +86 -0
- package/dist/TrailerCard/TrailerCard.js.map +1 -0
- package/dist/TrailerCard/index.d.ts +2 -0
- package/dist/TrailerCard/index.js +5 -0
- package/dist/TrailerCard/index.js.map +1 -0
- package/dist/Typography/Typography.d.ts +17 -0
- package/dist/Typography/Typography.js +55 -0
- package/dist/Typography/Typography.js.map +1 -0
- package/dist/Typography/index.d.ts +2 -0
- package/dist/Typography/index.js +5 -0
- package/dist/Typography/index.js.map +1 -0
- package/dist/index.d.ts +31 -372
- package/dist/index.js +50 -1099
- package/dist/index.js.map +1 -1
- package/dist/next/Button/Button.d.ts +9 -0
- package/dist/{chunk-ZTQU4GMY.js → next/Button/Button.js} +13 -10
- package/dist/next/Button/Button.js.map +1 -0
- package/dist/next/Button/Button.types.d.ts +26 -0
- package/dist/next/Button/Button.types.js +1 -0
- package/dist/next/Button/Button.types.js.map +1 -0
- package/dist/next/Button/index.d.ts +6 -0
- package/dist/next/Button/index.js +5 -0
- package/dist/next/Button/index.js.map +1 -0
- package/dist/next/HeroImage/HeroImage.d.ts +11 -0
- package/dist/next/HeroImage/HeroImage.js +44 -0
- package/dist/next/HeroImage/HeroImage.js.map +1 -0
- package/dist/next/HeroImage/index.d.ts +2 -0
- package/dist/next/HeroImage/index.js +5 -0
- package/dist/next/HeroImage/index.js.map +1 -0
- package/dist/next/Image/NextImage.d.ts +8 -0
- package/dist/next/Image/NextImage.js +84 -0
- package/dist/next/Image/NextImage.js.map +1 -0
- package/dist/next/Image/NextImage.types.d.ts +13 -0
- package/dist/next/Image/NextImage.types.js +1 -0
- package/dist/next/Image/NextImage.types.js.map +1 -0
- package/dist/next/Image/index.d.ts +5 -0
- package/dist/next/Image/index.js +5 -0
- package/dist/next/Image/index.js.map +1 -0
- package/dist/next/MovieCard/MovieCard.d.ts +9 -0
- package/dist/next/MovieCard/MovieCard.js +44 -0
- package/dist/next/MovieCard/MovieCard.js.map +1 -0
- package/dist/next/MovieCard/MovieCard.types.d.ts +15 -0
- package/dist/next/MovieCard/MovieCard.types.js +1 -0
- package/dist/next/MovieCard/MovieCard.types.js.map +1 -0
- package/dist/next/MovieCard/MovieCardContent.d.ts +19 -0
- package/dist/next/MovieCard/MovieCardContent.js +79 -0
- package/dist/next/MovieCard/MovieCardContent.js.map +1 -0
- package/dist/next/MovieCard/index.d.ts +6 -0
- package/dist/next/MovieCard/index.js +5 -0
- package/dist/next/MovieCard/index.js.map +1 -0
- package/dist/next/index.d.ts +14 -60
- package/dist/next/index.js +10 -270
- package/dist/next/index.js.map +1 -1
- package/dist/react-router/Button/Button.d.ts +9 -0
- package/dist/react-router/Button/Button.js +52 -0
- package/dist/react-router/Button/Button.js.map +1 -0
- package/dist/react-router/Button/Button.types.d.ts +24 -0
- package/dist/react-router/Button/Button.types.js +1 -0
- package/dist/react-router/Button/Button.types.js.map +1 -0
- package/dist/react-router/Button/index.d.ts +6 -0
- package/dist/react-router/Button/index.js +5 -0
- package/dist/react-router/Button/index.js.map +1 -0
- package/dist/react-router/MovieCard/MovieCard.d.ts +9 -0
- package/dist/react-router/MovieCard/MovieCard.js +42 -0
- package/dist/react-router/MovieCard/MovieCard.js.map +1 -0
- package/dist/react-router/MovieCard/MovieCard.types.d.ts +15 -0
- package/dist/react-router/MovieCard/MovieCard.types.js +1 -0
- package/dist/react-router/MovieCard/MovieCard.types.js.map +1 -0
- package/dist/react-router/MovieCard/index.d.ts +6 -0
- package/dist/react-router/MovieCard/index.js +5 -0
- package/dist/react-router/MovieCard/index.js.map +1 -0
- package/dist/react-router/index.d.ts +8 -17
- package/dist/react-router/index.js +6 -7
- package/dist/react-router/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6DP3KZQG.js +0 -214
- package/dist/chunk-6DP3KZQG.js.map +0 -1
- package/dist/chunk-6FBMTGXQ.js +0 -119
- package/dist/chunk-6FBMTGXQ.js.map +0 -1
- package/dist/chunk-7IAJQE27.js +0 -228
- package/dist/chunk-7IAJQE27.js.map +0 -1
- package/dist/chunk-DGJI4VNO.js +0 -3
- package/dist/chunk-DGJI4VNO.js.map +0 -1
- package/dist/chunk-FJZK3PY6.js.map +0 -1
- package/dist/chunk-IUGKH376.js.map +0 -1
- package/dist/chunk-RZU2FFBW.js.map +0 -1
- package/dist/chunk-ZTQU4GMY.js.map +0 -1
- package/dist/index.css +0 -1313
- package/dist/index.css.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/IconButton/index.ts"],"sourcesContent":["export { default as IconButton } from './IconButton'\nexport type { IconButtonProps } from './IconButton'\n"],"mappings":"AAAA,SAAoB,WAAXA,gBAA6B;","names":["default"]}
|
|
@@ -30,31 +30,4 @@ interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'placehol
|
|
|
30
30
|
}
|
|
31
31
|
declare function Image({ src, alt, blurDataUrl, autoBlur, blurSize, blurQuality, aspectRatio, fallback, className, onLoad, onError, loading, ...rest }: Readonly<ImageProps>): react_jsx_runtime.JSX.Element;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
/** Media ID */
|
|
35
|
-
id: number;
|
|
36
|
-
/** Media title */
|
|
37
|
-
title: string;
|
|
38
|
-
/** Poster URL (full URL or TMDB path) */
|
|
39
|
-
posterUrl: string;
|
|
40
|
-
/** Vote average (0-10) */
|
|
41
|
-
voteAverage: number;
|
|
42
|
-
/** Release year */
|
|
43
|
-
year?: number | null;
|
|
44
|
-
/** Additional class name */
|
|
45
|
-
className?: string;
|
|
46
|
-
/** Image loading strategy ('lazy' | 'eager'). Default: 'lazy' */
|
|
47
|
-
imageLoading?: ImageLoading;
|
|
48
|
-
/** Base64 blur data URL for next/image placeholder="blur" */
|
|
49
|
-
blurDataURL?: string;
|
|
50
|
-
}
|
|
51
|
-
interface MovieCardAsCard extends MovieCardBaseProps {
|
|
52
|
-
as?: 'card' | undefined;
|
|
53
|
-
onClick?: never;
|
|
54
|
-
}
|
|
55
|
-
interface MovieCardAsButton extends MovieCardBaseProps {
|
|
56
|
-
as: 'button';
|
|
57
|
-
onClick: () => void;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export { type AspectRatio as A, Image as I, type MovieCardAsCard as M, type MovieCardBaseProps as a, type MovieCardAsButton as b, type ImageProps as c, type ImageState as d };
|
|
33
|
+
export { type AspectRatio, type ImageLoading, type ImageProps, type ImageState, Image as default };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getBlurDataUrl } from "@vite-mf-monorepo/shared";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Icon } from "../Icon";
|
|
6
|
+
function Image({
|
|
7
|
+
src,
|
|
8
|
+
alt,
|
|
9
|
+
blurDataUrl,
|
|
10
|
+
autoBlur = false,
|
|
11
|
+
blurSize = 16,
|
|
12
|
+
blurQuality = 0.3,
|
|
13
|
+
aspectRatio = "2/3",
|
|
14
|
+
fallback,
|
|
15
|
+
className,
|
|
16
|
+
onLoad,
|
|
17
|
+
onError,
|
|
18
|
+
loading = "eager",
|
|
19
|
+
...rest
|
|
20
|
+
}) {
|
|
21
|
+
const imgRef = useRef(null);
|
|
22
|
+
const containerRef = useRef(null);
|
|
23
|
+
const [state, setState] = useState("loading");
|
|
24
|
+
const [generatedBlur, setGeneratedBlur] = useState(
|
|
25
|
+
void 0
|
|
26
|
+
);
|
|
27
|
+
const [blurReady, setBlurReady] = useState(!autoBlur || !!blurDataUrl);
|
|
28
|
+
const [isVisible, setIsVisible] = useState(loading === "eager");
|
|
29
|
+
const effectiveBlur = blurDataUrl ?? generatedBlur;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (loading === "eager" || !containerRef.current) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const observer = new IntersectionObserver(
|
|
35
|
+
([entry]) => {
|
|
36
|
+
if (entry.isIntersecting) {
|
|
37
|
+
setIsVisible(true);
|
|
38
|
+
observer.unobserve(entry.target);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{ rootMargin: "50px" }
|
|
42
|
+
);
|
|
43
|
+
observer.observe(containerRef.current);
|
|
44
|
+
return () => {
|
|
45
|
+
observer.disconnect();
|
|
46
|
+
};
|
|
47
|
+
}, [loading]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!autoBlur || blurDataUrl) {
|
|
50
|
+
setBlurReady(true);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setBlurReady(false);
|
|
54
|
+
getBlurDataUrl(src, blurSize, blurQuality).then((base64) => {
|
|
55
|
+
setGeneratedBlur(base64);
|
|
56
|
+
setBlurReady(true);
|
|
57
|
+
}).catch(() => {
|
|
58
|
+
setBlurReady(true);
|
|
59
|
+
});
|
|
60
|
+
}, [autoBlur, src, blurDataUrl, blurSize, blurQuality]);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setState("loading");
|
|
63
|
+
setGeneratedBlur(void 0);
|
|
64
|
+
if (imgRef.current?.complete && imgRef.current.naturalHeight !== 0) {
|
|
65
|
+
setState("loaded");
|
|
66
|
+
}
|
|
67
|
+
}, [src]);
|
|
68
|
+
const handleLoad = useCallback(() => {
|
|
69
|
+
setState("loaded");
|
|
70
|
+
onLoad?.();
|
|
71
|
+
}, [onLoad]);
|
|
72
|
+
const handleError = useCallback(() => {
|
|
73
|
+
setState("error");
|
|
74
|
+
onError?.();
|
|
75
|
+
}, [onError]);
|
|
76
|
+
const defaultFallback = /* @__PURE__ */ jsx("div", { className: "ui:flex ui:h-full ui:w-full ui:items-center ui:justify-center ui:bg-muted", children: /* @__PURE__ */ jsx(
|
|
77
|
+
Icon,
|
|
78
|
+
{
|
|
79
|
+
name: "Photo",
|
|
80
|
+
size: 48,
|
|
81
|
+
className: "ui:text-muted-foreground",
|
|
82
|
+
"aria-hidden": "true"
|
|
83
|
+
}
|
|
84
|
+
) });
|
|
85
|
+
return /* @__PURE__ */ jsx(
|
|
86
|
+
"div",
|
|
87
|
+
{
|
|
88
|
+
ref: containerRef,
|
|
89
|
+
className: clsx("ui:relative ui:overflow-hidden ui:bg-muted", className),
|
|
90
|
+
style: aspectRatio ? { aspectRatio } : void 0,
|
|
91
|
+
"data-state": state,
|
|
92
|
+
children: state === "error" ? fallback ?? defaultFallback : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
93
|
+
effectiveBlur && state === "loading" && /* @__PURE__ */ jsx(
|
|
94
|
+
"img",
|
|
95
|
+
{
|
|
96
|
+
src: effectiveBlur,
|
|
97
|
+
alt: "",
|
|
98
|
+
"aria-hidden": "true",
|
|
99
|
+
className: "ui:absolute ui:inset-0 ui:h-full ui:w-full ui:scale-105 ui:object-cover"
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
blurReady && isVisible && /* @__PURE__ */ jsx(
|
|
103
|
+
"img",
|
|
104
|
+
{
|
|
105
|
+
ref: imgRef,
|
|
106
|
+
src,
|
|
107
|
+
alt,
|
|
108
|
+
onLoad: handleLoad,
|
|
109
|
+
onError: handleError,
|
|
110
|
+
className: clsx(
|
|
111
|
+
"ui:absolute ui:inset-0 ui:h-full ui:w-full ui:object-cover ui:transition-opacity ui:duration-300",
|
|
112
|
+
state === "loaded" ? "ui:opacity-100" : "ui:opacity-0"
|
|
113
|
+
),
|
|
114
|
+
...rest
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
] })
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
var Image_default = Image;
|
|
122
|
+
export {
|
|
123
|
+
Image_default as default
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=Image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Image/Image.tsx"],"sourcesContent":["import { getBlurDataUrl } from '@vite-mf-monorepo/shared'\nimport clsx from 'clsx'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { Icon } from '../Icon'\n\nimport type { ImgHTMLAttributes, ReactNode } from 'react'\n\nexport type ImageState = 'loading' | 'loaded' | 'error'\n\nexport type AspectRatio = '2/3' | '16/9' | '1/1' | '4/3' | '3/2'\n\nexport type ImageLoading = 'lazy' | 'eager'\n\nexport interface ImageProps\n extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'placeholder'> {\n /** Image source URL */\n src: string\n /** Alt text for accessibility */\n alt: string\n /** Pre-generated blur data URL (base64) */\n blurDataUrl?: string\n /** Auto-generate blur placeholder from src using Canvas API */\n autoBlur?: boolean\n /** Size of blur canvas (smaller = more blur). Default: 16 */\n blurSize?: number\n /** JPEG quality for blur (0-1). Default: 0.3 */\n blurQuality?: number\n /** Aspect ratio of the image container */\n aspectRatio?: AspectRatio | (string & {})\n /** Fallback content when image fails to load */\n fallback?: ReactNode\n /** Callback when image loads successfully */\n onLoad?: () => void\n /** Callback when image fails to load */\n onError?: () => void\n /** Load strategy: 'lazy' waits for viewport visibility, 'eager' loads immediately. Default: 'eager' */\n loading?: ImageLoading\n}\n\nfunction Image({\n src,\n alt,\n blurDataUrl,\n autoBlur = false,\n blurSize = 16,\n blurQuality = 0.3,\n aspectRatio = '2/3',\n fallback,\n className,\n onLoad,\n onError,\n loading = 'eager',\n ...rest\n}: Readonly<ImageProps>) {\n const imgRef = useRef<HTMLImageElement>(null)\n const containerRef = useRef<HTMLDivElement>(null)\n const [state, setState] = useState<ImageState>('loading')\n const [generatedBlur, setGeneratedBlur] = useState<string | undefined>(\n undefined\n )\n const [blurReady, setBlurReady] = useState(!autoBlur || !!blurDataUrl)\n const [isVisible, setIsVisible] = useState(loading === 'eager')\n\n const effectiveBlur = blurDataUrl ?? generatedBlur\n\n // Lazy load with IntersectionObserver\n useEffect(() => {\n if (loading === 'eager' || !containerRef.current) {\n return\n }\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setIsVisible(true)\n observer.unobserve(entry.target)\n }\n },\n { rootMargin: '50px' }\n )\n\n observer.observe(containerRef.current)\n\n return () => {\n observer.disconnect()\n }\n }, [loading])\n\n useEffect(() => {\n if (!autoBlur || blurDataUrl) {\n setBlurReady(true)\n return\n }\n\n setBlurReady(false)\n\n getBlurDataUrl(src, blurSize, blurQuality)\n .then((base64) => {\n setGeneratedBlur(base64)\n setBlurReady(true)\n })\n .catch(() => {\n setBlurReady(true)\n })\n }, [autoBlur, src, blurDataUrl, blurSize, blurQuality])\n\n useEffect(() => {\n setState('loading')\n setGeneratedBlur(undefined)\n\n // Check if image is already loaded from cache\n if (imgRef.current?.complete && imgRef.current.naturalHeight !== 0) {\n setState('loaded')\n }\n }, [src])\n\n const handleLoad = useCallback(() => {\n setState('loaded')\n onLoad?.()\n }, [onLoad])\n\n const handleError = useCallback(() => {\n setState('error')\n onError?.()\n }, [onError])\n\n const defaultFallback = (\n <div className=\"ui:flex ui:h-full ui:w-full ui:items-center ui:justify-center ui:bg-muted\">\n <Icon\n name=\"Photo\"\n size={48}\n className=\"ui:text-muted-foreground\"\n aria-hidden=\"true\"\n />\n </div>\n )\n\n return (\n <div\n ref={containerRef}\n className={clsx('ui:relative ui:overflow-hidden ui:bg-muted', className)}\n style={aspectRatio ? { aspectRatio } : undefined}\n data-state={state}\n >\n {state === 'error' ? (\n (fallback ?? defaultFallback)\n ) : (\n <>\n {effectiveBlur && state === 'loading' && (\n <img\n src={effectiveBlur}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"ui:absolute ui:inset-0 ui:h-full ui:w-full ui:scale-105 ui:object-cover\"\n />\n )}\n\n {blurReady && isVisible && (\n <img\n ref={imgRef}\n src={src}\n alt={alt}\n onLoad={handleLoad}\n onError={handleError}\n className={clsx(\n 'ui:absolute ui:inset-0 ui:h-full ui:w-full ui:object-cover ui:transition-opacity ui:duration-300',\n state === 'loaded' ? 'ui:opacity-100' : 'ui:opacity-0'\n )}\n {...rest}\n />\n )}\n </>\n )}\n </div>\n )\n}\n\nexport default Image\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Image/index.ts"],"sourcesContent":["export { default as Image } from './Image'\nexport type { AspectRatio, ImageLoading, ImageProps, ImageState } from './Image'\n"],"mappings":"AAAA,SAAoB,WAAXA,gBAAwB;","names":["default"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface ModalProps {
|
|
5
|
+
/** Whether the modal is open */
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
/** Callback when modal should close (ESC key, backdrop click) */
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
/** Modal content */
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
/** Accessible label for screen readers (required) */
|
|
12
|
+
'aria-label': string;
|
|
13
|
+
/** Additional class name for the dialog element */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Optional callback for backdrop click. Falls back to onClose if not provided. */
|
|
16
|
+
onOverlayClick?: () => void;
|
|
17
|
+
}
|
|
18
|
+
declare function Modal({ isOpen, onClose, children, 'aria-label': ariaLabel, className, onOverlayClick, }: Readonly<ModalProps>): react_jsx_runtime.JSX.Element;
|
|
19
|
+
|
|
20
|
+
export { type ModalProps, Modal as default };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
function Modal({
|
|
5
|
+
isOpen,
|
|
6
|
+
onClose,
|
|
7
|
+
children,
|
|
8
|
+
"aria-label": ariaLabel,
|
|
9
|
+
className,
|
|
10
|
+
onOverlayClick
|
|
11
|
+
}) {
|
|
12
|
+
const ref = useRef(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const dialog = ref.current;
|
|
15
|
+
if (!dialog) return;
|
|
16
|
+
if (isOpen) {
|
|
17
|
+
dialog.showModal();
|
|
18
|
+
document.body.style.overflow = "hidden";
|
|
19
|
+
} else {
|
|
20
|
+
dialog.close();
|
|
21
|
+
document.body.style.overflow = "";
|
|
22
|
+
}
|
|
23
|
+
return () => {
|
|
24
|
+
document.body.style.overflow = "";
|
|
25
|
+
};
|
|
26
|
+
}, [isOpen]);
|
|
27
|
+
const handleClick = (e) => {
|
|
28
|
+
if (e.target === ref.current) (onOverlayClick ?? onClose)();
|
|
29
|
+
};
|
|
30
|
+
const handleClose = () => {
|
|
31
|
+
onClose();
|
|
32
|
+
};
|
|
33
|
+
return /* @__PURE__ */ jsx(
|
|
34
|
+
"dialog",
|
|
35
|
+
{
|
|
36
|
+
ref,
|
|
37
|
+
"aria-label": ariaLabel,
|
|
38
|
+
"aria-modal": "true",
|
|
39
|
+
onClick: handleClick,
|
|
40
|
+
onClose: handleClose,
|
|
41
|
+
className: clsx(
|
|
42
|
+
"ui:backdrop:bg-black/80",
|
|
43
|
+
"ui:bg-transparent ui:border-0 ui:p-0",
|
|
44
|
+
"ui:max-w-none ui:max-h-none ui:w-full ui:h-full",
|
|
45
|
+
className
|
|
46
|
+
),
|
|
47
|
+
children
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
var Modal_default = Modal;
|
|
52
|
+
export {
|
|
53
|
+
Modal_default as default
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=Modal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Modal/Modal.tsx"],"sourcesContent":["import clsx from 'clsx'\nimport { useEffect, useRef } from 'react'\n\nimport type { MouseEvent, ReactNode } from 'react'\n\nexport interface ModalProps {\n /** Whether the modal is open */\n isOpen: boolean\n /** Callback when modal should close (ESC key, backdrop click) */\n onClose: () => void\n /** Modal content */\n children: ReactNode\n /** Accessible label for screen readers (required) */\n 'aria-label': string\n /** Additional class name for the dialog element */\n className?: string\n /** Optional callback for backdrop click. Falls back to onClose if not provided. */\n onOverlayClick?: () => void\n}\n\nfunction Modal({\n isOpen,\n onClose,\n children,\n 'aria-label': ariaLabel,\n className,\n onOverlayClick,\n}: Readonly<ModalProps>) {\n const ref = useRef<HTMLDialogElement>(null)\n\n /**\n * Effect: Opens or closes the native <dialog> element and locks body scroll.\n * - showModal() places dialog in the top layer (above all MFE remotes, no z-index needed).\n * - Scroll lock is manual — <dialog> does not lock scroll natively.\n * - Cleanup restores overflow in case the component unmounts while open.\n */\n useEffect(() => {\n const dialog = ref.current\n if (!dialog) return\n\n if (isOpen) {\n dialog.showModal()\n document.body.style.overflow = 'hidden'\n } else {\n dialog.close()\n document.body.style.overflow = ''\n }\n\n return () => {\n document.body.style.overflow = ''\n }\n }, [isOpen])\n\n /**\n * Closes the modal when clicking the <dialog> element itself (the backdrop area).\n * e.target === ref.current only when clicking outside the dialog content,\n * since content clicks bubble up to a child, not to the dialog element directly.\n */\n const handleClick = (e: MouseEvent<HTMLDialogElement>) => {\n if (e.target === ref.current) (onOverlayClick ?? onClose)()\n }\n\n /**\n * Syncs the native ESC key close event (fired by the browser on <dialog>)\n * with the onClose callback, so parent state stays in sync.\n */\n const handleClose = () => {\n onClose()\n }\n\n return (\n <dialog\n ref={ref}\n aria-label={ariaLabel}\n aria-modal=\"true\"\n onClick={handleClick}\n onClose={handleClose}\n className={clsx(\n 'ui:backdrop:bg-black/80',\n 'ui:bg-transparent ui:border-0 ui:p-0',\n 'ui:max-w-none ui:max-h-none ui:w-full ui:h-full',\n className\n )}\n >\n {children}\n </dialog>\n )\n}\n\nexport default Modal\n"],"mappings":"AAuEI;AAvEJ,OAAO,UAAU;AACjB,SAAS,WAAW,cAAc;AAmBlC,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,MAAM,OAA0B,IAAI;AAQ1C,YAAU,MAAM;AACd,UAAM,SAAS,IAAI;AACnB,QAAI,CAAC,OAAQ;AAEb,QAAI,QAAQ;AACV,aAAO,UAAU;AACjB,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC,OAAO;AACL,aAAO,MAAM;AACb,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC;AAEA,WAAO,MAAM;AACX,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAOX,QAAM,cAAc,CAAC,MAAqC;AACxD,QAAI,EAAE,WAAW,IAAI,QAAS,EAAC,kBAAkB,SAAS;AAAA,EAC5D;AAMA,QAAM,cAAc,MAAM;AACxB,YAAQ;AAAA,EACV;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,cAAY;AAAA,MACZ,cAAW;AAAA,MACX,SAAS;AAAA,MACT,SAAS;AAAA,MACT,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,gBAAQ;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Modal/index.ts"],"sourcesContent":["export { default as Modal } from './Modal'\nexport type { ModalProps } from './Modal'\n"],"mappings":"AAAA,SAAoB,WAAXA,gBAAwB;","names":["default"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ImageLoading } from '../Image/Image.js';
|
|
2
|
+
import 'react/jsx-runtime';
|
|
3
|
+
import 'react';
|
|
4
|
+
|
|
5
|
+
interface MovieCardBaseProps {
|
|
6
|
+
/** Media ID */
|
|
7
|
+
id: number;
|
|
8
|
+
/** Media title */
|
|
9
|
+
title: string;
|
|
10
|
+
/** Poster URL (full URL or TMDB path) */
|
|
11
|
+
posterUrl: string;
|
|
12
|
+
/** Vote average (0-10) */
|
|
13
|
+
voteAverage: number;
|
|
14
|
+
/** Release year */
|
|
15
|
+
year?: number | null;
|
|
16
|
+
/** Additional class name */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Image loading strategy ('lazy' | 'eager'). Default: 'lazy' */
|
|
19
|
+
imageLoading?: ImageLoading;
|
|
20
|
+
/** Base64 blur data URL for next/image placeholder="blur" */
|
|
21
|
+
blurDataURL?: string;
|
|
22
|
+
}
|
|
23
|
+
interface MovieCardAsCard extends MovieCardBaseProps {
|
|
24
|
+
as?: 'card' | undefined;
|
|
25
|
+
onClick?: never;
|
|
26
|
+
}
|
|
27
|
+
interface MovieCardAsButton extends MovieCardBaseProps {
|
|
28
|
+
as: 'button';
|
|
29
|
+
onClick: () => void;
|
|
30
|
+
}
|
|
31
|
+
declare function getMovieCardClasses(isInteractive: boolean, className?: string): string;
|
|
32
|
+
declare function getMovieCardLinkClasses(): string;
|
|
33
|
+
|
|
34
|
+
export { type MovieCardAsButton, type MovieCardAsCard, type MovieCardBaseProps, getMovieCardClasses, getMovieCardLinkClasses };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
function getMovieCardClasses(isInteractive, className) {
|
|
3
|
+
return clsx(
|
|
4
|
+
"ui:group ui:relative ui:flex ui:flex-col ui:overflow-hidden",
|
|
5
|
+
isInteractive && [
|
|
6
|
+
"ui:cursor-pointer",
|
|
7
|
+
"ui:transition-transform ui:duration-200",
|
|
8
|
+
"hover:ui:scale-[1.02]"
|
|
9
|
+
],
|
|
10
|
+
className
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
function getMovieCardLinkClasses() {
|
|
14
|
+
return "ui:block ui:no-underline ui:text-inherit";
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
getMovieCardClasses,
|
|
18
|
+
getMovieCardLinkClasses
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=MovieCard.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/MovieCard/MovieCard.utils.ts"],"sourcesContent":["import clsx from 'clsx'\n\nimport type { ImageLoading } from '../Image'\n\nexport interface MovieCardBaseProps {\n /** Media ID */\n id: number\n /** Media title */\n title: string\n /** Poster URL (full URL or TMDB path) */\n posterUrl: string\n /** Vote average (0-10) */\n voteAverage: number\n /** Release year */\n year?: number | null\n /** Additional class name */\n className?: string\n /** Image loading strategy ('lazy' | 'eager'). Default: 'lazy' */\n imageLoading?: ImageLoading\n /** Base64 blur data URL for next/image placeholder=\"blur\" */\n blurDataURL?: string\n}\n\nexport interface MovieCardAsCard extends MovieCardBaseProps {\n as?: 'card' | undefined\n onClick?: never\n}\n\nexport interface MovieCardAsButton extends MovieCardBaseProps {\n as: 'button'\n onClick: () => void\n}\n\nexport function getMovieCardClasses(\n isInteractive: boolean,\n className?: string\n) {\n return clsx(\n 'ui:group ui:relative ui:flex ui:flex-col ui:overflow-hidden',\n isInteractive && [\n 'ui:cursor-pointer',\n 'ui:transition-transform ui:duration-200',\n 'hover:ui:scale-[1.02]',\n ],\n className\n )\n}\n\nexport function getMovieCardLinkClasses() {\n return 'ui:block ui:no-underline ui:text-inherit'\n}\n"],"mappings":"AAAA,OAAO,UAAU;AAiCV,SAAS,oBACd,eACA,WACA;AACA,SAAO;AAAA,IACL;AAAA,IACA,iBAAiB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,EACF;AACF;AAEO,SAAS,0BAA0B;AACxC,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ImageLoading } from '../Image/Image.js';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
interface MovieCardContentProps {
|
|
6
|
+
id: number;
|
|
7
|
+
title: string;
|
|
8
|
+
posterUrl: string;
|
|
9
|
+
voteAverage: number;
|
|
10
|
+
year?: number | null;
|
|
11
|
+
className?: string;
|
|
12
|
+
imageLoading?: ImageLoading;
|
|
13
|
+
isInteractive: boolean;
|
|
14
|
+
onClick?: (() => void) | undefined;
|
|
15
|
+
}
|
|
16
|
+
declare function MovieCardContent({ id, title, posterUrl, voteAverage, year, className, imageLoading, isInteractive, onClick, }: Readonly<MovieCardContentProps>): ReactNode;
|
|
17
|
+
|
|
18
|
+
export { MovieCardContent as default };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Card } from "../Card";
|
|
3
|
+
import { Image } from "../Image";
|
|
4
|
+
import { Rating } from "../Rating";
|
|
5
|
+
import { Typography } from "../Typography";
|
|
6
|
+
import { getMovieCardClasses } from "./MovieCard.utils";
|
|
7
|
+
function MovieCardContent({
|
|
8
|
+
id,
|
|
9
|
+
title,
|
|
10
|
+
posterUrl,
|
|
11
|
+
voteAverage,
|
|
12
|
+
year,
|
|
13
|
+
className,
|
|
14
|
+
imageLoading = "lazy",
|
|
15
|
+
isInteractive,
|
|
16
|
+
onClick
|
|
17
|
+
}) {
|
|
18
|
+
return /* @__PURE__ */ jsxs(
|
|
19
|
+
Card,
|
|
20
|
+
{
|
|
21
|
+
variant: "ghost",
|
|
22
|
+
className: getMovieCardClasses(isInteractive, className),
|
|
23
|
+
onClick,
|
|
24
|
+
"data-testid": `movie-card-${String(id)}`,
|
|
25
|
+
children: [
|
|
26
|
+
/* @__PURE__ */ jsxs("div", { className: "ui:relative ui:aspect-[2/3] ui:w-full ui:overflow-hidden ui:rounded-md ui:bg-gray-200", children: [
|
|
27
|
+
/* @__PURE__ */ jsx(
|
|
28
|
+
Image,
|
|
29
|
+
{
|
|
30
|
+
src: posterUrl,
|
|
31
|
+
alt: title,
|
|
32
|
+
loading: imageLoading,
|
|
33
|
+
aspectRatio: void 0,
|
|
34
|
+
className: "ui:h-full ui:w-full ui:object-cover"
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
/* @__PURE__ */ jsx("div", { className: "ui:absolute ui:bottom-2 ui:right-2 ui:flex ui:items-center ui:justify-center ui:rounded-full ui:bg-white/80 ui:p-1", children: /* @__PURE__ */ jsx(
|
|
38
|
+
Rating,
|
|
39
|
+
{
|
|
40
|
+
value: voteAverage,
|
|
41
|
+
size: "sm",
|
|
42
|
+
variant: "circle",
|
|
43
|
+
trackClassName: "ui:text-gray-200",
|
|
44
|
+
className: "ui:drop-shadow"
|
|
45
|
+
}
|
|
46
|
+
) })
|
|
47
|
+
] }),
|
|
48
|
+
/* @__PURE__ */ jsxs("div", { className: "ui:mt-2 ui:flex ui:flex-col ui:gap-0.5 ui:px-1", children: [
|
|
49
|
+
/* @__PURE__ */ jsx(
|
|
50
|
+
Typography,
|
|
51
|
+
{
|
|
52
|
+
variant: "label",
|
|
53
|
+
as: "h3",
|
|
54
|
+
className: "ui:line-clamp-2",
|
|
55
|
+
title,
|
|
56
|
+
children: title
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
year && /* @__PURE__ */ jsx(
|
|
60
|
+
Typography,
|
|
61
|
+
{
|
|
62
|
+
variant: "caption-xs",
|
|
63
|
+
className: "ui:[.media-section:nth-of-type(odd)_&]:text-badge-foreground",
|
|
64
|
+
children: year
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
] })
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
MovieCardContent as default
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=MovieCardContent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/MovieCard/MovieCardContent.tsx"],"sourcesContent":["import { Card } from '../Card'\nimport { Image } from '../Image'\nimport { Rating } from '../Rating'\nimport { Typography } from '../Typography'\n\nimport { getMovieCardClasses } from './MovieCard.utils'\n\nimport type { ImageLoading } from '../Image'\nimport type { ReactNode } from 'react'\n\ninterface MovieCardContentProps {\n id: number\n title: string\n posterUrl: string\n voteAverage: number\n year?: number | null\n className?: string\n imageLoading?: ImageLoading\n isInteractive: boolean\n onClick?: (() => void) | undefined\n}\n\nexport default function MovieCardContent({\n id,\n title,\n posterUrl,\n voteAverage,\n year,\n className,\n imageLoading = 'lazy',\n isInteractive,\n onClick,\n}: Readonly<MovieCardContentProps>): ReactNode {\n return (\n <Card\n variant=\"ghost\"\n className={getMovieCardClasses(isInteractive, className)}\n onClick={onClick}\n data-testid={`movie-card-${String(id)}`}\n >\n <div className=\"ui:relative ui:aspect-[2/3] ui:w-full ui:overflow-hidden ui:rounded-md ui:bg-gray-200\">\n <Image\n src={posterUrl}\n alt={title}\n loading={imageLoading}\n aspectRatio={undefined}\n className=\"ui:h-full ui:w-full ui:object-cover\"\n />\n <div className=\"ui:absolute ui:bottom-2 ui:right-2 ui:flex ui:items-center ui:justify-center ui:rounded-full ui:bg-white/80 ui:p-1\">\n <Rating\n value={voteAverage}\n size=\"sm\"\n variant=\"circle\"\n trackClassName=\"ui:text-gray-200\"\n className=\"ui:drop-shadow\"\n />\n </div>\n </div>\n\n <div className=\"ui:mt-2 ui:flex ui:flex-col ui:gap-0.5 ui:px-1\">\n <Typography\n variant=\"label\"\n as=\"h3\"\n className=\"ui:line-clamp-2\"\n title={title}\n >\n {title}\n </Typography>\n {year && (\n <Typography\n variant=\"caption-xs\"\n className=\"ui:[.media-section:nth-of-type(odd)_&]:text-badge-foreground\"\n >\n {year}\n </Typography>\n )}\n </div>\n </Card>\n )\n}\n"],"mappings":"AAwCM,SACE,KADF;AAxCN,SAAS,YAAY;AACrB,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AAiBrB,SAAR,iBAAkC;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AACF,GAA+C;AAC7C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAW,oBAAoB,eAAe,SAAS;AAAA,MACvD;AAAA,MACA,eAAa,cAAc,OAAO,EAAE,CAAC;AAAA,MAErC;AAAA,6BAAC,SAAI,WAAU,yFACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,KAAK;AAAA,cACL,KAAK;AAAA,cACL,SAAS;AAAA,cACT,aAAa;AAAA,cACb,WAAU;AAAA;AAAA,UACZ;AAAA,UACA,oBAAC,SAAI,WAAU,sHACb;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,gBAAe;AAAA,cACf,WAAU;AAAA;AAAA,UACZ,GACF;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,kDACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,IAAG;AAAA,cACH,WAAU;AAAA,cACV;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UACC,QACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,WAAU;AAAA,cAET;AAAA;AAAA,UACH;AAAA,WAEJ;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as MovieCard } from '../react-router/MovieCard/MovieCard.js';
|
|
2
|
+
export { MovieCardAsLink, MovieCardProps } from '../react-router/MovieCard/MovieCard.types.js';
|
|
3
|
+
export { MovieCardAsButton, MovieCardAsCard, MovieCardBaseProps } from './MovieCard.utils.js';
|
|
4
|
+
import 'react/jsx-runtime';
|
|
5
|
+
import '../Image/Image.js';
|
|
6
|
+
import 'react';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/MovieCard/index.ts"],"sourcesContent":["export { MovieCard } from '../react-router/MovieCard'\nexport type {\n MovieCardProps,\n MovieCardBaseProps,\n MovieCardAsCard,\n MovieCardAsLink,\n MovieCardAsButton,\n} from '../react-router/MovieCard'\n"],"mappings":"AAAA,SAAS,iBAAiB;","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { RatingSize } from './Rating.js';
|
|
3
|
+
|
|
4
|
+
interface CircleRatingProps {
|
|
5
|
+
/** Percentage value (0-100) for the progress ring */
|
|
6
|
+
percent: number;
|
|
7
|
+
/** Size of the rating circle */
|
|
8
|
+
size: RatingSize;
|
|
9
|
+
/** Whether to display the numeric value in the center */
|
|
10
|
+
showValue: boolean;
|
|
11
|
+
/** The actual rating value (used for display) */
|
|
12
|
+
value: number;
|
|
13
|
+
/** Maximum rating value (used for display formatting) */
|
|
14
|
+
max: number;
|
|
15
|
+
/** Custom class for the background track circle */
|
|
16
|
+
trackClassName?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function CircleRating({ percent, size, showValue, value, max, trackClassName, }: Readonly<CircleRatingProps>): react_jsx_runtime.JSX.Element;
|
|
19
|
+
|
|
20
|
+
export { type CircleRatingProps, CircleRating as default };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
const circleSizeMap = {
|
|
4
|
+
sm: { size: 32, stroke: 3, fontSize: "ui:text-xs" },
|
|
5
|
+
md: { size: 48, stroke: 4, fontSize: "ui:text-sm" },
|
|
6
|
+
lg: { size: 64, stroke: 5, fontSize: "ui:text-base" }
|
|
7
|
+
};
|
|
8
|
+
const getColorClass = (percent) => {
|
|
9
|
+
if (percent >= 70) return "ui:text-green-600";
|
|
10
|
+
if (percent >= 40) return "ui:text-amber-600";
|
|
11
|
+
return "ui:text-red-500";
|
|
12
|
+
};
|
|
13
|
+
function CircleRating({
|
|
14
|
+
percent,
|
|
15
|
+
size,
|
|
16
|
+
showValue,
|
|
17
|
+
value,
|
|
18
|
+
max,
|
|
19
|
+
trackClassName
|
|
20
|
+
}) {
|
|
21
|
+
const { size: svgSize, stroke, fontSize } = circleSizeMap[size];
|
|
22
|
+
const radius = (svgSize - stroke) / 2;
|
|
23
|
+
const circumference = 2 * Math.PI * radius;
|
|
24
|
+
const offset = circumference - percent / 100 * circumference;
|
|
25
|
+
return /* @__PURE__ */ jsxs("div", { className: "ui:relative ui:inline-flex ui:items-center ui:justify-center", children: [
|
|
26
|
+
/* @__PURE__ */ jsxs("svg", { width: svgSize, height: svgSize, className: "ui:-rotate-90", children: [
|
|
27
|
+
/* @__PURE__ */ jsx(
|
|
28
|
+
"circle",
|
|
29
|
+
{
|
|
30
|
+
cx: svgSize / 2,
|
|
31
|
+
cy: svgSize / 2,
|
|
32
|
+
r: radius,
|
|
33
|
+
fill: "none",
|
|
34
|
+
stroke: "currentColor",
|
|
35
|
+
strokeWidth: stroke,
|
|
36
|
+
className: trackClassName ?? "ui:text-gray-200"
|
|
37
|
+
}
|
|
38
|
+
),
|
|
39
|
+
/* @__PURE__ */ jsx(
|
|
40
|
+
"circle",
|
|
41
|
+
{
|
|
42
|
+
cx: svgSize / 2,
|
|
43
|
+
cy: svgSize / 2,
|
|
44
|
+
r: radius,
|
|
45
|
+
fill: "none",
|
|
46
|
+
stroke: "currentColor",
|
|
47
|
+
strokeWidth: stroke,
|
|
48
|
+
strokeLinecap: "round",
|
|
49
|
+
strokeDasharray: circumference,
|
|
50
|
+
strokeDashoffset: offset,
|
|
51
|
+
className: clsx(
|
|
52
|
+
"ui:transition-all ui:duration-500",
|
|
53
|
+
getColorClass(percent)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
] }),
|
|
58
|
+
showValue && /* @__PURE__ */ jsx(
|
|
59
|
+
"span",
|
|
60
|
+
{
|
|
61
|
+
className: clsx(
|
|
62
|
+
"ui:absolute ui:font-bold",
|
|
63
|
+
fontSize,
|
|
64
|
+
getColorClass(percent)
|
|
65
|
+
),
|
|
66
|
+
children: max === 100 ? Math.round(percent) : value.toFixed(1)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
] });
|
|
70
|
+
}
|
|
71
|
+
var CircleRating_default = CircleRating;
|
|
72
|
+
export {
|
|
73
|
+
CircleRating_default as default
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=CircleRating.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Rating/CircleRating.tsx"],"sourcesContent":["import clsx from 'clsx'\n\nimport type { RatingSize } from './Rating'\n\nconst circleSizeMap: Record<\n RatingSize,\n { size: number; stroke: number; fontSize: string }\n> = {\n sm: { size: 32, stroke: 3, fontSize: 'ui:text-xs' },\n md: { size: 48, stroke: 4, fontSize: 'ui:text-sm' },\n lg: { size: 64, stroke: 5, fontSize: 'ui:text-base' },\n}\n\nconst getColorClass = (percent: number): string => {\n if (percent >= 70) return 'ui:text-green-600'\n if (percent >= 40) return 'ui:text-amber-600'\n return 'ui:text-red-500'\n}\n\nexport interface CircleRatingProps {\n /** Percentage value (0-100) for the progress ring */\n percent: number\n /** Size of the rating circle */\n size: RatingSize\n /** Whether to display the numeric value in the center */\n showValue: boolean\n /** The actual rating value (used for display) */\n value: number\n /** Maximum rating value (used for display formatting) */\n max: number\n /** Custom class for the background track circle */\n trackClassName?: string\n}\n\nfunction CircleRating({\n percent,\n size,\n showValue,\n value,\n max,\n trackClassName,\n}: Readonly<CircleRatingProps>) {\n const { size: svgSize, stroke, fontSize } = circleSizeMap[size]\n const radius = (svgSize - stroke) / 2\n const circumference = 2 * Math.PI * radius\n const offset = circumference - (percent / 100) * circumference\n\n return (\n <div className=\"ui:relative ui:inline-flex ui:items-center ui:justify-center\">\n <svg width={svgSize} height={svgSize} className=\"ui:-rotate-90\">\n <circle\n cx={svgSize / 2}\n cy={svgSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={stroke}\n className={trackClassName ?? 'ui:text-gray-200'}\n />\n <circle\n cx={svgSize / 2}\n cy={svgSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={stroke}\n strokeLinecap=\"round\"\n strokeDasharray={circumference}\n strokeDashoffset={offset}\n className={clsx(\n 'ui:transition-all ui:duration-500',\n getColorClass(percent)\n )}\n />\n </svg>\n {showValue && (\n <span\n className={clsx(\n 'ui:absolute ui:font-bold',\n fontSize,\n getColorClass(percent)\n )}\n >\n {max === 100 ? Math.round(percent) : value.toFixed(1)}\n </span>\n )}\n </div>\n )\n}\n\nexport default CircleRating\n"],"mappings":"AAiDM,SACE,KADF;AAjDN,OAAO,UAAU;AAIjB,MAAM,gBAGF;AAAA,EACF,IAAI,EAAE,MAAM,IAAI,QAAQ,GAAG,UAAU,aAAa;AAAA,EAClD,IAAI,EAAE,MAAM,IAAI,QAAQ,GAAG,UAAU,aAAa;AAAA,EAClD,IAAI,EAAE,MAAM,IAAI,QAAQ,GAAG,UAAU,eAAe;AACtD;AAEA,MAAM,gBAAgB,CAAC,YAA4B;AACjD,MAAI,WAAW,GAAI,QAAO;AAC1B,MAAI,WAAW,GAAI,QAAO;AAC1B,SAAO;AACT;AAiBA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,EAAE,MAAM,SAAS,QAAQ,SAAS,IAAI,cAAc,IAAI;AAC9D,QAAM,UAAU,UAAU,UAAU;AACpC,QAAM,gBAAgB,IAAI,KAAK,KAAK;AACpC,QAAM,SAAS,gBAAiB,UAAU,MAAO;AAEjD,SACE,qBAAC,SAAI,WAAU,gEACb;AAAA,yBAAC,SAAI,OAAO,SAAS,QAAQ,SAAS,WAAU,iBAC9C;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAI,UAAU;AAAA,UACd,IAAI,UAAU;AAAA,UACd,GAAG;AAAA,UACH,MAAK;AAAA,UACL,QAAO;AAAA,UACP,aAAa;AAAA,UACb,WAAW,kBAAkB;AAAA;AAAA,MAC/B;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,IAAI,UAAU;AAAA,UACd,IAAI,UAAU;AAAA,UACd,GAAG;AAAA,UACH,MAAK;AAAA,UACL,QAAO;AAAA,UACP,aAAa;AAAA,UACb,eAAc;AAAA,UACd,iBAAiB;AAAA,UACjB,kBAAkB;AAAA,UAClB,WAAW;AAAA,YACT;AAAA,YACA,cAAc,OAAO;AAAA,UACvB;AAAA;AAAA,MACF;AAAA,OACF;AAAA,IACC,aACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,cAAc,OAAO;AAAA,QACvB;AAAA,QAEC,kBAAQ,MAAM,KAAK,MAAM,OAAO,IAAI,MAAM,QAAQ,CAAC;AAAA;AAAA,IACtD;AAAA,KAEJ;AAEJ;AAEA,IAAO,uBAAQ;","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
type RatingVariant = 'circle' | 'stars';
|
|
4
|
+
type RatingSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
interface RatingProps {
|
|
6
|
+
/** Rating value (0-10 by default, or 0-100 if max=100) */
|
|
7
|
+
value: number;
|
|
8
|
+
/** Maximum value (default: 10 for TMDB scores) */
|
|
9
|
+
max?: number;
|
|
10
|
+
/** Visual variant */
|
|
11
|
+
variant?: RatingVariant;
|
|
12
|
+
/** Size of the rating */
|
|
13
|
+
size?: RatingSize;
|
|
14
|
+
/** Show the numeric value */
|
|
15
|
+
showValue?: boolean;
|
|
16
|
+
/** Custom class for the background track circle (circle variant only) */
|
|
17
|
+
trackClassName?: string;
|
|
18
|
+
/** Additional class name */
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
declare function Rating({ value, max, variant, size, showValue, trackClassName, className, }: Readonly<RatingProps>): react_jsx_runtime.JSX.Element;
|
|
22
|
+
|
|
23
|
+
export { type RatingProps, type RatingSize, type RatingVariant, Rating as default };
|