diff --git a/apps/demo_web/src/app/layout.tsx b/apps/demo_web/src/app/layout.tsx
index cf9e4e9f1..428e1d973 100644
--- a/apps/demo_web/src/app/layout.tsx
+++ b/apps/demo_web/src/app/layout.tsx
@@ -6,6 +6,8 @@ import "@oko-wallet/oko-common-ui/styles/colors.scss";
import "@oko-wallet/oko-common-ui/styles/typography.scss";
import "@oko-wallet/oko-common-ui/styles/shadow.scss";
+import { themeInitScript } from "@oko-wallet-demo-web/state/theme";
+
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
@@ -47,10 +49,14 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+ // To prevent flickering in dark mode or light mode in Next.js, an inline script is required.
+ // The reason is that even when using LayoutEffect in React, it executes after hydration.
+
+
+
+
{children}
diff --git a/apps/demo_web/src/components/global_header/global_header.tsx b/apps/demo_web/src/components/global_header/global_header.tsx
index 02e92e1fa..72047d5f8 100644
--- a/apps/demo_web/src/components/global_header/global_header.tsx
+++ b/apps/demo_web/src/components/global_header/global_header.tsx
@@ -7,10 +7,12 @@ import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close";
import styles from "./global_header.module.scss";
import { useViewState } from "@oko-wallet-demo-web/state/view";
+import { useThemeState } from "@oko-wallet-demo-web/state/theme";
export const GlobalHeader: FC = () => {
const isLeftBarOpen = useViewState((state) => state.isLeftBarOpen);
const toggleLeftBarOpen = useViewState((state) => state.toggleLeftBarOpen);
+ const theme = useThemeState((state) => state.theme);
return (
@@ -21,8 +23,7 @@ export const GlobalHeader: FC = () => {
)}
- {/* NOTE: theme is hardcoded to light for now */}
-
+
);
};
diff --git a/apps/demo_web/src/components/left_bar/left_bar.tsx b/apps/demo_web/src/components/left_bar/left_bar.tsx
index 02c4dd70b..b4013d069 100644
--- a/apps/demo_web/src/components/left_bar/left_bar.tsx
+++ b/apps/demo_web/src/components/left_bar/left_bar.tsx
@@ -4,10 +4,12 @@ import { type FC } from "react";
import cn from "classnames";
import { MenuItem } from "@oko-wallet/oko-common-ui/menu";
import { HomeOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/home_outlined";
+import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
import styles from "./left_bar.module.scss";
import { IntegrationCard } from "./integration_card/integration_card";
import { useViewState } from "@oko-wallet-demo-web/state/view";
+import { ThemeButton } from "@oko-wallet-demo-web/components/theme/theme_button";
export const LeftBar: FC = () => {
const isLeftBarOpen = useViewState((state) => state.isLeftBarOpen);
@@ -34,9 +36,16 @@ export const LeftBar: FC = () => {
}
active={true}
/>
- {showIntegrationCard && (
-
- )}
+
+ {showIntegrationCard && (
+ <>
+
+
+ >
+ )}
+
+
+
>
);
diff --git a/apps/demo_web/src/components/oko_provider/oko_provider.tsx b/apps/demo_web/src/components/oko_provider/oko_provider.tsx
index 3bb8305a3..fdd2a4a0c 100644
--- a/apps/demo_web/src/components/oko_provider/oko_provider.tsx
+++ b/apps/demo_web/src/components/oko_provider/oko_provider.tsx
@@ -2,10 +2,12 @@
import { type FC, type PropsWithChildren } from "react";
-import { useInitOko } from "./use_oko";
+import { useInitOko } from "@oko-wallet-demo-web/components/oko_provider/use_oko";
+import { useThemeSync } from "@oko-wallet-demo-web/hooks/use_theme_sync";
export const OkoProvider: FC = ({ children }) => {
useInitOko();
+ useThemeSync();
return <>{children}>;
};
diff --git a/apps/demo_web/src/components/preview_panel/preview_panel.module.scss b/apps/demo_web/src/components/preview_panel/preview_panel.module.scss
index cae99502e..b865e7e24 100644
--- a/apps/demo_web/src/components/preview_panel/preview_panel.module.scss
+++ b/apps/demo_web/src/components/preview_panel/preview_panel.module.scss
@@ -23,6 +23,10 @@
/* background dot pattern image*/
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Crect x='4' y='4' width='2' height='2' rx='1' fill='%23E9EAEB'/%3E%3C/svg%3E");
+ [data-theme="dark"] & {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Crect x='4' y='4' width='2' height='2' rx='1' fill='%2322262F'/%3E%3C/svg%3E");
+ }
+
background-size: 16px 16px;
background-position: 7px -4px;
}
diff --git a/apps/demo_web/src/components/preview_panel/preview_panel.tsx b/apps/demo_web/src/components/preview_panel/preview_panel.tsx
index 4d7994917..ba4207e74 100644
--- a/apps/demo_web/src/components/preview_panel/preview_panel.tsx
+++ b/apps/demo_web/src/components/preview_panel/preview_panel.tsx
@@ -20,10 +20,10 @@ import { useSDKState } from "@oko-wallet-demo-web/state/sdk";
export const PreviewPanel: FC = () => {
const isLazyInitialized = useSDKState(
- (st) =>
- st.isCosmosLazyInitialized &&
- st.isEthLazyInitialized &&
- st.isSolLazyInitialized,
+ (st) => st.isCosmosLazyInitialized && st.isEthLazyInitialized,
+ // TODO: refactor this @chemonoworld @Ryz0nd
+ // &&
+ // st.isSolLazyInitialized,
);
const isSignedIn = useUserInfoState((state) => state.isSignedIn);
diff --git a/apps/demo_web/src/components/theme/theme_button.module.scss b/apps/demo_web/src/components/theme/theme_button.module.scss
new file mode 100644
index 000000000..a7c860eca
--- /dev/null
+++ b/apps/demo_web/src/components/theme/theme_button.module.scss
@@ -0,0 +1,22 @@
+.themeButtonContainer {
+ width: fit-content;
+
+ .themeButton {
+ transition: color 0.15s ease;
+ color: var(--fg-quaternary);
+ &:hover {
+ color: var(--fg-quaternary-hover);
+ }
+
+ padding: 10px;
+
+ border-radius: 8px;
+ border: 1px solid var(--border-primary);
+ background: var(--bg-primary);
+
+ box-shadow:
+ 0 0 0 1px rgba(10, 13, 18, 0.18) inset,
+ 0 -2px 0 0 rgba(10, 13, 18, 0.05) inset,
+ 0 1px 2px 0 var(--shadow-xs, rgba(10, 13, 18, 0.05));
+ }
+}
diff --git a/apps/demo_web/src/components/theme/theme_button.tsx b/apps/demo_web/src/components/theme/theme_button.tsx
new file mode 100644
index 000000000..c9f716d67
--- /dev/null
+++ b/apps/demo_web/src/components/theme/theme_button.tsx
@@ -0,0 +1,40 @@
+import { Button } from "@oko-wallet/oko-common-ui/button";
+import { SunIcon } from "@oko-wallet/oko-common-ui/icons/sun_icon";
+import { MoonIcon } from "@oko-wallet/oko-common-ui/icons/moon_icon";
+import { Tooltip } from "@oko-wallet/oko-common-ui/tooltip";
+
+import { useThemeState } from "@oko-wallet-demo-web/state/theme";
+import styles from "./theme_button.module.scss";
+
+export const ThemeButton = () => {
+ const theme = useThemeState((state) => state.theme);
+ const setPreference = useThemeState((state) => state.setPreference);
+
+ const handleThemeToggle = () => {
+ setPreference(theme === "light" ? "dark" : "light");
+ };
+
+ return (
+
+
+ {theme === "light" ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/apps/demo_web/src/components/widgets/account_widget/auth_progress_widget.module.scss b/apps/demo_web/src/components/widgets/account_widget/auth_progress_widget.module.scss
index 9d4365b97..73b1f4ceb 100644
--- a/apps/demo_web/src/components/widgets/account_widget/auth_progress_widget.module.scss
+++ b/apps/demo_web/src/components/widgets/account_widget/auth_progress_widget.module.scss
@@ -15,7 +15,7 @@
justify-content: center;
width: 54px;
height: 54px;
- background: #fff;
+ background: var(--bg-background);
position: relative;
}
diff --git a/apps/demo_web/src/components/widgets/account_widget/spinner.tsx b/apps/demo_web/src/components/widgets/account_widget/spinner.tsx
index 2121b8d23..ac828d450 100644
--- a/apps/demo_web/src/components/widgets/account_widget/spinner.tsx
+++ b/apps/demo_web/src/components/widgets/account_widget/spinner.tsx
@@ -1,6 +1,7 @@
import type { FC } from "react";
import styles from "./spinner.module.scss";
+import { useThemeState } from "@oko-wallet-demo-web/state/theme";
interface SpinnerProps {
size?: number;
@@ -13,6 +14,7 @@ export const Spinner: FC = ({
className,
status = "loading",
}) => {
+ const theme = useThemeState((state) => state.theme);
const isFailed = status === "failed";
return (
@@ -27,7 +29,7 @@ export const Spinner: FC = ({
{isFailed ? (
= ({
<>
= ({}) => {
/>
-
+
diff --git a/apps/demo_web/src/components/widgets/address_widget/view_chains_button.tsx b/apps/demo_web/src/components/widgets/address_widget/view_chains_button.tsx
index c4f424285..d4130e970 100644
--- a/apps/demo_web/src/components/widgets/address_widget/view_chains_button.tsx
+++ b/apps/demo_web/src/components/widgets/address_widget/view_chains_button.tsx
@@ -6,12 +6,9 @@ import { Button } from "@oko-wallet/oko-common-ui/button";
import styles from "./view_chains_button.module.scss";
-export const ViewChainsButton: FC = ({
- buttonVariant,
- onClick,
-}) => {
+export const ViewChainsButton: FC = ({ onClick }) => {
return (
-
+
@@ -23,6 +20,5 @@ export const ViewChainsButton: FC
= ({
};
export interface ViewChainsButtonProps {
- buttonVariant: "primary" | "secondary";
onClick?: () => void;
}
diff --git a/apps/demo_web/src/components/widgets/docs_widget/docs_widget.tsx b/apps/demo_web/src/components/widgets/docs_widget/docs_widget.tsx
index f82494e90..f880e745f 100644
--- a/apps/demo_web/src/components/widgets/docs_widget/docs_widget.tsx
+++ b/apps/demo_web/src/components/widgets/docs_widget/docs_widget.tsx
@@ -39,7 +39,7 @@ export const DocsWidget: FC = () => {
Explore the SDK, APIs, and integration guides to start building.
void;
@@ -24,10 +25,12 @@ export const LoginDefaultView: FC = ({
onSignIn,
onShowSocials,
}) => {
+ const theme = useThemeState((state) => state.theme);
+
return (
-
+
@@ -73,7 +76,7 @@ export const LoginDefaultView: FC
= ({
-
+
{
+ const { preference, initialize, setTheme } = useThemeState();
+
+ useLayoutEffect(() => {
+ initialize();
+ }, [initialize]);
+
+ useEffect(() => {
+ if (preference !== "system") return;
+
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ const handleChange = (e: MediaQueryListEvent) => {
+ setTheme(e.matches ? "dark" : "light");
+ };
+
+ mediaQuery.addEventListener("change", handleChange);
+ return () => mediaQuery.removeEventListener("change", handleChange);
+ }, [preference, setTheme]);
+};
diff --git a/apps/demo_web/src/state/theme.ts b/apps/demo_web/src/state/theme.ts
new file mode 100644
index 000000000..82fa12887
--- /dev/null
+++ b/apps/demo_web/src/state/theme.ts
@@ -0,0 +1,86 @@
+import { create } from "zustand";
+import { combine, persist } from "zustand/middleware";
+
+export type Theme = "light" | "dark";
+export type ThemePreference = Theme | "system";
+
+export const THEME_STORAGE_KEY = "theme";
+export const THEME_ATTRIBUTE = "data-theme";
+
+export const themeInitScript = `
+(function() {
+ try {
+ var stored = localStorage.getItem('${THEME_STORAGE_KEY}');
+ var preference = 'system';
+ if (stored) {
+ var parsed = JSON.parse(stored);
+ preference = parsed.state?.preference || 'system';
+ }
+ var theme = preference;
+ if (preference === 'system') {
+ theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ }
+ document.documentElement.setAttribute('${THEME_ATTRIBUTE}', theme);
+ } catch (e) {
+ document.documentElement.setAttribute('${THEME_ATTRIBUTE}', 'light');
+ }
+})();
+`;
+
+interface ThemeState {
+ preference: ThemePreference;
+ theme: Theme;
+}
+
+interface ThemeAction {
+ initialize: () => void;
+ setPreference: (preference: ThemePreference) => void;
+ setTheme: (theme: Theme) => void;
+}
+
+const getSystemTheme = (): Theme => {
+ if (typeof window === "undefined") return "light";
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+};
+
+const applyTheme = (theme: Theme) => {
+ if (typeof document !== "undefined") {
+ document.documentElement.setAttribute(THEME_ATTRIBUTE, theme);
+ }
+};
+
+const initialState: ThemeState = {
+ preference: "system",
+ theme: "light",
+};
+
+export const useThemeState = create(
+ persist(
+ combine(initialState, (set, get) => ({
+ initialize: () => {
+ const { preference } = get();
+ const resolvedTheme =
+ preference === "system" ? getSystemTheme() : preference;
+ set({ theme: resolvedTheme });
+ },
+
+ setPreference: (preference: ThemePreference) => {
+ const resolvedTheme =
+ preference === "system" ? getSystemTheme() : preference;
+ set({ preference, theme: resolvedTheme });
+ applyTheme(resolvedTheme);
+ },
+
+ setTheme: (theme: Theme) => {
+ set({ theme });
+ applyTheme(theme);
+ },
+ })),
+ {
+ name: THEME_STORAGE_KEY,
+ partialize: (state) => ({ preference: state.preference }),
+ },
+ ),
+);
diff --git a/ui/oko_common_ui/src/icons/moon_icon.tsx b/ui/oko_common_ui/src/icons/moon_icon.tsx
new file mode 100644
index 000000000..cf5bee7b1
--- /dev/null
+++ b/ui/oko_common_ui/src/icons/moon_icon.tsx
@@ -0,0 +1,40 @@
+import type { BasicIconProps } from "./types";
+
+export const MoonIcon: React.FC = ({
+ className,
+ color = "currentColor",
+ size = 20,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/ui/oko_common_ui/src/icons/sun_icon.tsx b/ui/oko_common_ui/src/icons/sun_icon.tsx
new file mode 100644
index 000000000..c984d3580
--- /dev/null
+++ b/ui/oko_common_ui/src/icons/sun_icon.tsx
@@ -0,0 +1,33 @@
+import type { BasicIconProps } from "./types";
+
+export const SunIcon: React.FC = ({
+ className,
+ color = "currentColor",
+ size = 20,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/ui/oko_common_ui/src/tooltip/tooltip.module.scss b/ui/oko_common_ui/src/tooltip/tooltip.module.scss
index a0bf80d2e..428d70676 100644
--- a/ui/oko_common_ui/src/tooltip/tooltip.module.scss
+++ b/ui/oko_common_ui/src/tooltip/tooltip.module.scss
@@ -7,7 +7,8 @@
primary-solid,
secondary-solid,
tertiary-solid,
- quaternary-solid
+ quaternary-solid,
+ brand-solid
)
{
.bg-#{$background-color} {
@@ -24,7 +25,8 @@
primary-solid,
secondary-solid,
tertiary-solid,
- quaternary-solid
+ quaternary-solid,
+ brand-solid
)
{
.arrow-color-#{$background-color} {
diff --git a/ui/oko_common_ui/src/tooltip/tooltip.tsx b/ui/oko_common_ui/src/tooltip/tooltip.tsx
index 2b18bcd2c..9ead59912 100644
--- a/ui/oko_common_ui/src/tooltip/tooltip.tsx
+++ b/ui/oko_common_ui/src/tooltip/tooltip.tsx
@@ -10,7 +10,11 @@ import { FloatingArrow, arrow } from "@floating-ui/react";
import cn from "classnames";
import styles from "./tooltip.module.scss";
-import { Typography } from "@oko-wallet-common-ui/typography/typography";
+import {
+ Typography,
+ type BaseTypographyColor,
+ type BaseTypographyCustomColor,
+} from "@oko-wallet-common-ui/typography/typography";
export type TooltipProps = {
children: React.ReactNode;
@@ -27,7 +31,10 @@ export type TooltipProps = {
| "primary-solid"
| "secondary-solid"
| "tertiary-solid"
- | "quaternary-solid";
+ | "quaternary-solid"
+ | "brand-solid";
+ titleColor?: BaseTypographyColor;
+ titleCustomColor?: BaseTypographyCustomColor;
};
export const Tooltip: FC = ({
@@ -37,8 +44,11 @@ export const Tooltip: FC = ({
title,
content,
hideFloatingArrow,
- backgroundColor = "primary-solid",
+ backgroundColor: bgColor,
+ titleColor,
+ titleCustomColor,
}) => {
+ const backgroundColor = bgColor ?? "primary-solid";
const [isOpen, setIsOpen] = useState(false);
const arrowRef = useRef(null);
@@ -91,7 +101,12 @@ export const Tooltip: FC = ({
{...getFloatingProps()}
>
{title && (
-
+
{title}
)}
diff --git a/ui/oko_common_ui/src/typography/typography.tsx b/ui/oko_common_ui/src/typography/typography.tsx
index fee4fd371..2ee023598 100644
--- a/ui/oko_common_ui/src/typography/typography.tsx
+++ b/ui/oko_common_ui/src/typography/typography.tsx
@@ -3,6 +3,92 @@ import cn from "classnames";
import styles from "./typography.module.scss";
+export type BaseTypographyColor =
+ | "primary"
+ | "secondary"
+ | "tertiary"
+ | "quaternary"
+ | "primary-on-brand"
+ | "secondary-on-brand"
+ | "tertiary-on-brand"
+ | "quaternary-on-brand"
+ | "secondary-hover"
+ | "tertiary-hover"
+ | "white"
+ | "disabled"
+ | "placeholder"
+ | "placeholder-subtle"
+ | "subtle"
+ | "brand-primary"
+ | "brand-secondary"
+ | "brand-secondary-hover"
+ | "brand-tertiary"
+ | "error-primary"
+ | "error-primary-hover"
+ | "warning-primary"
+ | "success-primary";
+
+export type BaseTypographyCustomColor =
+ | "white"
+ | "black"
+ | "transparent"
+ | "gray-50"
+ | "gray-100"
+ | "gray-200"
+ | "gray-300"
+ | "gray-400"
+ | "gray-500"
+ | "gray-600"
+ | "gray-700"
+ | "gray-800"
+ | "gray-900"
+ | "brand-50"
+ | "brand-100"
+ | "brand-200"
+ | "brand-300"
+ | "brand-400"
+ | "brand-500"
+ | "brand-600"
+ | "brand-700"
+ | "brand-800"
+ | "brand-900"
+ | "error-25"
+ | "error-50"
+ | "error-100"
+ | "error-200"
+ | "error-300"
+ | "error-400"
+ | "error-500"
+ | "error-600"
+ | "error-700"
+ | "error-800"
+ | "error-900"
+ | "error-950"
+ | "warning-25"
+ | "warning-50"
+ | "warning-100"
+ | "warning-200"
+ | "warning-300"
+ | "warning-400"
+ | "warning-500"
+ | "warning-600"
+ | "warning-700"
+ | "warning-800"
+ | "warning-900"
+ | "warning-950"
+ | "success-25"
+ | "success-50"
+ | "success-100"
+ | "success-200"
+ | "success-300"
+ | "success-400"
+ | "success-500"
+ | "success-600"
+ | "success-700"
+ | "success-800"
+ | "success-900"
+ | "success-950";
+
export type BaseTypographyProps = {
children: string | React.ReactNode;
size?:
@@ -19,91 +105,9 @@ export type BaseTypographyProps = {
| "display-2xl";
weight?: "regular" | "medium" | "semibold" | "bold";
- color?:
- | "primary"
- | "secondary"
- | "tertiary"
- | "quaternary"
- | "primary-on-brand"
- | "secondary-on-brand"
- | "tertiary-on-brand"
- | "quaternary-on-brand"
- | "secondary-hover"
- | "tertiary-hover"
- | "white"
- | "disabled"
- | "placeholder"
- | "placeholder-subtle"
- | "subtle"
- | "brand-primary"
- | "brand-secondary"
- | "brand-secondary-hover"
- | "brand-tertiary"
- | "error-primary"
- | "error-primary-hover"
- | "warning-primary"
- | "success-primary";
+ color?: BaseTypographyColor;
- customColor?:
- | "white"
- | "black"
- | "transparent"
- | "gray-50"
- | "gray-100"
- | "gray-200"
- | "gray-300"
- | "gray-400"
- | "gray-500"
- | "gray-600"
- | "gray-700"
- | "gray-800"
- | "gray-900"
- | "brand-50"
- | "brand-100"
- | "brand-200"
- | "brand-300"
- | "brand-400"
- | "brand-500"
- | "brand-600"
- | "brand-700"
- | "brand-800"
- | "brand-900"
- | "error-25"
- | "error-50"
- | "error-100"
- | "error-200"
- | "error-300"
- | "error-400"
- | "error-500"
- | "error-600"
- | "error-700"
- | "error-800"
- | "error-900"
- | "error-950"
- | "warning-25"
- | "warning-50"
- | "warning-100"
- | "warning-200"
- | "warning-300"
- | "warning-400"
- | "warning-500"
- | "warning-600"
- | "warning-700"
- | "warning-800"
- | "warning-900"
- | "warning-950"
- | "success-25"
- | "success-50"
- | "success-100"
- | "success-200"
- | "success-300"
- | "success-400"
- | "success-500"
- | "success-600"
- | "success-700"
- | "success-800"
- | "success-900"
- | "success-950";
+ customColor?: BaseTypographyCustomColor;
className?: string;
style?: React.CSSProperties;