@tomo-inc/tomo-ui 0.0.8 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomo-inc/tomo-ui",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -17,9 +17,11 @@
17
17
  "private": false,
18
18
  "dependencies": {
19
19
  "@heroui/react": "2.8.5",
20
- "@heroui/toast": "^2.0.17",
20
+ "@heroui/system": "^2.4.23",
21
+ "@heroui/theme": "^2.4.23",
22
+ "color": "^5.0.3",
23
+ "flat": "^6.0.1",
21
24
  "framer-motion": "^11.5.6",
22
- "next-themes": "^0.4.6",
23
25
  "qr-code-styling": "^1.9.2"
24
26
  },
25
27
  "peerDependencies": {
package/src/App.tsx CHANGED
@@ -1,23 +1,143 @@
1
- import { QRCodeComponent } from "./components";
1
+ import { Switch, useDisclosure } from "@heroui/react";
2
+ import { useTheme } from ".";
2
3
  import { Button } from "./components/button";
3
- import { Input, NumberInput } from "./components/input";
4
+ import { Input } from "./components/input";
5
+ import { Modal, ModalContent, ModalHeader } from "./components/modal";
6
+
4
7
  import "./style.css";
5
8
  import { TomoUIProvider } from "./theme-provider";
9
+ import { ThemeConfig } from "./types";
6
10
 
7
- function App() {
11
+ const _dogeThemeConfig: ThemeConfig = {
12
+ themes: {
13
+ light: {
14
+ colors: {
15
+ foreground: "#12122A",
16
+ background: "#FFF",
17
+ content1: "#FCFCFD",
18
+ primary: {
19
+ 50: "#FFFBEA",
20
+ 100: "#FFF3C4",
21
+ 200: "#FCE588",
22
+ 300: "#FADB5F",
23
+ 400: "#F7C948",
24
+ 500: "#FCD436",
25
+ 600: "#F0B429",
26
+ 700: "#DE911D",
27
+ 800: "#CB6E17",
28
+ 900: "#B44D12",
29
+ foreground: "#12122A",
30
+ DEFAULT: "#FCD436",
31
+ },
32
+ danger: "#FF5A5A",
33
+ warning: "#FFAD32",
34
+ success: "#079455",
35
+ t1: "#12122A",
36
+ t2: "#616184",
37
+ t3: "#8989AB",
38
+ t4: "#C1C0D8",
39
+ t5: "#EEC41F",
40
+ } as any,
41
+ },
42
+ },
43
+ };
44
+
45
+ export const MoonIcon = (props: React.SVGProps<SVGSVGElement>) => {
8
46
  return (
9
- <TomoUIProvider defaultTheme="doge_light" forcedTheme="doge_light">
10
- 123123
11
- <Input label="Email" type="email" />
12
- <NumberInput label="Number" placeholder="Enter your number" type="number" />
13
- <Button color="primary">Click me</Button>
14
- <QRCodeComponent
15
- content="https://tomo.inc"
16
- size={280}
17
- logo="https://faucet.testnet.dogeos.com/images/h4.avif"
18
- logoBackground="#1C1917"
47
+ <svg
48
+ aria-hidden="true"
49
+ focusable="false"
50
+ height="1em"
51
+ role="presentation"
52
+ viewBox="0 0 24 24"
53
+ width="1em"
54
+ {...props}
55
+ >
56
+ <path
57
+ d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
58
+ fill="currentColor"
19
59
  />
20
- </TomoUIProvider>
60
+ </svg>
61
+ );
62
+ };
63
+
64
+ export const SunIcon = (props: React.SVGProps<SVGSVGElement>) => {
65
+ return (
66
+ <svg
67
+ aria-hidden="true"
68
+ focusable="false"
69
+ height="1em"
70
+ role="presentation"
71
+ viewBox="0 0 24 24"
72
+ width="1em"
73
+ {...props}
74
+ >
75
+ <g fill="currentColor">
76
+ <path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
77
+ <path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
78
+ </g>
79
+ </svg>
80
+ );
81
+ };
82
+
83
+ export const SwitchTheme = () => {
84
+ const { theme, setTheme } = useTheme();
85
+ return (
86
+ <Switch
87
+ color="success"
88
+ size="lg"
89
+ endContent={<MoonIcon />}
90
+ startContent={<SunIcon />}
91
+ isSelected={theme === "dark"}
92
+ onValueChange={(isSelected) => setTheme(isSelected ? "dark" : "light")}
93
+ />
94
+ );
95
+ };
96
+
97
+ function App() {
98
+ const { isOpen, onOpen, onOpenChange } = useDisclosure();
99
+
100
+ return (
101
+ <div>
102
+ <TomoUIProvider>
103
+ <div className="bg-content1 p-4 rounded-lg flex flex-col gap-4 border border-primary">
104
+ <div className="flex items-center justify-between">
105
+ <h1 className="text-2xl font-bold text-foreground">Tomo Theme Config Preview</h1>
106
+ <SwitchTheme />
107
+ </div>
108
+ <Input label="Email" type="email" />
109
+ <Button color="primary" variant="solid">
110
+ Click me
111
+ </Button>
112
+ <Button color="primary" variant="bordered">
113
+ Click me
114
+ </Button>
115
+ </div>
116
+ </TomoUIProvider>
117
+ <br />
118
+ <TomoUIProvider themeConfig={_dogeThemeConfig}>
119
+ <div className="bg-content1 p-4 rounded-lg flex flex-col gap-4 border border-primary">
120
+ <div className="flex items-center justify-between">
121
+ <h1 className="text-2xl font-bold text-foreground">Doge Theme Config Preview</h1>
122
+ <SwitchTheme />
123
+ </div>
124
+ <Input label="Email" type="email" />
125
+ <Button color="primary" variant="solid">
126
+ Click me
127
+ </Button>
128
+ <Button color="primary" variant="bordered" onPress={onOpen}>
129
+ Open Modal
130
+ </Button>
131
+ </div>
132
+ <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
133
+ <ModalContent>
134
+ <ModalHeader>
135
+ <h1>Modal</h1>
136
+ </ModalHeader>
137
+ </ModalContent>
138
+ </Modal>
139
+ </TomoUIProvider>
140
+ </div>
21
141
  );
22
142
  }
23
143
  export default App;
@@ -6,6 +6,7 @@ export * from "./input";
6
6
  export * from "./link";
7
7
  export * from "./mfaTypeChoose";
8
8
  export * from "./modal";
9
+ export * from "./passcodeInput";
9
10
  export * from "./qr";
10
11
  export * from "./select";
11
12
  export * from "./skeleton";
@@ -1,13 +1,10 @@
1
- export { Modal } from "./modal";
2
- export { ModalBody } from "./modal-body";
3
- export { ModalContent } from "./modal-content";
4
- export { ModalFooter } from "./modal-footer";
5
- export { ModalHeader } from "./modal-header";
6
- export { useDisclosure } from "./use-disclosure";
1
+ export { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, useDisclosure } from "@heroui/react";
7
2
 
8
- export type { ModalProps } from "./modal";
9
- export type { ModalBodyProps } from "./modal-body";
10
- export type { ModalContentProps } from "./modal-content";
11
- export type { ModalFooterProps } from "./modal-footer";
12
- export type { ModalHeaderProps } from "./modal-header";
13
- export type { UseDisclosureProps } from "./use-disclosure";
3
+ export type {
4
+ ModalBodyProps,
5
+ ModalContentProps,
6
+ ModalFooterProps,
7
+ ModalHeaderProps,
8
+ ModalProps,
9
+ UseDisclosureProps,
10
+ } from "@heroui/react";
@@ -0,0 +1,81 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ interface PasscodeInputProps {
4
+ length?: number;
5
+ value: string;
6
+ onChange: (val: string) => void;
7
+ onFill?: (val: string) => void;
8
+ }
9
+
10
+ export function PasscodeInput({ length = 6, value, onChange, onFill }: PasscodeInputProps) {
11
+ const inputRef = useRef<HTMLInputElement>(null);
12
+
13
+ useEffect(() => {
14
+ if (inputRef.current) {
15
+ inputRef.current.focus();
16
+ }
17
+ }, []);
18
+
19
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
20
+ let val = e.target.value.replace(/\D/g, "");
21
+ if (val.length > length) val = val.slice(0, length);
22
+
23
+ onChange(val);
24
+
25
+ if (val.length === length) {
26
+ onFill?.(val);
27
+ }
28
+ };
29
+
30
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
31
+ if (e.key === "Backspace" && value.length > 0) {
32
+ e.preventDefault();
33
+ onChange(value.slice(0, -1));
34
+ }
35
+ };
36
+
37
+ const chars = Array(length)
38
+ .fill("")
39
+ .map((_, i) => value[i] || "");
40
+
41
+ // Current cursor position
42
+ const activeIndex = chars.findIndex((c) => c === "") === -1 ? length - 1 : chars.findIndex((c) => c === "");
43
+
44
+ return (
45
+ <div className="flex gap-1 cursor-text w-full" onClick={() => inputRef.current?.focus()}>
46
+ {chars.map((char, i) => {
47
+ const isActive = i === activeIndex;
48
+ const isFilled = !!char;
49
+
50
+ let boxStyle = "";
51
+ if (isFilled) {
52
+ boxStyle = "border-primary bg-primary-50";
53
+ } else if (isActive) {
54
+ boxStyle = "border-primary bg-background";
55
+ } else {
56
+ boxStyle = "border-default bg-background";
57
+ }
58
+
59
+ return (
60
+ <div
61
+ key={i}
62
+ className={`w-full h-[70px] flex items-center justify-center rounded-2xl border text-2xl font-bold transition-colors ${boxStyle}`}
63
+ >
64
+ {char || "-"}
65
+ </div>
66
+ );
67
+ })}
68
+
69
+ <input
70
+ ref={inputRef}
71
+ type="text"
72
+ inputMode="numeric"
73
+ maxLength={length}
74
+ value={value}
75
+ onChange={handleChange}
76
+ onKeyDown={handleKeyDown}
77
+ className="absolute opacity-0 pointer-events-none"
78
+ />
79
+ </div>
80
+ );
81
+ }
@@ -1,2 +1 @@
1
- // Re-export toast functionality from @heroui/toast
2
- export { addToast, ToastProvider } from "@heroui/toast";
1
+ export { addToast, ToastProvider } from "@heroui/react";
package/src/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./components";
2
2
  export * from "./icons";
3
3
 
4
4
  export { TomoUIProvider } from "./theme-provider";
5
+ export { useTheme } from "./theme-context";
package/src/style.css CHANGED
@@ -1,4 +1,3 @@
1
1
  @import "./tailwind/tailwind.css";
2
2
 
3
- @source '../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
4
- @source "../src/**/*.{js,ts,jsx,tsx,mdx}";
3
+ @source "./**/*.{js,ts,jsx,tsx,mdx}";
@@ -1,11 +1,23 @@
1
- import { heroui } from "@heroui/react";
1
+ import { heroui, HeroUIPluginConfig } from "@heroui/react";
2
2
 
3
- export const ThemePlugin: any = heroui({
4
- layout: {},
3
+ export const baseConfig: HeroUIPluginConfig = {
4
+ // Don't change this prefix, some css not working
5
+ prefix: "heroui",
6
+ // prefix: "tomoui",
5
7
  themes: {
6
- light: {},
7
- dark: {},
8
- doge_light: {
8
+ // light: {
9
+ // colors: {
10
+ // foreground: "#000",
11
+ // background: "#FFF",
12
+ // content1: "#FCFCFD",
13
+ // primary: {
14
+ // ...generateColorScale("#fe3c9c"),
15
+ // DEFAULT: "#fe3c9c",
16
+ // foreground: "#fff",
17
+ // },
18
+ // },
19
+ // },
20
+ light: {
9
21
  colors: {
10
22
  foreground: "#12122A",
11
23
  background: "#FFF",
@@ -34,144 +46,183 @@ export const ThemePlugin: any = heroui({
34
46
  t5: "#EEC41F",
35
47
  } as any,
36
48
  },
37
- wallet_kit_light: {
38
- colors: {
39
- foreground: "#12122A",
40
- background: "#FFF",
41
- content1: "#FCFCFD",
42
- primary: {
43
- 50: "#FFFBEA",
44
- 100: "#FFF3C4",
45
- 200: "#FCE588",
46
- 300: "#FADB5F",
47
- 400: "#F7C948",
48
- 500: "#FCD436",
49
- 600: "#F0B429",
50
- 700: "#DE911D",
51
- 800: "#CB6E17",
52
- 900: "#B44D12",
53
- DEFAULT: "#FCD436",
54
- },
55
- danger: "#FF5A5A",
56
- warning: "#FFAD32",
57
- success: "#079455",
58
- t1: "#12122A",
59
- t2: "#616184",
60
- t3: "#8989AB",
61
- t4: "#C1C0D8",
62
- t5: "#EEC41F",
63
- } as any,
64
- },
65
- mydoge: {
66
- colors: {
67
- primary: {
68
- "50": "#fffef7",
69
- "100": "#fffceb",
70
- "200": "#fff9d5",
71
- "300": "#fff5b8",
72
- "400": "#feef8a",
73
- "500": "#FED70B",
74
- "600": "#e5c009",
75
- "700": "#bfa007",
76
- "800": "#998006",
77
- "900": "#736005",
78
- foreground: "#000",
79
- DEFAULT: "#FED70B",
80
- },
81
- secondary: {
82
- "50": "#f5f3ff",
83
- "100": "#ede9fe",
84
- "200": "#ddd6fe",
85
- "300": "#c4b5fd",
86
- "400": "#a78bfa",
87
- "500": "#8b5cf6",
88
- "600": "#7c3aed",
89
- "700": "#6d28d9",
90
- "800": "#5b21b6",
91
- "900": "#4c1d95",
92
- foreground: "#fff",
93
- DEFAULT: "#8b5cf6",
94
- },
95
- default: {
96
- "50": "#0e0e0e",
97
- "100": "#1c1c1c",
98
- "200": "#2a2a2a",
99
- "300": "#383838",
100
- "400": "#464646",
101
- "500": "#6b6b6b",
102
- "600": "#909090",
103
- "700": "#b5b5b5",
104
- "800": "#dadada",
105
- "900": "#ffffff",
106
- foreground: "#ffffff",
107
- DEFAULT: "#464646",
108
- },
109
- success: {
110
- "50": "#f0fdf4",
111
- "100": "#dcfce7",
112
- "200": "#bbf7d0",
113
- "300": "#86efac",
114
- "400": "#4ade80",
115
- "500": "#10b981",
116
- "600": "#059669",
117
- "700": "#047857",
118
- "800": "#065f46",
119
- "900": "#064e3b",
120
- foreground: "#fff",
121
- DEFAULT: "#10b981",
122
- },
123
- warning: {
124
- "50": "#fffbeb",
125
- "100": "#fef3c7",
126
- "200": "#fde68a",
127
- "300": "#fcd34d",
128
- "400": "#fbbf24",
129
- "500": "#f59e0b",
130
- "600": "#d97706",
131
- "700": "#b45309",
132
- "800": "#92400e",
133
- "900": "#78350f",
134
- foreground: "#000",
135
- DEFAULT: "#f59e0b",
136
- },
137
- danger: {
138
- "50": "#fef2f2",
139
- "100": "#fee2e2",
140
- "200": "#fecaca",
141
- "300": "#fca5a5",
142
- "400": "#f87171",
143
- "500": "#ef4444",
144
- "600": "#dc2626",
145
- "700": "#b91c1c",
146
- "800": "#991b1b",
147
- "900": "#7f1d1d",
148
- foreground: "#fff",
149
- DEFAULT: "#ef4444",
150
- },
151
- background: "#000000",
152
- foreground: "#ffffff",
153
- content1: {
154
- DEFAULT: "#1A1A1A",
155
- foreground: "#fff",
156
- },
157
- content2: {
158
- DEFAULT: "#242424",
159
- foreground: "#fff",
160
- },
161
- content3: {
162
- DEFAULT: "#464646",
163
- foreground: "#fff",
164
- },
165
- content4: {
166
- DEFAULT: "#666666",
167
- foreground: "#fff",
168
- },
169
- focus: "#FED70B",
170
- overlay: "#000000",
171
- divider: "#464646",
172
- },
173
- },
49
+ dark: {},
174
50
  },
175
- });
51
+ };
176
52
 
53
+ export const ThemePlugin = heroui(baseConfig) as any;
177
54
  export default ThemePlugin;
55
+
56
+ // export const ThemePlugin: any = heroui({
57
+ // layout: {},
58
+ // themes: {
59
+ // light: {
60
+ // colors: {
61
+ // foreground: "#12122A",
62
+ // background: "#FFF",
63
+ // content1: "#FCFCFD",
64
+ // primary: {
65
+ // 50: "#FFFBEA",
66
+ // 100: "#FFF3C4",
67
+ // 200: "#FCE588",
68
+ // 300: "#FADB5F",
69
+ // 400: "#F7C948",
70
+ // 500: "#FCD436",
71
+ // 600: "#F0B429",
72
+ // 700: "#DE911D",
73
+ // 800: "#CB6E17",
74
+ // 900: "#B44D12",
75
+ // DEFAULT: "#FCD436",
76
+ // },
77
+ // danger: "#FF5A5A",
78
+ // warning: "#FFAD32",
79
+ // success: "#079455",
80
+ // // Remove no required colors
81
+ // // t1: "#12122A",
82
+ // // t2: "#616184",
83
+ // // t3: "#8989AB",
84
+ // // t4: "#C1C0D8",
85
+ // // t5: "#EEC41F",
86
+ // },
87
+ // },
88
+ // // doge_light: {
89
+ // // colors: {
90
+ // // foreground: "#12122A",
91
+ // // background: "#FFF",
92
+ // // content1: "#FCFCFD",
93
+ // // primary: {
94
+ // // 50: "#FFFBEA",
95
+ // // 100: "#FFF3C4",
96
+ // // 200: "#FCE588",
97
+ // // 300: "#FADB5F",
98
+ // // 400: "#F7C948",
99
+ // // 500: "#FCD436",
100
+ // // 600: "#F0B429",
101
+ // // 700: "#DE911D",
102
+ // // 800: "#CB6E17",
103
+ // // 900: "#B44D12",
104
+ // // foreground: "#12122A",
105
+ // // DEFAULT: "#FCD436",
106
+ // // },
107
+ // // danger: "#FF5A5A",
108
+ // // warning: "#FFAD32",
109
+ // // success: "#079455",
110
+ // // t1: "#12122A",
111
+ // // t2: "#616184",
112
+ // // t3: "#8989AB",
113
+ // // t4: "#C1C0D8",
114
+ // // t5: "#EEC41F",
115
+ // // } as any,
116
+ // // },
117
+
118
+ // // mydoge: {
119
+ // // colors: {
120
+ // // primary: {
121
+ // // "50": "#fffef7",
122
+ // // "100": "#fffceb",
123
+ // // "200": "#fff9d5",
124
+ // // "300": "#fff5b8",
125
+ // // "400": "#feef8a",
126
+ // // "500": "#FED70B",
127
+ // // "600": "#e5c009",
128
+ // // "700": "#bfa007",
129
+ // // "800": "#998006",
130
+ // // "900": "#736005",
131
+ // // foreground: "#000",
132
+ // // DEFAULT: "#FED70B",
133
+ // // },
134
+ // // secondary: {
135
+ // // "50": "#f5f3ff",
136
+ // // "100": "#ede9fe",
137
+ // // "200": "#ddd6fe",
138
+ // // "300": "#c4b5fd",
139
+ // // "400": "#a78bfa",
140
+ // // "500": "#8b5cf6",
141
+ // // "600": "#7c3aed",
142
+ // // "700": "#6d28d9",
143
+ // // "800": "#5b21b6",
144
+ // // "900": "#4c1d95",
145
+ // // foreground: "#fff",
146
+ // // DEFAULT: "#8b5cf6",
147
+ // // },
148
+ // // default: {
149
+ // // "50": "#0e0e0e",
150
+ // // "100": "#1c1c1c",
151
+ // // "200": "#2a2a2a",
152
+ // // "300": "#383838",
153
+ // // "400": "#464646",
154
+ // // "500": "#6b6b6b",
155
+ // // "600": "#909090",
156
+ // // "700": "#b5b5b5",
157
+ // // "800": "#dadada",
158
+ // // "900": "#ffffff",
159
+ // // foreground: "#ffffff",
160
+ // // DEFAULT: "#464646",
161
+ // // },
162
+ // // success: {
163
+ // // "50": "#f0fdf4",
164
+ // // "100": "#dcfce7",
165
+ // // "200": "#bbf7d0",
166
+ // // "300": "#86efac",
167
+ // // "400": "#4ade80",
168
+ // // "500": "#10b981",
169
+ // // "600": "#059669",
170
+ // // "700": "#047857",
171
+ // // "800": "#065f46",
172
+ // // "900": "#064e3b",
173
+ // // foreground: "#fff",
174
+ // // DEFAULT: "#10b981",
175
+ // // },
176
+ // // warning: {
177
+ // // "50": "#fffbeb",
178
+ // // "100": "#fef3c7",
179
+ // // "200": "#fde68a",
180
+ // // "300": "#fcd34d",
181
+ // // "400": "#fbbf24",
182
+ // // "500": "#f59e0b",
183
+ // // "600": "#d97706",
184
+ // // "700": "#b45309",
185
+ // // "800": "#92400e",
186
+ // // "900": "#78350f",
187
+ // // foreground: "#000",
188
+ // // DEFAULT: "#f59e0b",
189
+ // // },
190
+ // // danger: {
191
+ // // "50": "#fef2f2",
192
+ // // "100": "#fee2e2",
193
+ // // "200": "#fecaca",
194
+ // // "300": "#fca5a5",
195
+ // // "400": "#f87171",
196
+ // // "500": "#ef4444",
197
+ // // "600": "#dc2626",
198
+ // // "700": "#b91c1c",
199
+ // // "800": "#991b1b",
200
+ // // "900": "#7f1d1d",
201
+ // // foreground: "#fff",
202
+ // // DEFAULT: "#ef4444",
203
+ // // },
204
+ // // background: "#000000",
205
+ // // foreground: "#ffffff",
206
+ // // content1: {
207
+ // // DEFAULT: "#1A1A1A",
208
+ // // foreground: "#fff",
209
+ // // },
210
+ // // content2: {
211
+ // // DEFAULT: "#242424",
212
+ // // foreground: "#fff",
213
+ // // },
214
+ // // content3: {
215
+ // // DEFAULT: "#464646",
216
+ // // foreground: "#fff",
217
+ // // },
218
+ // // content4: {
219
+ // // DEFAULT: "#666666",
220
+ // // foreground: "#fff",
221
+ // // },
222
+ // // focus: "#FED70B",
223
+ // // overlay: "#000000",
224
+ // // divider: "#464646",
225
+ // // },
226
+ // // },
227
+ // },
228
+ // });
@@ -1,5 +1,6 @@
1
1
  @import "tailwindcss";
2
2
 
3
- @plugin "./plugin";
3
+ @plugin "./plugin.ts";
4
4
 
5
5
  @source "../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx,mjs}";
6
+ @source "../components/**/*.{js,ts,jsx,tsx,mjs}";
@@ -0,0 +1,256 @@
1
+ import type { ColorScale, HeroUIPluginConfig, LayoutTheme } from "@heroui/theme";
2
+
3
+ import Color from "color";
4
+ import * as flat from "flat";
5
+ import { baseConfig } from "./plugin";
6
+
7
+ /**
8
+ * Convert camelCase to kebab-case
9
+ * Matches heroui's kebabCase utility
10
+ */
11
+ function toKebabCase(str: string): string {
12
+ return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
13
+ }
14
+
15
+ /**
16
+ * Remove DEFAULT keys (e.g., "primary-DEFAULT" -> "primary")
17
+ * This matches heroui plugin's removeDefaultKeys function
18
+ */
19
+ function removeDefaultKeys(obj: Record<string, unknown>): Record<string, unknown> {
20
+ const newObj: Record<string, unknown> = {};
21
+
22
+ for (const key in obj) {
23
+ if (key.endsWith("-DEFAULT")) {
24
+ newObj[key.replace("-DEFAULT", "")] = obj[key];
25
+ continue;
26
+ }
27
+ newObj[key] = obj[key];
28
+ }
29
+
30
+ return newObj;
31
+ }
32
+
33
+ /**
34
+ * Flatten nested theme objects to `a-b-c` keys.
35
+ * Uses the same logic as heroui plugin's flattenThemeObject
36
+ */
37
+ function flattenThemeObject(obj: Record<string, unknown>): Record<string, unknown> {
38
+ return removeDefaultKeys(
39
+ flat.flatten(obj, {
40
+ safe: true,
41
+ delimiter: "-",
42
+ }) as Record<string, unknown>,
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Convert color string to HSL format (h s% l%)
48
+ * Matches heroui plugin's color conversion logic
49
+ */
50
+ function toHslString(colorValue: string): string | null {
51
+ try {
52
+ const parsedColor = Color(colorValue).hsl().round(2).array();
53
+ const [h, s, l] = parsedColor;
54
+
55
+ return `${h} ${s}% ${l}%`;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Generate CSS variable name from prefix and color key
63
+ * This matches the naming convention used by heroui() plugin
64
+ */
65
+ function generateCSSVariableName(prefix: string, colorKey: string): string {
66
+ return `--${prefix}-${colorKey}`;
67
+ }
68
+
69
+ /**
70
+ * Process layout theme and convert to CSS variables
71
+ * Matches heroui plugin's layout processing logic
72
+ *
73
+ * @param layout - Layout theme object
74
+ * @param prefix - CSS variable prefix
75
+ * @returns Array of CSS variable strings
76
+ */
77
+ function processLayoutTheme(layout: LayoutTheme, prefix: string): string[] {
78
+ const cssVars: string[] = [];
79
+
80
+ if (!layout) return cssVars;
81
+
82
+ // Convert keys to kebab-case (matching heroui's mapKeys logic)
83
+ const kebabLayout: Record<string, unknown> = {};
84
+
85
+ for (const [key, value] of Object.entries(layout)) {
86
+ kebabLayout[toKebabCase(key)] = value;
87
+ }
88
+
89
+ // Flatten nested layout objects
90
+ const flatLayout = flat.flatten(kebabLayout, {
91
+ safe: true,
92
+ delimiter: "-",
93
+ }) as Record<string, unknown>;
94
+
95
+ // Process each layout value
96
+ for (const [key, value] of Object.entries(flatLayout)) {
97
+ if (!value) continue;
98
+
99
+ const layoutVariablePrefix = `--${prefix}-${key}`;
100
+
101
+ if (typeof value === "object" && !Array.isArray(value)) {
102
+ // Handle nested objects (e.g., boxShadow: { small: "...", medium: "..." })
103
+ for (const [nestedKey, nestedValue] of Object.entries(value)) {
104
+ if (!nestedValue) continue;
105
+
106
+ const nestedLayoutVariable = `${layoutVariablePrefix}-${nestedKey}`;
107
+
108
+ cssVars.push(` ${nestedLayoutVariable}: ${nestedValue};`);
109
+ }
110
+ } else {
111
+ // Handle singular values (e.g., disabledOpacity: 0.5)
112
+ // Special handling for opacity values: 0.5 -> .5
113
+ let formattedValue = value;
114
+
115
+ if (layoutVariablePrefix.includes("opacity") && typeof value === "number") {
116
+ formattedValue = value.toString().replace(/^0\./, ".");
117
+ }
118
+
119
+ cssVars.push(` ${layoutVariablePrefix}: ${formattedValue};`);
120
+ }
121
+ }
122
+
123
+ return cssVars;
124
+ }
125
+
126
+ /**
127
+ * Convert heroui theme configuration to CSS variables string
128
+ *
129
+ * This function takes a standard HeroUI theme config (same format as heroui({}))
130
+ * and converts it to CSS variables that can override the base theme.
131
+ *
132
+ * The base theme is automatically generated by heroui() plugin at build time.
133
+ * This function uses the same processing logic as heroui plugin:
134
+ * - Uses flat() to flatten nested objects
135
+ * - Uses Color() to convert colors to HSL
136
+ * - Generates CSS variables with the same naming convention
137
+ *
138
+ * @param config - HeroUI theme configuration object (same format as heroui({}))
139
+ * @param scopeId - Optional unique ID to scope CSS variables to a specific element
140
+ * @returns CSS variables as string that can be injected into CSS to override base theme
141
+ *
142
+ * @example
143
+ * const config: HeroUIPluginConfig = {
144
+ * themes: {
145
+ * light: {
146
+ * colors: {
147
+ * primary: "#ff0000"
148
+ * }
149
+ * }
150
+ * }
151
+ * };
152
+ * const css = themeConfigToCSS(config);
153
+ * // Returns: ":root { --heroui-primary: 0 100% 50%; }"
154
+ * const css = themeConfigToCSS(config, "unique-id");
155
+ * // Returns: "#unique-id [class=\"light\"] { --heroui-primary: 0 100% 50%; }"
156
+ */
157
+ export function themeConfigToCSS(config: HeroUIPluginConfig, scopeId?: string): string {
158
+ const prefix = baseConfig.prefix || "tomo"; // is Fixed
159
+ const themes = config.themes || {};
160
+ const cssBlocks: string[] = [];
161
+
162
+ // Process global layout (applies to all themes)
163
+ const globalLayoutVars = config.layout ? processLayoutTheme(config.layout, prefix) : [];
164
+
165
+ // Build scope selector prefix if scopeId is provided
166
+ // CSS variables are scoped to the element with the given ID
167
+ const scopeSelector = scopeId ? `#${scopeId.replace(/:/g, "\\:")}` : "";
168
+
169
+ // Process each theme (matching heroui plugin's resolveConfig logic)
170
+ for (const [themeName, themeConfig] of Object.entries(themes)) {
171
+ const cssVars: string[] = [];
172
+
173
+ // Process colors
174
+ if (themeConfig?.colors) {
175
+ // Flatten colors using the same method as heroui plugin
176
+ const flatColors = flattenThemeObject(themeConfig.colors as Record<string, ColorScale>);
177
+
178
+ // Extract colors -> HSL triplet values (matching heroui plugin)
179
+ for (const [colorKey, colorValue] of Object.entries(flatColors)) {
180
+ if (!colorValue) continue;
181
+
182
+ const value = toHslString(colorValue as string);
183
+
184
+ if (value) {
185
+ const varName = generateCSSVariableName(prefix, colorKey);
186
+
187
+ cssVars.push(` ${varName}: ${value};`);
188
+ }
189
+ }
190
+ }
191
+
192
+ // Process theme-specific layout (overrides global layout)
193
+ if (themeConfig?.layout) {
194
+ const themeLayoutVars = processLayoutTheme(themeConfig.layout, prefix);
195
+
196
+ cssVars.push(...themeLayoutVars);
197
+ } else if (globalLayoutVars.length > 0) {
198
+ // Apply global layout if no theme-specific layout
199
+ cssVars.push(...globalLayoutVars);
200
+ }
201
+
202
+ if (cssVars.length > 0) {
203
+ // Scope CSS variables to the element with the given ID when class matches
204
+ // Each TomoUIProvider instance has its own scoped div with class attribute
205
+ const selector = scopeId ? `${scopeSelector}[class="${themeName}"]` : `[class="${themeName}"]`;
206
+
207
+ cssBlocks.push(`${selector} {${cssVars.join("")}}`);
208
+ }
209
+ }
210
+
211
+ // If no themes but global layout exists, add it to :root or scoped element
212
+ if (cssBlocks.length === 0 && globalLayoutVars.length > 0) {
213
+ const rootSelector = scopeId ? scopeSelector : ":root";
214
+
215
+ cssBlocks.push(`${rootSelector} {${globalLayoutVars.join("")}}`);
216
+ }
217
+
218
+ return cssBlocks.join("\n");
219
+ }
220
+
221
+ /**
222
+ * Generate a color scale from a base color
223
+ * @param baseColor - The base color to generate the color scale from
224
+ * @returns A record of color scale values
225
+ *
226
+ * @example
227
+ * const colorScale = generateColorScale("#000000");
228
+ * // Returns: { "50": "#000000", "100": "#000000", "200": "#000000", "300": "#000000", "400": "#000000", "500": "#000000", "600": "#000000", "700": "#000000", "800": "#000000", "900": "#000000", "950": "#000000" }
229
+ */
230
+ export function generateColorScale(baseColor: string) {
231
+ const base = Color(baseColor);
232
+
233
+ const steps = {
234
+ "50": 0.45,
235
+ "100": 0.35,
236
+ "200": 0.25,
237
+ "300": 0.15,
238
+ "400": 0.07,
239
+ "500": 0,
240
+ "600": -0.07,
241
+ "700": -0.15,
242
+ "800": -0.25,
243
+ "900": -0.35,
244
+ "950": -0.45,
245
+ };
246
+
247
+ const scale: Record<string, string> = {};
248
+
249
+ Object.keys(steps).forEach((key) => {
250
+ const amount = steps[key as keyof typeof steps];
251
+
252
+ scale[key] = amount >= 0 ? base.lighten(amount).hex() : base.darken(Math.abs(amount)).hex();
253
+ });
254
+
255
+ return scale;
256
+ }
@@ -0,0 +1,47 @@
1
+ import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
2
+
3
+ type Theme = string;
4
+
5
+ interface ThemeContextType {
6
+ theme: Theme;
7
+ setTheme: (theme: Theme) => void;
8
+ }
9
+
10
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
11
+
12
+ interface ThemeContextProviderProps {
13
+ children: ReactNode;
14
+ defaultTheme?: Theme;
15
+ forcedTheme?: Theme;
16
+ themes?: string[];
17
+ }
18
+
19
+ export function ThemeContextProvider({ children, defaultTheme = "light", forcedTheme }: ThemeContextProviderProps) {
20
+ const [theme, setThemeState] = useState<Theme>(forcedTheme || defaultTheme);
21
+
22
+ const setTheme = useCallback(
23
+ (newTheme: Theme) => {
24
+ if (!forcedTheme) {
25
+ setThemeState(newTheme);
26
+ }
27
+ },
28
+ [forcedTheme],
29
+ );
30
+
31
+ const value: ThemeContextType = {
32
+ theme: forcedTheme || theme,
33
+ setTheme,
34
+ };
35
+
36
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
37
+ }
38
+
39
+ export function useTheme(): ThemeContextType {
40
+ const context = useContext(ThemeContext);
41
+
42
+ if (context === undefined) {
43
+ throw new Error("useTheme must be used within a ThemeContextProvider");
44
+ }
45
+
46
+ return context;
47
+ }
@@ -1,19 +1,45 @@
1
1
  import { HeroUIProvider } from "@heroui/react";
2
- import { ThemeProvider } from "next-themes";
3
- import type { ReactElement, ReactNode } from "react";
2
+ import { useId, useMemo, type ReactElement, type ReactNode } from "react";
3
+ import { baseConfig } from "./tailwind/plugin";
4
+ import { themeConfigToCSS } from "./tailwind/theme-to-css";
5
+ import { ThemeContextProvider, useTheme } from "./theme-context";
6
+ import { ThemeConfig } from "./types";
4
7
 
5
8
  type Props = {
6
9
  children: ReactNode;
10
+ themeConfig?: ThemeConfig;
7
11
  defaultTheme?: string;
8
12
  forcedTheme?: string;
9
13
  };
10
14
 
11
- export const TomoUIProvider = ({ children, defaultTheme = "dark", forcedTheme = "dark" }: Props): ReactElement => {
15
+ // Internal component that applies theme to the scoped div
16
+ function ThemedContent({ children, uniqueId }: { children: ReactNode; uniqueId: string }) {
17
+ const { theme } = useTheme();
18
+
19
+ return (
20
+ <div id={uniqueId} className={theme} data-theme>
21
+ {children}
22
+ </div>
23
+ );
24
+ }
25
+
26
+ export const TomoUIProvider = ({ children, themeConfig, defaultTheme, forcedTheme }: Props): ReactElement => {
27
+ const uniqueId = useId().replace(/:/g, "-");
28
+ const styleId = `${baseConfig.prefix}-theme-override-${uniqueId}`;
29
+ const themeCSS = useMemo(
30
+ () => (themeConfig ? themeConfigToCSS(themeConfig, uniqueId) : null),
31
+ [themeConfig, uniqueId],
32
+ );
33
+
12
34
  return (
13
35
  <HeroUIProvider>
14
- <ThemeProvider defaultTheme={defaultTheme} attribute="class" forcedTheme={forcedTheme}>
15
- {children}
16
- </ThemeProvider>
36
+ <ThemeContextProvider
37
+ defaultTheme={defaultTheme || themeConfig?.defaultTheme || "light"}
38
+ forcedTheme={forcedTheme}
39
+ >
40
+ {themeCSS ? <style id={styleId}>{themeCSS}</style> : null}
41
+ <ThemedContent uniqueId={uniqueId}>{children}</ThemedContent>
42
+ </ThemeContextProvider>
17
43
  </HeroUIProvider>
18
44
  );
19
45
  };
package/src/types.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { ConfigThemes, DefaultThemeType, LayoutTheme } from "@heroui/theme";
2
+
3
+ export type ThemeConfig = {
4
+ layout?: LayoutTheme;
5
+ themes?: ConfigThemes;
6
+ defaultTheme?: DefaultThemeType;
7
+ };
@@ -1,10 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- export interface ModalBodyProps {
4
- children: ReactNode;
5
- className?: string;
6
- }
7
-
8
- export function ModalBody({ children, className = "" }: ModalBodyProps) {
9
- return <div className={`p-4 overflow-y-auto flex-1 ${className}`}>{children}</div>;
10
- }
@@ -1,17 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { useModalContext } from "./modal-context";
3
-
4
- export interface ModalContentProps {
5
- children: ReactNode | ((onClose: () => void) => ReactNode);
6
- className?: string;
7
- onClose?: () => void;
8
- }
9
-
10
- export function ModalContent({ children, className = "", onClose }: ModalContentProps) {
11
- const context = useModalContext();
12
- const handleClose = onClose || context.onClose || (() => {});
13
-
14
- const content = typeof children === "function" ? children(handleClose) : children;
15
-
16
- return <div className={`flex flex-col ${className}`}>{content}</div>;
17
- }
@@ -1,12 +0,0 @@
1
- import { createContext, useContext } from "react";
2
-
3
- export interface ModalContextValue {
4
- onClose?: () => void;
5
- hideCloseButton?: boolean;
6
- }
7
-
8
- export const ModalContext = createContext<ModalContextValue>({});
9
-
10
- export function useModalContext() {
11
- return useContext(ModalContext);
12
- }
@@ -1,12 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- export interface ModalFooterProps {
4
- children: ReactNode;
5
- className?: string;
6
- }
7
-
8
- export function ModalFooter({ children, className = "" }: ModalFooterProps) {
9
- return (
10
- <div className={`flex items-center justify-end gap-2 p-4 border-t border-default-200 ${className}`}>{children}</div>
11
- );
12
- }
@@ -1,28 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { CloseIcon } from "../../icons";
3
- import { Button } from "../button";
4
- import { useModalContext } from "./modal-context";
5
-
6
- export interface ModalHeaderProps {
7
- children: ReactNode;
8
- className?: string;
9
- onClose?: () => void;
10
- hideCloseButton?: boolean;
11
- }
12
-
13
- export function ModalHeader({ children, className = "", onClose, hideCloseButton }: ModalHeaderProps) {
14
- const context = useModalContext();
15
- const handleClose = onClose || context.onClose;
16
- const shouldHideCloseButton = hideCloseButton ?? context.hideCloseButton ?? false;
17
-
18
- return (
19
- <div className={`flex items-center justify-between p-4 border-b border-default-200 ${className}`}>
20
- <div className="flex-1">{children}</div>
21
- {!shouldHideCloseButton && handleClose && (
22
- <Button isIconOnly size="sm" variant="light" onPress={handleClose} className="ml-2" aria-label="Close">
23
- <CloseIcon />
24
- </Button>
25
- )}
26
- </div>
27
- );
28
- }
@@ -1,152 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { cloneElement, isValidElement, useEffect, useRef } from "react";
3
- import { createPortal } from "react-dom";
4
- import { ModalBody } from "./modal-body";
5
- import { ModalContent } from "./modal-content";
6
- import { ModalContext } from "./modal-context";
7
- import { ModalFooter } from "./modal-footer";
8
- import { ModalHeader } from "./modal-header";
9
-
10
- export type ModalSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "full";
11
-
12
- export type ModalRadius = "none" | "sm" | "md" | "lg" | "full";
13
-
14
- export type ScrollBehavior = "inside" | "outside";
15
-
16
- export interface ModalProps {
17
- isOpen?: boolean;
18
- onOpenChange?: (isOpen: boolean) => void;
19
- children: ReactNode;
20
- size?: ModalSize;
21
- radius?: ModalRadius;
22
- isDismissable?: boolean;
23
- isKeyboardDismissDisabled?: boolean;
24
- hideCloseButton?: boolean;
25
- scrollBehavior?: ScrollBehavior;
26
- className?: string;
27
- classNames?: {
28
- base?: string;
29
- backdrop?: string;
30
- wrapper?: string;
31
- };
32
- }
33
-
34
- const sizeClasses: Record<ModalSize, string> = {
35
- xs: "max-w-xs",
36
- sm: "max-w-sm",
37
- md: "max-w-md",
38
- lg: "max-w-lg",
39
- xl: "max-w-xl",
40
- "2xl": "max-w-2xl",
41
- "3xl": "max-w-3xl",
42
- "4xl": "max-w-4xl",
43
- "5xl": "max-w-5xl",
44
- full: "max-w-full",
45
- };
46
-
47
- const radiusClasses: Record<ModalRadius, string> = {
48
- none: "rounded-none",
49
- sm: "rounded-sm",
50
- md: "rounded-md",
51
- lg: "rounded-lg",
52
- full: "rounded-full",
53
- };
54
-
55
- export function Modal({
56
- isOpen = false,
57
- onOpenChange,
58
- children,
59
- size = "md",
60
- radius = "md",
61
- isDismissable = true,
62
- isKeyboardDismissDisabled = false,
63
- hideCloseButton = false,
64
- scrollBehavior = "outside",
65
- className = "",
66
- classNames = {},
67
- }: ModalProps) {
68
- const modalRef = useRef<HTMLDivElement>(null);
69
-
70
- useEffect(() => {
71
- if (!isOpen) return;
72
-
73
- const handleEscape = (e: KeyboardEvent) => {
74
- if (e.key === "Escape" && !isKeyboardDismissDisabled && isDismissable) {
75
- onOpenChange?.(false);
76
- }
77
- };
78
-
79
- document.addEventListener("keydown", handleEscape);
80
- document.body.style.overflow = "hidden";
81
-
82
- return () => {
83
- document.removeEventListener("keydown", handleEscape);
84
- document.body.style.overflow = "";
85
- };
86
- }, [isOpen, isKeyboardDismissDisabled, isDismissable, onOpenChange]);
87
-
88
- const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
89
- if (isDismissable && e.target === e.currentTarget) {
90
- onOpenChange?.(false);
91
- }
92
- };
93
-
94
- if (!isOpen) return null;
95
-
96
- const sizeClass = sizeClasses[size];
97
- const radiusClass = radiusClasses[radius];
98
-
99
- const handleClose = () => {
100
- onOpenChange?.(false);
101
- };
102
-
103
- const contextValue = {
104
- onClose: handleClose,
105
- hideCloseButton,
106
- };
107
-
108
- // Clone children to pass onClose prop
109
- const childrenWithProps = isValidElement(children)
110
- ? cloneElement(children, { onClose: handleClose } as any)
111
- : children;
112
-
113
- const modalContent = (
114
- <ModalContext.Provider value={contextValue}>
115
- <div
116
- className={`fixed inset-0 z-50 flex items-center justify-center ${classNames.wrapper || ""}`}
117
- onClick={handleBackdropClick}
118
- >
119
- {/* Backdrop */}
120
- <div className={`fixed inset-0 bg-black/50 backdrop-blur-sm ${classNames.backdrop || ""}`} aria-hidden="true" />
121
-
122
- {/* Modal Content */}
123
- <div
124
- ref={modalRef}
125
- className={`
126
- relative z-10 bg-background shadow-lg
127
- ${sizeClass}
128
- ${radiusClass}
129
- ${scrollBehavior === "inside" ? "max-h-[90vh] overflow-hidden flex flex-col" : ""}
130
- ${className}
131
- ${classNames.base || ""}
132
- `}
133
- onClick={(e) => e.stopPropagation()}
134
- role="dialog"
135
- aria-modal="true"
136
- >
137
- {childrenWithProps}
138
- </div>
139
- </div>
140
- </ModalContext.Provider>
141
- );
142
-
143
- return createPortal(modalContent, document.body);
144
- }
145
-
146
- // Export sub-components
147
- Modal.Content = ModalContent;
148
- Modal.Header = ModalHeader;
149
- Modal.Body = ModalBody;
150
- Modal.Footer = ModalFooter;
151
-
152
- export { ModalBody, ModalContent, ModalFooter, ModalHeader };
@@ -1,46 +0,0 @@
1
- import { useCallback, useState } from "react";
2
-
3
- export interface UseDisclosureProps {
4
- defaultOpen?: boolean;
5
- isOpen?: boolean;
6
- onOpenChange?: (isOpen: boolean) => void;
7
- }
8
-
9
- export function useDisclosure(props: UseDisclosureProps = {}) {
10
- const { defaultOpen = false, isOpen: controlledIsOpen, onOpenChange } = props;
11
-
12
- const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState(defaultOpen);
13
-
14
- const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : uncontrolledIsOpen;
15
-
16
- const onOpen = useCallback(() => {
17
- if (controlledIsOpen === undefined) {
18
- setUncontrolledIsOpen(true);
19
- }
20
- onOpenChange?.(true);
21
- }, [controlledIsOpen, onOpenChange]);
22
-
23
- const onClose = useCallback(() => {
24
- if (controlledIsOpen === undefined) {
25
- setUncontrolledIsOpen(false);
26
- }
27
- onOpenChange?.(false);
28
- }, [controlledIsOpen, onOpenChange]);
29
-
30
- const onOpenChangeHandler = useCallback(
31
- (newIsOpen: boolean) => {
32
- if (controlledIsOpen === undefined) {
33
- setUncontrolledIsOpen(newIsOpen);
34
- }
35
- onOpenChange?.(newIsOpen);
36
- },
37
- [controlledIsOpen, onOpenChange],
38
- );
39
-
40
- return {
41
- isOpen,
42
- onOpen,
43
- onClose,
44
- onOpenChange: onOpenChangeHandler,
45
- };
46
- }