Skip to content

Commit

Permalink
rd-375 customize theme via typed provider
Browse files Browse the repository at this point in the history
  • Loading branch information
vordgi committed Nov 5, 2024
1 parent f765020 commit ba0bf83
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 57 deletions.
38 changes: 38 additions & 0 deletions packages/robindoc/src/components/blocks/theme-detector/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react";

const clientLogic = () => {
const userTheme = localStorage.getItem("theme");
if (userTheme && ["light", "dark"].includes(userTheme)) {
document.documentElement.classList.add(`theme-${userTheme}`);
} else {
document.documentElement.classList.add(`theme-system`);
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add("theme-dark");
} else {
document.documentElement.classList.add("theme-light");
}
}

const store = localStorage.getItem("r-tabs");
const items = store?.split(";").filter((item) => item && /[\w-]+=[\w]+/.test(item)) || [];
const classNames = Array.from(document.documentElement.classList);
classNames.forEach((className) => {
if (className.startsWith(`r-tabs-global`)) {
document.documentElement.classList.remove(className);
}
});
items.forEach((item) => {
const [tabsKey, tab] = item.split("=");
document.documentElement.classList.add(`r-tabs-global__${tabsKey}`, `r-tabs-global__${tabsKey}_${tab}`);
});
};

export const ThemeDetector: React.FC = () => (
<script
id="detect-theme"
dangerouslySetInnerHTML={{
__html: `(${clientLogic})()`,
}}
async
/>
);
26 changes: 26 additions & 0 deletions packages/robindoc/src/components/blocks/theme-styles/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";

import { type Theme } from "@src/core/types/theme";

export interface ThemeStylesProps {
theme: Theme;
}

export const ThemeStyles: React.FC<ThemeStylesProps> = ({ theme }) => {
const { colors = {} } = theme;

const colorsStyles = Object.entries(colors).reduce<string[]>((acc, [type, variants]) => {
Object.entries(variants).forEach(([key, color]) => {
acc.push(`--${type}-${key}:${color};`);
});
return acc;
}, []);

return (
<style
dangerouslySetInnerHTML={{
__html: `:root{${colorsStyles.join("")}}`,
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import "./theme.scss";
import React from "react";

export type ThemeProps = {
import "./theme-switcher.scss";

export type ThemeSwitcherProps = {
translations?: {
/** Dark */
dark?: string;
Expand All @@ -14,7 +15,7 @@ export type ThemeProps = {
};
};

export const Theme: React.FC<ThemeProps> = ({ translations }) => {
export const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ translations }) => {
const { dark = "Dark", system = "System", light = "light" } = translations || {};
const changeTheme = (theme: string) => {
localStorage.setItem("theme", theme);
Expand All @@ -32,8 +33,12 @@ export const Theme: React.FC<ThemeProps> = ({ translations }) => {
};

return (
<div className="r-theme">
<button className="r-theme-btn r-theme-btn__dark" type="button" onClick={() => changeTheme("dark")}>
<div className="r-theme-switcher">
<button
className="r-theme-switcher-btn r-theme-switcher-btn__dark"
type="button"
onClick={() => changeTheme("dark")}
>
<svg width="16" height="16" viewBox="0 0 20 20">
<title>{dark}</title>
<path
Expand All @@ -44,7 +49,11 @@ export const Theme: React.FC<ThemeProps> = ({ translations }) => {
/>
</svg>
</button>
<button className="r-theme-btn r-theme-btn__system" type="button" onClick={() => changeTheme("system")}>
<button
className="r-theme-switcher-btn r-theme-switcher-btn__system"
type="button"
onClick={() => changeTheme("system")}
>
<svg width="16" height="16" viewBox="0 0 20 20">
<title>{system}</title>
<path
Expand Down Expand Up @@ -73,7 +82,11 @@ export const Theme: React.FC<ThemeProps> = ({ translations }) => {
/>
</svg>
</button>
<button className="r-theme-btn r-theme-btn__light" type="button" onClick={() => changeTheme("light")}>
<button
className="r-theme-switcher-btn r-theme-switcher-btn__light"
type="button"
onClick={() => changeTheme("light")}
>
<svg width="16" height="16" viewBox="0 0 20 20">
<title>{light}</title>
<path
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@import "src/assets/_vars";

.r-theme {
.r-theme-switcher {
position: relative;
display: flex;
gap: 4px;
Expand All @@ -24,7 +24,7 @@
}
}

.r-theme-btn {
.r-theme-switcher-btn {
padding: 8px;
background: none;
border: 0;
Expand All @@ -43,29 +43,29 @@
}
}

.theme-dark:not(.theme-system) .r-theme::before {
.theme-dark:not(.theme-system) .r-theme-switcher::before {
left: 2px;
}

.theme-dark:not(.theme-system) .r-theme-btn__dark {
.theme-dark:not(.theme-system) .r-theme-switcher-btn__dark {
color: var(--neutral-950);
pointer-events: none;
}

.theme-system .r-theme::before {
.theme-system .r-theme-switcher::before {
left: 38px;
}

.theme-system .r-theme-btn__system {
.theme-system .r-theme-switcher-btn__system {
color: var(--neutral-950);
pointer-events: none;
}

.theme-light:not(.theme-system) .r-theme::before {
.theme-light:not(.theme-system) .r-theme-switcher::before {
left: 74px;
}

.theme-light:not(.theme-system) .r-theme-btn__light {
.theme-light:not(.theme-system) .r-theme-switcher-btn__light {
color: var(--neutral-950);
pointer-events: none;
}
8 changes: 5 additions & 3 deletions packages/robindoc/src/components/elements/footer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import "./footer.scss";
import React from "react";
import { Theme } from "@src/components/blocks/theme";

import { ThemeSwitcher } from "@src/components/blocks/theme-switcher";
import { Container } from "@src/components/ui/container";

import "./footer.scss";

export type FooterProps = {
copyright: string;
hidePoweredBy?: boolean;
Expand All @@ -13,7 +15,7 @@ export const Footer: React.FC<FooterProps> = ({ copyright, hidePoweredBy }) => (
<Container>
<div className="r-footer-row">
<div className="r-copyright">{copyright}</div>
<Theme />
<ThemeSwitcher />
</div>
{!hidePoweredBy && (
<div className="r-footer-row r-footer-additional">
Expand Down
52 changes: 13 additions & 39 deletions packages/robindoc/src/components/elements/robin-provider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,18 @@
import React from "react";

import { type Theme } from "@src/core/types/theme";
import { NavigateProvider } from "@src/components/contexts/navigate/provider";
import { ThemeStyles } from "@src/components/blocks/theme-styles";
import { ThemeDetector } from "@src/components/blocks/theme-detector";

const clientLogic = () => {
const userTheme = localStorage.getItem("theme");
if (userTheme && ["light", "dark"].includes(userTheme)) {
document.documentElement.classList.add(`theme-${userTheme}`);
} else {
document.documentElement.classList.add(`theme-system`);
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add("theme-dark");
} else {
document.documentElement.classList.add("theme-light");
}
}
interface RobinProviderProps {
theme?: Theme;
}

const store = localStorage.getItem("r-tabs");
const items = store?.split(";").filter((item) => item && /[\w-]+=[\w]+/.test(item)) || [];
const classNames = Array.from(document.documentElement.classList);
classNames.forEach((className) => {
if (className.startsWith(`r-tabs-global`)) {
document.documentElement.classList.remove(className);
}
});
items.forEach((item) => {
const [tabsKey, tab] = item.split("=");
document.documentElement.classList.add(`r-tabs-global__${tabsKey}`, `r-tabs-global__${tabsKey}_${tab}`);
});
};

export const RobinProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<>
<script
id="detect-theme"
dangerouslySetInnerHTML={{
__html: `(${clientLogic})()`,
}}
/>
<NavigateProvider>{children}</NavigateProvider>
</>
);
};
export const RobinProvider: React.FC<React.PropsWithChildren<RobinProviderProps>> = ({ children, theme }) => (
<>
{theme && <ThemeStyles theme={theme} />}
<ThemeDetector />
<NavigateProvider>{children}</NavigateProvider>
</>
);
26 changes: 26 additions & 0 deletions packages/robindoc/src/core/types/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export type ThemeColors = {
accent?: {
"50": string;
"100": string;
"200": string;
"300": string;
"400": string;
"500": string;
"600": string;
"700": string;
"800": string;
"900": string;
"950": string;
};
link?: {
base: string;
"base-hovered": string;
visited: string;
"visited-hovered": string;
active: string;
};
};

export type Theme = {
colors?: ThemeColors;
};

0 comments on commit ba0bf83

Please sign in to comment.