@tomo-inc/tomo-ui 0.0.8 → 0.0.10

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.
@@ -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
- }