notionsoft-ui 1.0.34 → 1.0.35

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/cache-svg/CachedSvg.stories.tsx +74 -0
  11. package/src/notion-ui/cache-svg/cached-svg.tsx +150 -0
  12. package/src/notion-ui/cache-svg/index.ts +3 -0
  13. package/src/notion-ui/cache-svg/utils.ts +7 -0
  14. package/src/notion-ui/cached-image/cached-image.stories.tsx +109 -0
  15. package/src/notion-ui/cached-image/cached-image.tsx +213 -0
  16. package/src/notion-ui/cached-image/index.ts +3 -0
  17. package/src/notion-ui/cached-image/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.35",
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,74 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+
3
+ import CachedSvg from "./cached-svg";
4
+
5
+ /* ---------------------------------- */
6
+ /* Meta */
7
+ /* ---------------------------------- */
8
+ const meta: Meta<typeof CachedSvg> = {
9
+ title: "Media/CachedSvg",
10
+ component: CachedSvg,
11
+ tags: ["autodocs"],
12
+ argTypes: {
13
+ className: { control: "text" },
14
+ classNames: { control: false },
15
+ src: { control: "text" },
16
+ fetch: { control: false },
17
+ apiConfig: { control: false },
18
+ },
19
+ };
20
+
21
+ export default meta;
22
+
23
+ type Story = StoryObj<typeof CachedSvg>;
24
+
25
+ /* ---------------------------------- */
26
+ /* Sample SVG string */
27
+ /* ---------------------------------- */
28
+ const sampleSvg = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
29
+ <circle cx="12" cy="12" r="10" fill="currentColor"/>
30
+ </svg>`;
31
+
32
+ /* ---------------------------------- */
33
+ /* Stories */
34
+ /* ---------------------------------- */
35
+
36
+ /* -------- Fetch-based example -------- */
37
+ export const FetchExample: Story = {
38
+ render: () => (
39
+ <div style={{ width: 40, height: 40 }}>
40
+ <CachedSvg
41
+ src="https://dummy-svg-url.com/sample.svg"
42
+ fetch={async () => {
43
+ return new Response(sampleSvg, { status: 200 });
44
+ }}
45
+ />
46
+ </div>
47
+ ),
48
+ };
49
+
50
+ /* -------- API-config-based example -------- */
51
+ export const ApiConfigExample: Story = {
52
+ render: () => (
53
+ <div style={{ width: 40, height: 40 }}>
54
+ <CachedSvg
55
+ apiConfig={{
56
+ src: "https://dummy-api.com/svg",
57
+ headers: { Authorization: "Bearer token" },
58
+ }}
59
+ />
60
+ </div>
61
+ ),
62
+ };
63
+
64
+ /* -------- Loading state (Shimmer) -------- */
65
+ export const LoadingState: Story = {
66
+ render: () => (
67
+ <div style={{ width: 40, height: 40 }}>
68
+ <CachedSvg
69
+ src="https://dummy-loading.com/svg"
70
+ fetch={() => new Promise(() => {})} // never resolves to simulate loading
71
+ />
72
+ </div>
73
+ ),
74
+ };
@@ -0,0 +1,150 @@
1
+ import React, { forwardRef, useEffect, useRef, useState } from "react";
2
+ import Shimmer from "../shimmer";
3
+ import { cn } from "../../utils/cn";
4
+ import DOMPurify from "dompurify";
5
+
6
+ /* ---------------------------------- */
7
+ /* Types */
8
+ /* ---------------------------------- */
9
+
10
+ interface BaseSvgProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ classNames?: {
12
+ shimmerClassName?: string;
13
+ };
14
+ }
15
+
16
+ interface FetchSvgProps extends BaseSvgProps {
17
+ src: string;
18
+ fetch: (src: string) => Promise<Response>;
19
+ apiConfig?: never;
20
+ }
21
+
22
+ interface ApiConfigSvgProps extends BaseSvgProps {
23
+ apiConfig: {
24
+ src: string;
25
+ headers?: Record<string, string>;
26
+ };
27
+ src?: never;
28
+ fetch?: never;
29
+ }
30
+
31
+ export type CachedSvgProps = FetchSvgProps | ApiConfigSvgProps;
32
+
33
+ /* ---------------------------------- */
34
+ /* Cache */
35
+ /* ---------------------------------- */
36
+
37
+ const SVG_CACHE = "svg-cache-v1";
38
+
39
+ /* ---------------------------------- */
40
+ /* Sanitizer */
41
+ /* ---------------------------------- */
42
+
43
+ function sanitizeSvg(svg: string): string {
44
+ return DOMPurify.sanitize(svg, {
45
+ USE_PROFILES: { svg: true, svgFilters: true },
46
+ FORBID_TAGS: ["script", "foreignObject", "iframe", "object", "embed"],
47
+ FORBID_ATTR: [
48
+ "onload",
49
+ "onclick",
50
+ "onmouseover",
51
+ "onerror",
52
+ "href",
53
+ "xlink:href",
54
+ ],
55
+ });
56
+ }
57
+
58
+ /* ---------------------------------- */
59
+ /* Component */
60
+ /* ---------------------------------- */
61
+
62
+ const CachedSvg = forwardRef<HTMLDivElement, CachedSvgProps>(
63
+ ({ className, classNames, fetch, apiConfig, ...rest }, ref) => {
64
+ const [svg, setSvg] = useState<string | null>(null);
65
+ const [loading, setLoading] = useState(true);
66
+
67
+ const fetchRef = useRef(fetch);
68
+ useEffect(() => {
69
+ fetchRef.current = fetch;
70
+ }, [fetch]);
71
+
72
+ async function loadSvg() {
73
+ try {
74
+ const src = apiConfig?.src ?? (rest as any).src;
75
+ if (!src) return;
76
+
77
+ /* ---------------- Cache ---------------- */
78
+
79
+ if ("caches" in window) {
80
+ const cache = await caches.open(SVG_CACHE);
81
+ const cached = await cache.match(src);
82
+
83
+ if (cached) {
84
+ const text = await cached.text();
85
+ setSvg(sanitizeSvg(text));
86
+ setLoading(false);
87
+ return;
88
+ }
89
+ }
90
+
91
+ /* ---------------- Fetch ---------------- */
92
+
93
+ const response = fetchRef.current
94
+ ? await fetchRef.current(src)
95
+ : await window.fetch(src, { headers: apiConfig?.headers });
96
+
97
+ if (!response.ok) throw new Error("SVG fetch failed");
98
+
99
+ const clone = response.clone();
100
+ const text = await response.text();
101
+
102
+ setSvg(sanitizeSvg(text));
103
+
104
+ if ("caches" in window) {
105
+ const cache = await caches.open(SVG_CACHE);
106
+ await cache.put(src, clone);
107
+ }
108
+ } catch (err) {
109
+ console.error(err);
110
+ } finally {
111
+ setLoading(false);
112
+ }
113
+ }
114
+
115
+ useEffect(() => {
116
+ loadSvg();
117
+ }, [apiConfig?.src, (rest as any).src]);
118
+
119
+ /* ---------------- UI ---------------- */
120
+ const iconStyle = "opacity-90 rounded-full w-[20px] h-[18px]";
121
+ if (loading || !svg) {
122
+ return (
123
+ <Shimmer
124
+ className={cn(
125
+ "bg-primary/10",
126
+ iconStyle,
127
+ classNames?.shimmerClassName
128
+ )}
129
+ />
130
+ );
131
+ }
132
+
133
+ return (
134
+ <div
135
+ ref={ref}
136
+ className={cn(
137
+ "[&>svg>path]:fill-current [&>svg>g>*]:fill-current items-center justify-center flex",
138
+ iconStyle,
139
+ className
140
+ )}
141
+ dangerouslySetInnerHTML={{ __html: svg }}
142
+ {...rest}
143
+ />
144
+ );
145
+ }
146
+ );
147
+
148
+ CachedSvg.displayName = "CachedSvg";
149
+
150
+ export default CachedSvg;
@@ -0,0 +1,3 @@
1
+ import CachedSvg from "./cached-svg";
2
+
3
+ export default CachedSvg;
@@ -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
+ }