@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 +5 -3
- package/src/App.tsx +134 -14
- package/src/components/index.ts +1 -0
- package/src/components/modal/index.tsx +9 -12
- package/src/components/passcodeInput/index.tsx +81 -0
- package/src/components/toast/index.ts +1 -2
- package/src/index.ts +1 -0
- package/src/style.css +1 -2
- package/src/tailwind/plugin.ts +195 -144
- package/src/tailwind/tailwind.css +2 -1
- package/src/tailwind/theme-to-css.ts +256 -0
- package/src/theme-context.tsx +47 -0
- package/src/theme-provider.tsx +32 -6
- package/src/types.ts +7 -0
- package/src/components/modal/modal-body.tsx +0 -10
- package/src/components/modal/modal-content.tsx +0 -17
- package/src/components/modal/modal-context.tsx +0 -12
- package/src/components/modal/modal-footer.tsx +0 -12
- package/src/components/modal/modal-header.tsx +0 -28
- package/src/components/modal/modal.tsx +0 -152
- package/src/components/modal/use-disclosure.ts +0 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomo-inc/tomo-ui",
|
|
3
|
-
"version": "0.0.
|
|
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/
|
|
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 {
|
|
1
|
+
import { Switch, useDisclosure } from "@heroui/react";
|
|
2
|
+
import { useTheme } from ".";
|
|
2
3
|
import { Button } from "./components/button";
|
|
3
|
-
import { 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
|
-
|
|
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
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
</
|
|
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;
|
package/src/components/index.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
export { Modal } from "
|
|
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 {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
2
|
-
export { addToast, ToastProvider } from "@heroui/toast";
|
|
1
|
+
export { addToast, ToastProvider } from "@heroui/react";
|
package/src/index.ts
CHANGED
package/src/style.css
CHANGED
package/src/tailwind/plugin.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { heroui } from "@heroui/react";
|
|
1
|
+
import { heroui, HeroUIPluginConfig } from "@heroui/react";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
+
// });
|
|
@@ -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
|
+
}
|
package/src/theme-provider.tsx
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
import { HeroUIProvider } from "@heroui/react";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
-
<
|
|
15
|
-
{
|
|
16
|
-
|
|
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
|
@@ -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
|
-
}
|