notionsoft-ui 1.0.34 → 1.0.36

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.
Files changed (33) hide show
  1. package/.storybook/main.ts +19 -13
  2. package/.storybook/preview.css +0 -16
  3. package/package.json +7 -2
  4. package/src/notion-ui/animated-item/animated-item.tsx +1 -1
  5. package/src/notion-ui/animated-item/index.ts +1 -1
  6. package/src/notion-ui/button/Button.stories.tsx +31 -8
  7. package/src/notion-ui/button/button.tsx +10 -2
  8. package/src/notion-ui/button-spinner/ButtonSpinner.stories.tsx +42 -34
  9. package/src/notion-ui/button-spinner/button-spinner.tsx +4 -5
  10. package/src/notion-ui/cached-image/cached-image.stories.tsx +109 -0
  11. package/src/notion-ui/cached-image/cached-image.tsx +213 -0
  12. package/src/notion-ui/cached-image/index.ts +3 -0
  13. package/src/notion-ui/cached-image/utils.ts +7 -0
  14. package/src/notion-ui/cached-svg/CachedSvg.stories.tsx +74 -0
  15. package/src/notion-ui/cached-svg/cached-svg.tsx +150 -0
  16. package/src/notion-ui/cached-svg/index.ts +3 -0
  17. package/src/notion-ui/cached-svg/utils.ts +7 -0
  18. package/src/notion-ui/date-picker/DatePicker.stories.tsx +0 -2
  19. package/src/notion-ui/date-picker/date-picker.tsx +5 -5
  20. package/src/notion-ui/input/Input.stories.tsx +1 -1
  21. package/src/notion-ui/input/input.tsx +5 -4
  22. package/src/notion-ui/multi-date-picker/multi-date-picker.tsx +5 -5
  23. package/src/notion-ui/page-size-select/page-size-select.tsx +29 -12
  24. package/src/notion-ui/password-input/password-input.tsx +3 -3
  25. package/src/notion-ui/phone-input/phone-input.tsx +38 -8
  26. package/src/notion-ui/shimmer/shimmer.tsx +9 -3
  27. package/src/notion-ui/shining-text/shining-text.tsx +2 -6
  28. package/src/notion-ui/sidebar/index.ts +3 -0
  29. package/src/notion-ui/sidebar/sidebar-item.tsx +198 -0
  30. package/src/notion-ui/sidebar/sidebar.stories.tsx +181 -0
  31. package/src/notion-ui/sidebar/sidebar.tsx +284 -0
  32. package/src/notion-ui/textarea/Textarea.stories.tsx +1 -1
  33. package/src/notion-ui/textarea/textarea.tsx +3 -3
@@ -1,19 +1,25 @@
1
- import type { StorybookConfig } from '@storybook/react-vite';
2
-
1
+ import type { StorybookConfig } from "@storybook/react-vite";
2
+ import { mergeConfig } from "vite";
3
+ import tsconfigPaths from "vite-tsconfig-paths";
3
4
  const config: StorybookConfig = {
4
- "stories": [
5
- "../src/**/*.mdx",
6
- "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
7
- ],
8
- "addons": [
5
+ stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
6
+ addons: [
9
7
  "@chromatic-com/storybook",
10
8
  "@storybook/addon-docs",
11
9
  "@storybook/addon-a11y",
12
- "@storybook/addon-vitest"
10
+ "@storybook/addon-vitest",
13
11
  ],
14
- "framework": {
15
- "name": "@storybook/react-vite",
16
- "options": {}
17
- }
12
+ framework: {
13
+ name: "@storybook/react-vite",
14
+ options: {},
15
+ },
16
+ async viteFinal(config) {
17
+ return mergeConfig(config, {
18
+ plugins: [tsconfigPaths()],
19
+ define: {
20
+ "import.meta.env.VITE_API_BASE_URL": JSON.stringify(""),
21
+ },
22
+ });
23
+ },
18
24
  };
19
- export default config;
25
+ export default config;
@@ -47,22 +47,6 @@
47
47
  --color-primary-background: var(--primary-background);
48
48
  --primary-background: oklch(20.435% 0.06485 262.22);
49
49
 
50
- --text-sm-ltr: 11px;
51
- --text-md-ltr: 12px;
52
- --text-lg-ltr: 13px;
53
- --text-xl-ltr: 14px;
54
- --text-2xl-ltr: 15px;
55
- --text-3xl-ltr: 17px;
56
- --text-4xl-ltr: 20px;
57
-
58
- --text-sm-rtl: 15px;
59
- --text-md-rtl: 16px;
60
- --text-lg-rtl: 17px;
61
- --text-xl-rtl: 18px;
62
- --text-2xl-rtl: 19px;
63
- --text-3xl-rtl: 20px;
64
- --text-4xl-rtl: 22px;
65
-
66
50
  --breakpoint-xxl: 500px;
67
51
  --animate-shimmer: shimmer 2.2s infinite linear;
68
52
  @keyframes shimmer {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notionsoft-ui",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "A React UI component installer (shadcn-style). Installs components directly into your project.",
5
5
  "bin": {
6
6
  "notionsoft-ui": "./cli/index.cjs"
@@ -22,12 +22,17 @@
22
22
  "chalk": "^4.1.2",
23
23
  "clsx": "^2.1.1",
24
24
  "commander": "^12.0.0",
25
+ "dompurify": "^3.3.1",
25
26
  "fs-extra": "^11.2.0",
27
+ "i18next": "^25.7.3",
26
28
  "lucide-react": "^0.554.0",
27
29
  "react": "^19.2.0",
28
30
  "react-dom": "^19.2.0",
31
+ "react-i18next": "^16.5.0",
29
32
  "react-multi-date-picker": "^4.5.2",
30
- "tailwind-merge": "^3.4.0"
33
+ "react-router": "^7.11.0",
34
+ "tailwind-merge": "^3.4.0",
35
+ "vite-tsconfig-paths": "^6.0.3"
31
36
  },
32
37
  "devDependencies": {
33
38
  "@chromatic-com/storybook": "^4.1.3",
@@ -32,7 +32,7 @@ export interface AnimatedItemProps {
32
32
  children: React.ReactNode | ((inView: boolean) => React.ReactNode);
33
33
  }
34
34
 
35
- export default function AnimatedItem(props: AnimatedItemProps) {
35
+ export function AnimatedItem(props: AnimatedItemProps) {
36
36
  const [inView, setInView] = useState(false);
37
37
  const { springProps, intersectionArgs, children } = props;
38
38
  const defaultOnStart = useCallback(
@@ -1,3 +1,3 @@
1
- import AnimatedItem from "./animated-item";
1
+ import { AnimatedItem } from "./animated-item";
2
2
 
3
3
  export default AnimatedItem;
@@ -2,12 +2,13 @@ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import Button from "./button";
3
3
 
4
4
  const meta: Meta<typeof Button> = {
5
- title: "Button/Button",
5
+ title: "Components/Button",
6
6
  component: Button,
7
+ tags: ["autodocs"],
7
8
  argTypes: {
8
9
  variant: {
9
10
  control: "select",
10
- options: ["default", "primary", "secondary", "warning"],
11
+ options: ["primary", "secondary", "warning", "success", "outline"],
11
12
  },
12
13
  disabled: {
13
14
  control: "boolean",
@@ -15,7 +16,11 @@ const meta: Meta<typeof Button> = {
15
16
  children: {
16
17
  control: "text",
17
18
  },
18
- onClick: { action: "clicked" },
19
+ },
20
+ args: {
21
+ children: "Button",
22
+ variant: "primary",
23
+ disabled: false,
19
24
  },
20
25
  };
21
26
 
@@ -25,33 +30,51 @@ type Story = StoryObj<typeof Button>;
25
30
 
26
31
  export const Primary: Story = {
27
32
  args: {
28
- children: "Primary Button",
29
33
  variant: "primary",
30
34
  },
31
35
  };
32
36
 
33
37
  export const Secondary: Story = {
34
38
  args: {
35
- children: "Secondary Button",
36
39
  variant: "secondary",
37
40
  },
38
41
  };
39
42
 
40
43
  export const Warning: Story = {
41
44
  args: {
42
- children: "Warning Button",
43
45
  variant: "warning",
46
+ children: "Delete",
44
47
  },
45
48
  };
49
+
46
50
  export const Success: Story = {
47
51
  args: {
48
- children: "Success Button",
49
52
  variant: "success",
53
+ children: "Saved",
50
54
  },
51
55
  };
56
+
52
57
  export const Outline: Story = {
53
58
  args: {
54
- children: "Outline Button",
55
59
  variant: "outline",
56
60
  },
57
61
  };
62
+
63
+ export const Disabled: Story = {
64
+ args: {
65
+ disabled: true,
66
+ },
67
+ };
68
+
69
+ export const AllVariants: Story = {
70
+ render: () => (
71
+ <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
72
+ <Button variant="primary">Primary</Button>
73
+ <Button variant="secondary">Secondary</Button>
74
+ <Button variant="success">Success</Button>
75
+ <Button variant="warning">Warning</Button>
76
+ <Button variant="outline">Outline</Button>
77
+ <Button disabled>Disabled</Button>
78
+ </div>
79
+ ),
80
+ };
@@ -3,7 +3,13 @@ import { cn } from "../../utils/cn";
3
3
  // import { cn } from "@/utils/cn";
4
4
 
5
5
  interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6
- variant?: "primary" | "secondary" | "warning" | "success" | "outline";
6
+ variant?:
7
+ | "primary"
8
+ | "secondary"
9
+ | "warning"
10
+ | "success"
11
+ | "outline"
12
+ | "icon";
7
13
  }
8
14
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
9
15
  (props, ref: any) => {
@@ -13,6 +19,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
13
19
  ? "border hover:bg-primary hover:text-primary-foreground"
14
20
  : variant == "warning"
15
21
  ? "bg-red-500 text-primary-foreground"
22
+ : variant == "icon"
23
+ ? "rtl:px-3 rtl:py-1 ltr:py-2 border gap-x-3 text-primary border border-primary/10 hover:bg-primary/5 hover:opacity-90 transition-opacity px-5"
16
24
  : variant == "success"
17
25
  ? "bg-green-500 text-primary-foreground"
18
26
  : variant == "outline"
@@ -24,7 +32,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
24
32
  disabled={disabled}
25
33
  ref={ref}
26
34
  className={cn(
27
- `rounded-sm grid grid-cols-[1fr_1fr_auto] leading-snug cursor-pointer font-medium ltr:text-xs rtl:text-[13px] sm:rtl:text-sm rtl:font-semibold
35
+ `rounded-sm items-center justify-center flex gap-x-1 leading-snug cursor-pointer ltr:text-xs rtl:text-[13px] sm:rtl:text-sm rtl:font-semibold
28
36
  transition w-fit px-3 py-1.5 duration-200 ease-linear`,
29
37
  style,
30
38
  disabled &&
@@ -1,58 +1,66 @@
1
- import Button from "../button/button";
2
- import ButtonSpinner from "./button-spinner";
3
1
  import type { Meta, StoryObj } from "@storybook/react";
2
+ import React from "react";
3
+ import ButtonSpinner from "./button-spinner";
4
+ import Button from "../button/button";
4
5
 
5
6
  const meta: Meta<typeof ButtonSpinner> = {
6
- title: "Loader/ButtonSpinner",
7
+ title: "Components/ButtonSpinner",
7
8
  component: ButtonSpinner,
8
- parameters: {
9
- layout: "centered",
10
- },
9
+ tags: ["autodocs"],
11
10
  argTypes: {
12
- loading: { control: "boolean" },
13
- className: { control: "text" },
14
- children: { control: "text" },
11
+ loading: {
12
+ control: "boolean",
13
+ },
14
+ children: {
15
+ control: false,
16
+ },
17
+ },
18
+ args: {
19
+ loading: false,
15
20
  },
16
21
  };
17
22
 
18
23
  export default meta;
19
- type Story = StoryObj<typeof ButtonSpinner>;
20
-
21
- /* -----------------------------
22
- Template: how ButtonSpinner
23
- is intended to be used
24
- ------------------------------ */
25
- const Template = (args) => (
26
- <Button disabled={args.loading} className="flex items-center gap-3">
27
- <ButtonSpinner {...args} />
28
- </Button>
29
- );
30
24
 
31
- /* -----------------------------
32
- Stories
33
- ------------------------------ */
25
+ type Story = StoryObj<typeof ButtonSpinner>;
34
26
 
35
27
  export const Default: Story = {
36
- render: Template,
37
- args: {
38
- loading: false,
39
- children: "Save",
40
- },
28
+ render: (args) => (
29
+ <Button>
30
+ <ButtonSpinner {...args}>Submit</ButtonSpinner>
31
+ </Button>
32
+ ),
41
33
  };
42
34
 
43
35
  export const Loading: Story = {
44
- render: Template,
45
36
  args: {
46
37
  loading: true,
47
- children: "Saving...",
48
38
  },
39
+ render: (args) => (
40
+ <Button>
41
+ <ButtonSpinner {...args}>Submitting</ButtonSpinner>
42
+ </Button>
43
+ ),
49
44
  };
50
45
 
51
- export const WithCustomClass: Story = {
52
- render: Template,
46
+ export const WithDifferentText: Story = {
53
47
  args: {
54
48
  loading: true,
55
- className: "text-blue-600 font-semibold",
56
- children: "Processing...",
57
49
  },
50
+ render: (args) => (
51
+ <Button variant="success">
52
+ <ButtonSpinner {...args}>Saving</ButtonSpinner>
53
+ </Button>
54
+ ),
55
+ };
56
+
57
+ export const InlineUsage: Story = {
58
+ render: () => (
59
+ <div className="flex items-center gap-2">
60
+ <span>Loading</span>
61
+ <ButtonSpinner loading={true}>
62
+ <span className="sr-only">spinner</span>
63
+ </ButtonSpinner>
64
+ </div>
65
+ ),
58
66
  };
@@ -4,23 +4,22 @@ import { cn } from "../../utils/cn";
4
4
  export interface IButtonSpinnerProps {
5
5
  children: any;
6
6
  loading: boolean;
7
- className?: string;
8
7
  }
9
8
 
10
9
  export default function ButtonSpinner(props: IButtonSpinnerProps) {
11
- const { loading, children, className } = props;
10
+ const { loading, children } = props;
12
11
  return (
13
12
  <>
13
+ {children}
14
14
  {loading && (
15
- <div className="relative w-[16px] h-[16px]">
15
+ <div className="relative size-3">
16
16
  {/* <!-- Ring --> */}
17
17
  <div
18
- className="w-[16px] h-[16px] rounded-full animate-spin absolute
18
+ className="size-3 rounded-full animate-spin absolute
19
19
  border border-solid border-secondary border-t-transparent"
20
20
  />
21
21
  </div>
22
22
  )}
23
- <h1 className={cn("", className)}>{children}</h1>
24
23
  </>
25
24
  );
26
25
  }
@@ -0,0 +1,109 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import CachedImage, { ImageProps } from "./cached-image";
3
+
4
+ /* ---------------------------------- */
5
+ /* Helpers */
6
+ /* ---------------------------------- */
7
+
8
+ // Basic fetch wrapper
9
+ const fetchImage = async (src: string): Promise<Response> => {
10
+ return fetch(src);
11
+ };
12
+
13
+ // Delayed fetch to show shimmer/loading
14
+ const delayedFetch = async (src: string): Promise<Response> => {
15
+ await new Promise((resolve) => setTimeout(resolve, 2000));
16
+ return fetch(src);
17
+ };
18
+
19
+ /* ---------------------------------- */
20
+ /* Meta */
21
+ /* ---------------------------------- */
22
+
23
+ const meta: Meta<ImageProps> = {
24
+ title: "Media/CachedImage",
25
+ component: CachedImage,
26
+ parameters: {
27
+ layout: "centered",
28
+ },
29
+ argTypes: {
30
+ className: {
31
+ control: "text",
32
+ },
33
+ classNames: {
34
+ control: "object",
35
+ },
36
+ fetch: {
37
+ table: { disable: true },
38
+ },
39
+ apiConfig: {
40
+ table: { disable: true },
41
+ },
42
+ },
43
+ };
44
+
45
+ export default meta;
46
+ type Story = StoryObj<ImageProps>;
47
+
48
+ /* ---------------------------------- */
49
+ /* Stories */
50
+ /* ---------------------------------- */
51
+
52
+ // Default (fetch + cache)
53
+ export const Default: Story = {
54
+ args: {
55
+ src: "/images/sample.jpg",
56
+ fetch: fetchImage,
57
+ className: "w-40 h-40 rounded-md",
58
+ },
59
+ };
60
+
61
+ // API config variant
62
+ export const WithApiConfig: Story = {
63
+ args: {
64
+ apiConfig: {
65
+ src: "/images/sample.jpg",
66
+ },
67
+ className: "w-40 h-40 rounded-lg",
68
+ },
69
+ };
70
+
71
+ // Cross-origin image (bypasses cache)
72
+ export const CrossOrigin: Story = {
73
+ args: {
74
+ src: "https://picsum.photos/200",
75
+ fetch: fetchImage,
76
+ className: "w-40 h-40 rounded-full",
77
+ },
78
+ };
79
+
80
+ // Loading / shimmer state
81
+ export const LoadingState: Story = {
82
+ args: {
83
+ src: "/images/sample.jpg",
84
+ fetch: delayedFetch,
85
+ className: "w-32 h-32",
86
+ classNames: {
87
+ shimmerClassName: "bg-gray-200",
88
+ shimmerIconClassName: "stroke-gray-400",
89
+ },
90
+ },
91
+ };
92
+
93
+ // Small size
94
+ export const Small: Story = {
95
+ args: {
96
+ src: "/images/sample.jpg",
97
+ fetch: fetchImage,
98
+ className: "w-16 h-16 rounded",
99
+ },
100
+ };
101
+
102
+ // Large size
103
+ export const Large: Story = {
104
+ args: {
105
+ src: "/images/sample.jpg",
106
+ fetch: fetchImage,
107
+ className: "w-64 h-64 rounded-xl",
108
+ },
109
+ };
@@ -0,0 +1,213 @@
1
+ import Shimmer from "@/components/notion-ui/shimmer";
2
+ import { cn } from "@/utils/cn";
3
+ import React, { useEffect, useState, forwardRef, useRef } from "react";
4
+
5
+ /* ---------------------------------- */
6
+ /* Types */
7
+ /* ---------------------------------- */
8
+
9
+ interface FetchConfig {
10
+ src: string;
11
+ headers?: Record<string, string>;
12
+ params?: string;
13
+ }
14
+
15
+ interface BaseImageProps extends React.HTMLAttributes<HTMLDivElement> {
16
+ classNames?: {
17
+ shimmerClassName?: string;
18
+ shimmerIconClassName?: string;
19
+ };
20
+ }
21
+
22
+ interface FetchImageProps extends BaseImageProps {
23
+ src: string;
24
+ fetch: (src: string) => Promise<Response>;
25
+ apiConfig?: never;
26
+ }
27
+
28
+ interface ApiConfigImageProps extends BaseImageProps {
29
+ apiConfig: FetchConfig;
30
+ src?: never;
31
+ fetch?: never;
32
+ }
33
+
34
+ export type ImageProps = FetchImageProps | ApiConfigImageProps;
35
+
36
+ /* ---------------------------------- */
37
+ /* Cache helpers */
38
+ /* ---------------------------------- */
39
+
40
+ const IMAGE_CACHE = "image-cache-v1";
41
+
42
+ async function getCachedImage(url: string): Promise<string | null> {
43
+ const cache = await caches.open(IMAGE_CACHE);
44
+ const cached = await cache.match(url);
45
+
46
+ if (!cached) return null;
47
+
48
+ const blob = await cached.blob();
49
+ return URL.createObjectURL(blob);
50
+ }
51
+
52
+ async function cacheImage(url: string, response: Response) {
53
+ const cache = await caches.open(IMAGE_CACHE);
54
+ await cache.put(url, response.clone());
55
+ }
56
+
57
+ /* ---------------------------------- */
58
+ /* Utils */
59
+ /* ---------------------------------- */
60
+
61
+ function isCrossOrigin(url: string): boolean {
62
+ try {
63
+ return new URL(url, window.location.href).origin !== window.location.origin;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /* ---------------------------------- */
70
+ /* Component */
71
+ /* ---------------------------------- */
72
+
73
+ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
74
+ const { className, classNames, fetch, apiConfig, src, ...imgProps } = props;
75
+
76
+ const [imageUrl, setImageUrl] = useState<string | null>(null);
77
+ const [loading, setLoading] = useState(true);
78
+
79
+ const { shimmerClassName, shimmerIconClassName } = classNames || {};
80
+
81
+ const fetchRef = useRef(fetch);
82
+ useEffect(() => {
83
+ fetchRef.current = fetch;
84
+ }, [fetch]);
85
+
86
+ async function loadImage() {
87
+ try {
88
+ const resolvedSrc = apiConfig?.src ?? src;
89
+
90
+ if (!resolvedSrc) {
91
+ setImageUrl(null);
92
+ return;
93
+ }
94
+
95
+ /* ---------------------------------- */
96
+ /* Cross-origin → use <img src> */
97
+ /* ---------------------------------- */
98
+ if (isCrossOrigin(resolvedSrc)) {
99
+ setImageUrl(resolvedSrc);
100
+ setLoading(false);
101
+ return;
102
+ }
103
+
104
+ /* ---------------------------------- */
105
+ /* Cache Storage */
106
+ /* ---------------------------------- */
107
+ const cached = await getCachedImage(resolvedSrc);
108
+ if (cached) {
109
+ setImageUrl(cached);
110
+ setLoading(false);
111
+ return;
112
+ }
113
+
114
+ /* ---------------------------------- */
115
+ /* Fetch */
116
+ /* ---------------------------------- */
117
+ const response = fetchRef.current
118
+ ? await fetchRef.current(resolvedSrc)
119
+ : await window.fetch(resolvedSrc, {
120
+ headers: apiConfig?.headers,
121
+ });
122
+
123
+ const contentType = response.headers.get("content-type") ?? "";
124
+
125
+ if (
126
+ !response.ok ||
127
+ (!contentType.startsWith("image/") &&
128
+ contentType !== "application/octet-stream")
129
+ ) {
130
+ throw new Error(`Invalid image response: ${contentType}`);
131
+ }
132
+
133
+ await cacheImage(resolvedSrc, response.clone());
134
+
135
+ const blob = await response.blob();
136
+ setImageUrl(URL.createObjectURL(blob));
137
+ } catch (err) {
138
+ console.error(err);
139
+ } finally {
140
+ setLoading(false);
141
+ }
142
+ }
143
+
144
+ useEffect(() => {
145
+ loadImage();
146
+ }, [src, apiConfig?.src]);
147
+
148
+ /* ---------------------------------- */
149
+ /* Cleanup */
150
+ /* ---------------------------------- */
151
+
152
+ useEffect(() => {
153
+ return () => {
154
+ if (imageUrl?.startsWith("blob:")) {
155
+ URL.revokeObjectURL(imageUrl);
156
+ }
157
+ };
158
+ }, [imageUrl]);
159
+
160
+ /* ---------------------------------- */
161
+ /* UI */
162
+ /* ---------------------------------- */
163
+
164
+ if (loading || !imageUrl) {
165
+ const stop = loading ? false : !imageUrl && true;
166
+
167
+ return (
168
+ <Shimmer
169
+ className={cn(
170
+ "bg-primary/10 mx-auto flex p-2 items-center size-8 rounded border border-tertiary/10",
171
+ shimmerClassName
172
+ )}
173
+ stop={stop}
174
+ >
175
+ <svg
176
+ xmlns="http://www.w3.org/2000/svg"
177
+ width="24"
178
+ height="24"
179
+ viewBox="0 0 24 24"
180
+ fill="none"
181
+ strokeLinecap="round"
182
+ strokeLinejoin="round"
183
+ className={cn(
184
+ "stroke-primary/40 mx-auto stroke-2",
185
+ shimmerIconClassName
186
+ )}
187
+ >
188
+ <rect x="1" y="1" width="22" height="22" rx="2" ry="2" />
189
+ <polyline points="3,20 8,13 13,17 17,12 21,16" />
190
+ <circle cx="16" cy="6" r="2" />
191
+ </svg>
192
+ </Shimmer>
193
+ );
194
+ }
195
+
196
+ return (
197
+ <div
198
+ ref={ref}
199
+ // src={imageUrl}
200
+ style={{ backgroundImage: `url(${imageUrl})` }}
201
+ // alt={alt}
202
+ className={cn(
203
+ "cursor-pointer shadow-lg bg-cover bg-center mx-auto",
204
+ className
205
+ )}
206
+ {...imgProps}
207
+ />
208
+ );
209
+ });
210
+
211
+ CachedImage.displayName = "CachedImage";
212
+
213
+ export default CachedImage;
@@ -0,0 +1,3 @@
1
+ import CachedImage from "./cached-image";
2
+
3
+ export default CachedImage;
@@ -0,0 +1,7 @@
1
+ export function isCrossOrigin(url: string): boolean {
2
+ try {
3
+ return new URL(url, window.location.href).origin !== window.location.origin;
4
+ } catch {
5
+ return false;
6
+ }
7
+ }