notionsoft-ui 1.0.43 → 1.0.46
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/cli/index.cjs +23 -37
- package/package.json +1 -3
- package/src/notion-ui/animated-item/animated-item.tsx +6 -4
- package/src/notion-ui/animated-item/index.ts +2 -2
- package/src/notion-ui/breadcrumb/breadcrumb.tsx +7 -7
- package/src/notion-ui/breadcrumb/index.ts +8 -0
- package/src/notion-ui/button/Button.stories.tsx +2 -2
- package/src/notion-ui/button/button.tsx +11 -11
- package/src/notion-ui/button/index.ts +2 -2
- package/src/notion-ui/button-spinner/ButtonSpinner.stories.tsx +3 -4
- package/src/notion-ui/button-spinner/button-spinner.tsx +3 -5
- package/src/notion-ui/button-spinner/index.ts +2 -2
- package/src/notion-ui/cached-image/cached-image.stories.tsx +7 -7
- package/src/notion-ui/cached-image/cached-image.tsx +44 -23
- package/src/notion-ui/cached-image/index.ts +2 -2
- package/src/notion-ui/cached-svg/CachedSvg.stories.tsx +9 -11
- package/src/notion-ui/cached-svg/cached-svg.tsx +53 -48
- package/src/notion-ui/cached-svg/index.ts +2 -2
- package/src/notion-ui/card/card.tsx +4 -4
- package/src/notion-ui/card/index.ts +18 -19
- package/src/notion-ui/checkbox/checkbox.tsx +5 -6
- package/src/notion-ui/checkbox/index.ts +3 -0
- package/src/notion-ui/circle-loader/CircleLoader.stories.tsx +57 -25
- package/src/notion-ui/circle-loader/circle-loader.tsx +6 -6
- package/src/notion-ui/circle-loader/index.ts +2 -2
- package/src/notion-ui/date-picker/DatePicker.stories.tsx +5 -3
- package/src/notion-ui/date-picker/date-picker.tsx +35 -33
- package/src/notion-ui/date-picker/index.ts +2 -2
- package/src/notion-ui/filter-dialog/FilterDialog.stories.tsx +117 -85
- package/src/notion-ui/filter-dialog/filter-dialog.tsx +40 -16
- package/src/notion-ui/filter-dialog/index.ts +15 -0
- package/src/notion-ui/input/Input.stories.tsx +2 -2
- package/src/notion-ui/input/index.ts +2 -2
- package/src/notion-ui/input/input.tsx +45 -40
- package/src/notion-ui/multi-date-picker/MultiDatePicker.stories.tsx +5 -3
- package/src/notion-ui/multi-date-picker/index.ts +6 -2
- package/src/notion-ui/multi-date-picker/multi-date-picker.tsx +35 -34
- package/src/notion-ui/multi-select-input/helper.ts +17 -0
- package/src/notion-ui/multi-select-input/hook.ts +10 -0
- package/src/notion-ui/multi-select-input/index.ts +6 -2
- package/src/notion-ui/multi-select-input/multi-select-input.stories.tsx +7 -10
- package/src/notion-ui/multi-select-input/multi-select-input.tsx +33 -30
- package/src/notion-ui/multi-tab-input/index.ts +2 -2
- package/src/notion-ui/multi-tab-input/multi-tab-input.tsx +11 -13
- package/src/notion-ui/multi-tab-input/multi.tab.input.stories.tsx +4 -4
- package/src/notion-ui/multi-tab-textarea/index.ts +2 -2
- package/src/notion-ui/multi-tab-textarea/multi-tab-textarea.tsx +8 -9
- package/src/notion-ui/multi-tab-textarea/multi.tab.textarea.stories.tsx +6 -7
- package/src/notion-ui/page-size-select/index.ts +2 -2
- package/src/notion-ui/page-size-select/page-size-select.stories.tsx +1 -1
- package/src/notion-ui/page-size-select/page-size-select.tsx +6 -6
- package/src/notion-ui/pagination/Pagination.stories.tsx +2 -3
- package/src/notion-ui/pagination/index.ts +3 -0
- package/src/notion-ui/pagination/pagination.tsx +2 -2
- package/src/notion-ui/password-input/helper.ts +21 -0
- package/src/notion-ui/password-input/index.ts +6 -2
- package/src/notion-ui/password-input/password-input.stories.tsx +4 -6
- package/src/notion-ui/password-input/password-input.tsx +9 -28
- package/src/notion-ui/phone-input/PhoneInput.stories.tsx +2 -2
- package/src/notion-ui/phone-input/index.ts +2 -2
- package/src/notion-ui/phone-input/lazy-flag.tsx +1 -1
- package/src/notion-ui/phone-input/phone-input.tsx +33 -29
- package/src/notion-ui/search-input/helper.ts +17 -0
- package/src/notion-ui/search-input/hook.ts +10 -0
- package/src/notion-ui/search-input/index.ts +18 -2
- package/src/notion-ui/search-input/search-input.tsx +40 -30
- package/src/notion-ui/search-input/search.Input.stories.tsx +3 -4
- package/src/notion-ui/sheet/AnimatedSheet.stories.tsx +2 -2
- package/src/notion-ui/sheet/{AnimatedSheet.tsx → animated-sheet.tsx} +43 -43
- package/src/notion-ui/sheet/index.ts +2 -2
- package/src/notion-ui/shimmer/index.ts +8 -2
- package/src/notion-ui/shimmer/shimmer.stories.tsx +1 -2
- package/src/notion-ui/shimmer/shimmer.tsx +6 -10
- package/src/notion-ui/shining-text/index.ts +1 -1
- package/src/notion-ui/shining-text/shining-text.stories.tsx +1 -1
- package/src/notion-ui/shining-text/shining-text.tsx +5 -5
- package/src/notion-ui/sidebar/index.ts +34 -2
- package/src/notion-ui/sidebar/sidebar-item.tsx +15 -16
- package/src/notion-ui/sidebar/sidebar.stories.tsx +22 -45
- package/src/notion-ui/sidebar/sidebar.tsx +36 -26
- package/src/notion-ui/status-button/index.ts +5 -2
- package/src/notion-ui/status-button/status-button.stories.tsx +1 -1
- package/src/notion-ui/status-button/status-button.tsx +7 -7
- package/src/notion-ui/tab/index.ts +2 -2
- package/src/notion-ui/tab/tab.tsx +8 -6
- package/src/notion-ui/table/Table.stories.tsx +156 -0
- package/src/notion-ui/table/index.ts +23 -0
- package/src/notion-ui/table/table.tsx +8 -8
- package/src/notion-ui/textarea/Textarea.stories.tsx +1 -1
- package/src/notion-ui/textarea/index.ts +2 -2
- package/src/notion-ui/textarea/textarea.tsx +12 -15
- package/src/utils/cn.ts +0 -26
- package/src/utils/helper.ts +5 -0
- package/src/utils/hook.ts +2 -0
- package/tsconfig.json +2 -0
- package/src/notion-ui/cached-image/utils.ts +0 -7
- package/src/notion-ui/cached-svg/utils.ts +0 -7
package/cli/index.cjs
CHANGED
|
@@ -6,19 +6,6 @@ const path = require("path");
|
|
|
6
6
|
const chalk = require("chalk");
|
|
7
7
|
const { execSync } = require("child_process");
|
|
8
8
|
|
|
9
|
-
/* ------------------------------
|
|
10
|
-
Helper: get template path
|
|
11
|
-
------------------------------- */
|
|
12
|
-
function getTemplateFile(component) {
|
|
13
|
-
// Library template: src/notion-ui/button/button.tsx
|
|
14
|
-
return path.join(
|
|
15
|
-
__dirname,
|
|
16
|
-
"../src/notion-ui",
|
|
17
|
-
component,
|
|
18
|
-
component + ".tsx"
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
9
|
/* ------------------------------
|
|
23
10
|
Install dependencies in project
|
|
24
11
|
------------------------------- */
|
|
@@ -41,7 +28,7 @@ function updateTsConfig() {
|
|
|
41
28
|
|
|
42
29
|
if (!fs.existsSync(tsconfigPath)) {
|
|
43
30
|
console.log(
|
|
44
|
-
chalk.yellow("⚠ tsconfig.json not found, skipping baseUrl setup.")
|
|
31
|
+
chalk.yellow("⚠ tsconfig.json not found, skipping baseUrl setup."),
|
|
45
32
|
);
|
|
46
33
|
return;
|
|
47
34
|
}
|
|
@@ -56,8 +43,8 @@ function updateTsConfig() {
|
|
|
56
43
|
fs.writeJsonSync(tsconfigPath, tsconfig, { spaces: 2 });
|
|
57
44
|
console.log(
|
|
58
45
|
chalk.green(
|
|
59
|
-
"✓ tsconfig.json updated for absolute imports (@utils/*, @components/*)"
|
|
60
|
-
)
|
|
46
|
+
"✓ tsconfig.json updated for absolute imports (@utils/*, @components/*)",
|
|
47
|
+
),
|
|
61
48
|
);
|
|
62
49
|
}
|
|
63
50
|
|
|
@@ -79,7 +66,7 @@ program
|
|
|
79
66
|
// Create config
|
|
80
67
|
fs.writeFileSync(
|
|
81
68
|
configPath,
|
|
82
|
-
JSON.stringify({ componentDir: "src/components/notion-ui" }, null, 2)
|
|
69
|
+
JSON.stringify({ componentDir: "src/components/notion-ui" }, null, 2),
|
|
83
70
|
);
|
|
84
71
|
|
|
85
72
|
// Ensure component folder
|
|
@@ -99,7 +86,7 @@ import { twMerge } from "tailwind-merge";
|
|
|
99
86
|
|
|
100
87
|
export function cn(...inputs: Parameters<typeof clsx>) {
|
|
101
88
|
return twMerge(clsx(...inputs));
|
|
102
|
-
}
|
|
89
|
+
}`,
|
|
103
90
|
);
|
|
104
91
|
console.log(chalk.green("✓ cn helper created at src/utils/cn.ts"));
|
|
105
92
|
}
|
|
@@ -130,6 +117,7 @@ program
|
|
|
130
117
|
|
|
131
118
|
const config = fs.readJSONSync(configFile);
|
|
132
119
|
const templateDir = path.join(__dirname, "../src/notion-ui", component);
|
|
120
|
+
fs.ensureDirSync(path.join(cwd, "src/utils"));
|
|
133
121
|
|
|
134
122
|
if (!fs.existsSync(templateDir)) {
|
|
135
123
|
console.log(chalk.red(`❌ Component '${component}' does not exist.`));
|
|
@@ -170,33 +158,31 @@ program
|
|
|
170
158
|
console.log(chalk.green(`✓ ${dataFile} merged to src/utils/dt.ts`));
|
|
171
159
|
});
|
|
172
160
|
|
|
173
|
-
// Handle
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.log(chalk.green(`✓ ${hookFile} merged to src/utils/hook.ts`));
|
|
181
|
-
});
|
|
161
|
+
// Handle hook.ts → src/utils/hook.ts
|
|
162
|
+
const hookFile = path.join(templateDir, "hook.ts");
|
|
163
|
+
if (fs.existsSync(hookFile)) {
|
|
164
|
+
const destHookFile = path.join(cwd, "src/utils/hook.ts");
|
|
165
|
+
appendIfNotExist(hookFile, destHookFile);
|
|
166
|
+
console.log(chalk.green("✓ hook.ts merged to src/utils/hook.ts"));
|
|
167
|
+
}
|
|
182
168
|
|
|
183
|
-
// Handle
|
|
184
|
-
const
|
|
185
|
-
if (fs.existsSync(
|
|
186
|
-
const
|
|
187
|
-
appendIfNotExist(
|
|
188
|
-
console.log(chalk.green("✓
|
|
169
|
+
// Handle helper.ts → src/utils/helper.ts
|
|
170
|
+
const helperFile = path.join(templateDir, "helper.ts");
|
|
171
|
+
if (fs.existsSync(helperFile)) {
|
|
172
|
+
const destHelperFile = path.join(cwd, "src/utils/helper.ts");
|
|
173
|
+
appendIfNotExist(helperFile, destHelperFile);
|
|
174
|
+
console.log(chalk.green("✓ helper.ts merged to src/utils/helper.ts"));
|
|
189
175
|
}
|
|
190
176
|
|
|
191
|
-
// Copy remaining files (exclude index.ts, *.stories.tsx, type.ts, *-data.ts,
|
|
177
|
+
// Copy remaining files (exclude index.ts, *.stories.tsx, type.ts, *-data.ts, hook.ts, helper.ts)
|
|
192
178
|
fs.readdirSync(templateDir).forEach((file) => {
|
|
193
179
|
if (
|
|
194
180
|
file === "index.ts" ||
|
|
195
181
|
file === "type.ts" ||
|
|
196
182
|
file.endsWith("-data.ts") ||
|
|
197
183
|
file.endsWith(".stories.tsx") ||
|
|
198
|
-
|
|
199
|
-
file === "
|
|
184
|
+
file === "hook.ts" ||
|
|
185
|
+
file === "helper.ts"
|
|
200
186
|
)
|
|
201
187
|
return;
|
|
202
188
|
|
|
@@ -207,7 +193,7 @@ program
|
|
|
207
193
|
});
|
|
208
194
|
|
|
209
195
|
console.log(
|
|
210
|
-
chalk.green(`✓ Installed ${component} to ${config.componentDir}`)
|
|
196
|
+
chalk.green(`✓ Installed ${component} to ${config.componentDir}`),
|
|
211
197
|
);
|
|
212
198
|
});
|
|
213
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "notionsoft-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
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"
|
|
@@ -24,11 +24,9 @@
|
|
|
24
24
|
"commander": "^12.0.0",
|
|
25
25
|
"dompurify": "^3.3.1",
|
|
26
26
|
"fs-extra": "^11.2.0",
|
|
27
|
-
"i18next": "^25.7.3",
|
|
28
27
|
"lucide-react": "^0.554.0",
|
|
29
28
|
"react": "^19.2.0",
|
|
30
29
|
"react-dom": "^19.2.0",
|
|
31
|
-
"react-i18next": "^16.5.0",
|
|
32
30
|
"react-multi-date-picker": "^4.5.2",
|
|
33
31
|
"react-router": "^7.11.0",
|
|
34
32
|
"tailwind-merge": "^3.4.0",
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "@react-spring/web";
|
|
9
9
|
import { useCallback, useState } from "react";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface AnimatedItemProps {
|
|
12
12
|
springProps: {
|
|
13
13
|
from?: object;
|
|
14
14
|
to?: object | object[];
|
|
@@ -20,7 +20,7 @@ export interface AnimatedItemProps {
|
|
|
20
20
|
onStart?: (
|
|
21
21
|
result: AnimationResult,
|
|
22
22
|
spring: Controller | SpringValue,
|
|
23
|
-
item?: any
|
|
23
|
+
item?: any,
|
|
24
24
|
) => void;
|
|
25
25
|
config?: {
|
|
26
26
|
mass: number;
|
|
@@ -32,14 +32,14 @@ export interface AnimatedItemProps {
|
|
|
32
32
|
children: React.ReactNode | ((inView: boolean) => React.ReactNode);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function AnimatedItem(props: AnimatedItemProps) {
|
|
36
36
|
const [inView, setInView] = useState(false);
|
|
37
37
|
const { springProps, intersectionArgs, children } = props;
|
|
38
38
|
const defaultOnStart = useCallback(
|
|
39
39
|
() => {
|
|
40
40
|
setInView(true);
|
|
41
41
|
},
|
|
42
|
-
[] // no dependencies, memoized once
|
|
42
|
+
[], // no dependencies, memoized once
|
|
43
43
|
);
|
|
44
44
|
const composedOnStart = springProps.onStart ?? defaultOnStart;
|
|
45
45
|
|
|
@@ -52,3 +52,5 @@ export function AnimatedItem(props: AnimatedItemProps) {
|
|
|
52
52
|
</animated.div>
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export { AnimatedItem, type AnimatedItemProps };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { AnimatedItem } from "./animated-item";
|
|
1
|
+
import { AnimatedItem, AnimatedItemProps } from "./animated-item";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export { AnimatedItem, type AnimatedItemProps };
|
|
@@ -13,13 +13,13 @@ const Breadcrumb = React.forwardRef<HTMLDivElement, BreadcrumbProps>(
|
|
|
13
13
|
{...rest}
|
|
14
14
|
className={cn(
|
|
15
15
|
"rounded-sm px-5 items-center border border-primary/15 bg-card w-full sm:w-fit overflow-x-auto flex gap-x-4",
|
|
16
|
-
className
|
|
16
|
+
className,
|
|
17
17
|
)}
|
|
18
18
|
>
|
|
19
19
|
{children}
|
|
20
20
|
</div>
|
|
21
21
|
);
|
|
22
|
-
}
|
|
22
|
+
},
|
|
23
23
|
);
|
|
24
24
|
|
|
25
25
|
interface BreadcrumbSeparatorProps extends React.SVGProps<SVGSVGElement> {}
|
|
@@ -40,7 +40,7 @@ const BreadcrumbSeparator = React.forwardRef<
|
|
|
40
40
|
aria-hidden="true"
|
|
41
41
|
className={cn(
|
|
42
42
|
"text-primary/15 min-h-9 min-w-4 h-9 w-4 rtl:rotate-180",
|
|
43
|
-
className
|
|
43
|
+
className,
|
|
44
44
|
)}
|
|
45
45
|
>
|
|
46
46
|
<path d="M.293 0l22 22-22 22h1.414l22-22-22-22H.293z"></path>
|
|
@@ -60,13 +60,13 @@ const BreadcrumbItem = React.forwardRef<HTMLDivElement, BreadcrumbItemProps>(
|
|
|
60
60
|
{...rest}
|
|
61
61
|
className={cn(
|
|
62
62
|
"text-primary/70 rtl:pt-0.5 hover:text-primary text-nowrap capitalize cursor-pointer transition-colors duration-200 font-medium rtl:text-4 ltr:text-xs",
|
|
63
|
-
className
|
|
63
|
+
className,
|
|
64
64
|
)}
|
|
65
65
|
>
|
|
66
66
|
{children}
|
|
67
67
|
</div>
|
|
68
68
|
);
|
|
69
|
-
}
|
|
69
|
+
},
|
|
70
70
|
);
|
|
71
71
|
|
|
72
72
|
interface BreadcrumbHomeProps extends React.SVGProps<SVGSVGElement> {}
|
|
@@ -85,7 +85,7 @@ const BreadcrumbHome = React.forwardRef<SVGSVGElement, BreadcrumbHomeProps>(
|
|
|
85
85
|
// data-slot="icon"
|
|
86
86
|
className={cn(
|
|
87
87
|
"text-primary/60 fill-primary/60 hover:scale-105 min-w-4 min-h-4 size-4 hover:fill-primary/90 transition-[fill] duration-300 cursor-pointer",
|
|
88
|
-
className
|
|
88
|
+
className,
|
|
89
89
|
)}
|
|
90
90
|
>
|
|
91
91
|
<path
|
|
@@ -95,7 +95,7 @@ const BreadcrumbHome = React.forwardRef<SVGSVGElement, BreadcrumbHomeProps>(
|
|
|
95
95
|
></path>
|
|
96
96
|
</svg>
|
|
97
97
|
);
|
|
98
|
-
}
|
|
98
|
+
},
|
|
99
99
|
);
|
|
100
100
|
|
|
101
101
|
export { Breadcrumb, BreadcrumbSeparator, BreadcrumbItem, BreadcrumbHome };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { Button } from "@/components/notion-ui/button";
|
|
1
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import Button from "./button";
|
|
3
3
|
|
|
4
4
|
const meta: Meta<typeof Button> = {
|
|
5
|
-
title: "
|
|
5
|
+
title: "Button/Button",
|
|
6
6
|
component: Button,
|
|
7
7
|
tags: ["autodocs"],
|
|
8
8
|
argTypes: {
|
|
@@ -17,14 +17,14 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
17
17
|
variant == "secondary"
|
|
18
18
|
? "bg-tertiary hover:shadow-sm shadow-lg hover:bg-tertiary rounded text-[12px] w-fit text-white"
|
|
19
19
|
: variant == "warning"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
? "bg-red-500 text-primary-foreground"
|
|
21
|
+
: variant == "icon"
|
|
22
|
+
? "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"
|
|
23
|
+
: variant == "success"
|
|
24
|
+
? "bg-green-500 text-primary-foreground"
|
|
25
|
+
: variant == "outline"
|
|
26
|
+
? "text-primary border border-primary/10 hover:bg-primary/5"
|
|
27
|
+
: "bg-primary hover:shadow hover:bg-primary shadow shadow-primary/50 text-primary-foreground/80 hover:opacity-90 hover:text-primary-foreground";
|
|
28
28
|
return (
|
|
29
29
|
<button
|
|
30
30
|
{...rest}
|
|
@@ -36,12 +36,12 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
36
36
|
style,
|
|
37
37
|
disabled &&
|
|
38
38
|
"opacity-35 pointer-events-none disabled:cursor-not-allowed",
|
|
39
|
-
className
|
|
39
|
+
className,
|
|
40
40
|
)}
|
|
41
41
|
>
|
|
42
42
|
{children}
|
|
43
43
|
</button>
|
|
44
44
|
);
|
|
45
|
-
}
|
|
45
|
+
},
|
|
46
46
|
);
|
|
47
|
-
export
|
|
47
|
+
export { Button };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import Button from "./button";
|
|
1
|
+
import { Button } from "./button";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export { Button };
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { Button } from "@/components/notion-ui/button";
|
|
2
|
+
import { ButtonSpinner } from "@/components/notion-ui/button-spinner";
|
|
1
3
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import ButtonSpinner from "./button-spinner";
|
|
4
|
-
import Button from "../button/button";
|
|
5
4
|
|
|
6
5
|
const meta: Meta<typeof ButtonSpinner> = {
|
|
7
|
-
title: "
|
|
6
|
+
title: "Button/ButtonSpinner",
|
|
8
7
|
component: ButtonSpinner,
|
|
9
8
|
tags: ["autodocs"],
|
|
10
9
|
argTypes: {
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
// import { cn } from "@/utils/cn";
|
|
3
|
-
|
|
4
|
-
export interface IButtonSpinnerProps {
|
|
1
|
+
interface IButtonSpinnerProps {
|
|
5
2
|
children: any;
|
|
6
3
|
loading: boolean;
|
|
7
4
|
}
|
|
8
5
|
|
|
9
|
-
|
|
6
|
+
function ButtonSpinner(props: IButtonSpinnerProps) {
|
|
10
7
|
const { loading, children } = props;
|
|
11
8
|
return (
|
|
12
9
|
<>
|
|
@@ -23,3 +20,4 @@ export default function ButtonSpinner(props: IButtonSpinnerProps) {
|
|
|
23
20
|
</>
|
|
24
21
|
);
|
|
25
22
|
}
|
|
23
|
+
export { ButtonSpinner, type IButtonSpinnerProps };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import ButtonSpinner from "./button-spinner";
|
|
1
|
+
import { ButtonSpinner, IButtonSpinnerProps } from "./button-spinner";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export { ButtonSpinner, type IButtonSpinnerProps };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { CachedImage, ImageProps } from "@/components/notion-ui/cached-image";
|
|
1
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import CachedImage, { ImageProps } from "./cached-image";
|
|
3
3
|
|
|
4
4
|
/* ---------------------------------- */
|
|
5
5
|
/* Helpers */
|
|
@@ -52,7 +52,7 @@ type Story = StoryObj<ImageProps>;
|
|
|
52
52
|
// Default (fetch + cache)
|
|
53
53
|
export const Default: Story = {
|
|
54
54
|
args: {
|
|
55
|
-
src: "/
|
|
55
|
+
src: "https://picsum.photos/200",
|
|
56
56
|
fetch: fetchImage,
|
|
57
57
|
className: "w-40 h-40 rounded-md",
|
|
58
58
|
},
|
|
@@ -62,7 +62,7 @@ export const Default: Story = {
|
|
|
62
62
|
export const WithApiConfig: Story = {
|
|
63
63
|
args: {
|
|
64
64
|
apiConfig: {
|
|
65
|
-
src: "/
|
|
65
|
+
src: "https://picsum.photos/200",
|
|
66
66
|
},
|
|
67
67
|
className: "w-40 h-40 rounded-lg",
|
|
68
68
|
},
|
|
@@ -73,14 +73,14 @@ export const CrossOrigin: Story = {
|
|
|
73
73
|
args: {
|
|
74
74
|
src: "https://picsum.photos/200",
|
|
75
75
|
fetch: fetchImage,
|
|
76
|
-
className: "w-40 h-40 rounded-full",
|
|
76
|
+
className: "w-40 h-40 rounded-full border/10",
|
|
77
77
|
},
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
// Loading / shimmer state
|
|
81
81
|
export const LoadingState: Story = {
|
|
82
82
|
args: {
|
|
83
|
-
src: "/
|
|
83
|
+
src: "https://picsum.photos/200",
|
|
84
84
|
fetch: delayedFetch,
|
|
85
85
|
className: "w-32 h-32",
|
|
86
86
|
classNames: {
|
|
@@ -93,7 +93,7 @@ export const LoadingState: Story = {
|
|
|
93
93
|
// Small size
|
|
94
94
|
export const Small: Story = {
|
|
95
95
|
args: {
|
|
96
|
-
src: "/
|
|
96
|
+
src: "https://picsum.photos/200",
|
|
97
97
|
fetch: fetchImage,
|
|
98
98
|
className: "w-16 h-16 rounded",
|
|
99
99
|
},
|
|
@@ -102,7 +102,7 @@ export const Small: Story = {
|
|
|
102
102
|
// Large size
|
|
103
103
|
export const Large: Story = {
|
|
104
104
|
args: {
|
|
105
|
-
src: "/
|
|
105
|
+
src: "https://picsum.photos/200",
|
|
106
106
|
fetch: fetchImage,
|
|
107
107
|
className: "w-64 h-64 rounded-xl",
|
|
108
108
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Shimmer from "@/components/notion-ui/shimmer";
|
|
1
|
+
import { Shimmer } from "@/components/notion-ui/shimmer";
|
|
2
2
|
import { cn } from "@/utils/cn";
|
|
3
3
|
import React, { useEffect, useState, forwardRef, useRef } from "react";
|
|
4
4
|
|
|
@@ -31,7 +31,7 @@ interface ApiConfigImageProps extends BaseImageProps {
|
|
|
31
31
|
fetch?: never;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
type ImageProps = FetchImageProps | ApiConfigImageProps;
|
|
35
35
|
|
|
36
36
|
/* ---------------------------------- */
|
|
37
37
|
/* Cache helpers */
|
|
@@ -40,6 +40,10 @@ export type ImageProps = FetchImageProps | ApiConfigImageProps;
|
|
|
40
40
|
const IMAGE_CACHE = "image-cache-v1";
|
|
41
41
|
|
|
42
42
|
async function getCachedImage(url: string): Promise<string | null> {
|
|
43
|
+
if (typeof window === "undefined" || typeof caches === "undefined") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
const cache = await caches.open(IMAGE_CACHE);
|
|
44
48
|
const cached = await cache.match(url);
|
|
45
49
|
|
|
@@ -50,6 +54,9 @@ async function getCachedImage(url: string): Promise<string | null> {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
async function cacheImage(url: string, response: Response) {
|
|
57
|
+
if (typeof window === "undefined" || typeof caches === "undefined") {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
53
60
|
const cache = await caches.open(IMAGE_CACHE);
|
|
54
61
|
await cache.put(url, response.clone());
|
|
55
62
|
}
|
|
@@ -75,6 +82,21 @@ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
|
75
82
|
|
|
76
83
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
77
84
|
const [loading, setLoading] = useState(true);
|
|
85
|
+
const previousUrlRef = useRef<string | null>(null);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (previousUrlRef.current?.startsWith("blob:")) {
|
|
89
|
+
URL.revokeObjectURL(previousUrlRef.current);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
previousUrlRef.current = imageUrl;
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
if (previousUrlRef.current?.startsWith("blob:")) {
|
|
96
|
+
URL.revokeObjectURL(previousUrlRef.current);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}, [imageUrl]);
|
|
78
100
|
|
|
79
101
|
const { shimmerClassName, shimmerIconClassName } = classNames || {};
|
|
80
102
|
|
|
@@ -83,7 +105,7 @@ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
|
83
105
|
fetchRef.current = fetch;
|
|
84
106
|
}, [fetch]);
|
|
85
107
|
|
|
86
|
-
async function loadImage() {
|
|
108
|
+
async function loadImage(signal: AbortSignal) {
|
|
87
109
|
try {
|
|
88
110
|
const resolvedSrc = apiConfig?.src ?? src;
|
|
89
111
|
|
|
@@ -106,8 +128,10 @@ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
|
106
128
|
/* ---------------------------------- */
|
|
107
129
|
const cached = await getCachedImage(resolvedSrc);
|
|
108
130
|
if (cached) {
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
if (!signal.aborted) {
|
|
132
|
+
setImageUrl(cached);
|
|
133
|
+
setLoading(false);
|
|
134
|
+
}
|
|
111
135
|
return;
|
|
112
136
|
}
|
|
113
137
|
|
|
@@ -118,7 +142,9 @@ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
|
118
142
|
? await fetchRef.current(resolvedSrc)
|
|
119
143
|
: await window.fetch(resolvedSrc, {
|
|
120
144
|
headers: apiConfig?.headers,
|
|
145
|
+
signal,
|
|
121
146
|
});
|
|
147
|
+
if (signal.aborted) return;
|
|
122
148
|
|
|
123
149
|
const contentType = response.headers.get("content-type") ?? "";
|
|
124
150
|
|
|
@@ -137,38 +163,35 @@ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
|
137
163
|
} catch (err) {
|
|
138
164
|
console.error(err);
|
|
139
165
|
} finally {
|
|
140
|
-
|
|
166
|
+
if (!signal.aborted) {
|
|
167
|
+
setLoading(false);
|
|
168
|
+
}
|
|
141
169
|
}
|
|
142
170
|
}
|
|
143
171
|
|
|
144
172
|
useEffect(() => {
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
setLoading(true);
|
|
147
175
|
|
|
148
|
-
|
|
149
|
-
/* Cleanup */
|
|
150
|
-
/* ---------------------------------- */
|
|
176
|
+
loadImage(controller.signal);
|
|
151
177
|
|
|
152
|
-
useEffect(() => {
|
|
153
178
|
return () => {
|
|
154
|
-
|
|
155
|
-
URL.revokeObjectURL(imageUrl);
|
|
156
|
-
}
|
|
179
|
+
controller.abort();
|
|
157
180
|
};
|
|
158
|
-
}, [
|
|
181
|
+
}, [src, apiConfig?.src]);
|
|
159
182
|
|
|
160
183
|
/* ---------------------------------- */
|
|
161
184
|
/* UI */
|
|
162
185
|
/* ---------------------------------- */
|
|
163
186
|
|
|
164
187
|
if (loading || !imageUrl) {
|
|
165
|
-
const stop = loading
|
|
188
|
+
const stop = !loading && !imageUrl;
|
|
166
189
|
|
|
167
190
|
return (
|
|
168
191
|
<Shimmer
|
|
169
192
|
className={cn(
|
|
170
193
|
"bg-primary/10 mx-auto flex p-2 items-center size-8 rounded border border-tertiary/10",
|
|
171
|
-
shimmerClassName
|
|
194
|
+
shimmerClassName,
|
|
172
195
|
)}
|
|
173
196
|
stop={stop}
|
|
174
197
|
>
|
|
@@ -182,7 +205,7 @@ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
|
182
205
|
strokeLinejoin="round"
|
|
183
206
|
className={cn(
|
|
184
207
|
"stroke-primary/40 mx-auto stroke-2",
|
|
185
|
-
shimmerIconClassName
|
|
208
|
+
shimmerIconClassName,
|
|
186
209
|
)}
|
|
187
210
|
>
|
|
188
211
|
<rect x="1" y="1" width="22" height="22" rx="2" ry="2" />
|
|
@@ -196,12 +219,10 @@ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
|
196
219
|
return (
|
|
197
220
|
<div
|
|
198
221
|
ref={ref}
|
|
199
|
-
// src={imageUrl}
|
|
200
222
|
style={{ backgroundImage: `url(${imageUrl})` }}
|
|
201
|
-
// alt={alt}
|
|
202
223
|
className={cn(
|
|
203
224
|
"cursor-pointer shadow-lg bg-cover bg-center mx-auto",
|
|
204
|
-
className
|
|
225
|
+
className,
|
|
205
226
|
)}
|
|
206
227
|
{...imgProps}
|
|
207
228
|
/>
|
|
@@ -210,4 +231,4 @@ const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
|
210
231
|
|
|
211
232
|
CachedImage.displayName = "CachedImage";
|
|
212
233
|
|
|
213
|
-
export
|
|
234
|
+
export { CachedImage, type ImageProps };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import CachedImage from "./cached-image";
|
|
1
|
+
import { ImageProps, CachedImage } from "./cached-image";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export { CachedImage, type ImageProps };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { CachedSvg } from "@/components/notion-ui/cached-svg";
|
|
1
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
3
|
|
|
3
|
-
import CachedSvg from "./cached-svg";
|
|
4
|
-
|
|
5
4
|
/* ---------------------------------- */
|
|
6
5
|
/* Meta */
|
|
7
6
|
/* ---------------------------------- */
|
|
@@ -36,11 +35,10 @@ const sampleSvg = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
|
36
35
|
/* -------- Fetch-based example -------- */
|
|
37
36
|
export const FetchExample: Story = {
|
|
38
37
|
render: () => (
|
|
39
|
-
<div
|
|
38
|
+
<div>
|
|
40
39
|
<CachedSvg
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return new Response(sampleSvg, { status: 200 });
|
|
40
|
+
apiConfig={{
|
|
41
|
+
src: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/af.svg",
|
|
44
42
|
}}
|
|
45
43
|
/>
|
|
46
44
|
</div>
|
|
@@ -50,12 +48,12 @@ export const FetchExample: Story = {
|
|
|
50
48
|
/* -------- API-config-based example -------- */
|
|
51
49
|
export const ApiConfigExample: Story = {
|
|
52
50
|
render: () => (
|
|
53
|
-
<div
|
|
51
|
+
<div>
|
|
54
52
|
<CachedSvg
|
|
55
53
|
apiConfig={{
|
|
56
|
-
src: "https://
|
|
57
|
-
headers: { Authorization: "Bearer token" },
|
|
54
|
+
src: "https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/af.svg",
|
|
58
55
|
}}
|
|
56
|
+
className="size-32"
|
|
59
57
|
/>
|
|
60
58
|
</div>
|
|
61
59
|
),
|
|
@@ -64,9 +62,9 @@ export const ApiConfigExample: Story = {
|
|
|
64
62
|
/* -------- Loading state (Shimmer) -------- */
|
|
65
63
|
export const LoadingState: Story = {
|
|
66
64
|
render: () => (
|
|
67
|
-
<div
|
|
65
|
+
<div>
|
|
68
66
|
<CachedSvg
|
|
69
|
-
src="https://
|
|
67
|
+
src="https://cdn.jsdelivr.net/gh/hampusborgos/country-flags@main/svg/af.svg"
|
|
70
68
|
fetch={() => new Promise(() => {})} // never resolves to simulate loading
|
|
71
69
|
/>
|
|
72
70
|
</div>
|