Skip to content

Commit

Permalink
chore(docs): add initial dark mode documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Aug 3, 2024
1 parent e65fe7f commit ec16375
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 118 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"use client";
import { Box } from "@react-md/core/box/Box";
import { Card } from "@react-md/core/card/Card";
import { SegmentedButton } from "@react-md/core/segmented-button/SegmentedButton";
import { SegmentedButtonContainer } from "@react-md/core/segmented-button/SegmentedButtonContainer";
import { LocalStorageColorSchemeProvider } from "@react-md/core/theme/LocalStorageColorSchemeProvider";
import {
useColorScheme,
type ColorSchemeMode,
} from "@react-md/core/theme/useColorScheme";
import { type ColorSchemeMode } from "@react-md/core/theme/types";
import { useColorScheme } from "@react-md/core/theme/useColorScheme";
import { Typography } from "@react-md/core/typography/Typography";
import { cnb } from "cnbuilder";
import { type ReactElement } from "react";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@ simplified version of how this documentation site handles the color scheme.

```tsx fileName="src/components/CookieColorSchemeProvider.tsx"
"use client";
import {
ColorSchemeProvider,
type ColorSchemeMode,
} from "@react-md/core/theme/useColorScheme";
import { type ColorSchemeMode } from "@react-md/core/theme/types";
import { ColorSchemeProvider } from "@react-md/core/theme/useColorScheme";
import { useColorSchemeProvider } from "@react-md/core/theme/useColorSchemeProvider";
import { type UseStateSetter } from "@react-md/core/types";
import Cookies from "js-cookie";
Expand Down Expand Up @@ -91,6 +89,7 @@ export function CookieColorSchemeProvider(
```

```tsx fileName="src/app/layout.tsx"
import { isColorSchemeMode } from "@react-md/core/theme/isColorScheme";
import { type ReactElement, type ReactNode } from "react";
import { cookies } from "next/headers.js";

Expand All @@ -103,10 +102,7 @@ export default function RootLayout(props: RootLayoutProps): ReactElement {

const instance = cookies();
const value = instance.get(COLOR_SCHEME_KEY)?.value;
const defaultColorSchemeMode =
value === "light" || value === "dark" || value === "system"
? value
: "system";
const defaultColorSchemeMode = isColorSchemeMode(value) ? value : "system";

return (
<CoreProviders ssr>
Expand Down
230 changes: 230 additions & 0 deletions apps/docs/src/app/(main)/(markdown)/customization/dark-mode/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Dark Mode

There are a few different type of light/dark themes that can be available for a
web application:

- Light Only
- Dark Only
- System Preference
- Light/Dark Mode Toggle
- Light/Dark/System Toggle

> !Info! This page will guide you through all the approaches except for the
> "Light Only" theme since that is the default behavior. Check out the
> [theme customization documentation](./theme) for more theme behavior.
If the application should only use a dark theme, set the `$color-scheme`
variable to `dark` when importing the `react-md` styles which will generate all
styles with the dark theme variants.

```scss
@use "@react-md/core" with (
$color-scheme: dark
);

@include core.styles;
```

# System Mode

If the application should use the dark theme only if the user has set their
system preference to dark, set the `$color-scheme` to `system`. The generated
styles will default to the light theme but add a media query to use the dark
theme when the `@media (prefers-color-scheme: dark)` matches.

```scss
@use "@react-md/core" with (
$color-scheme: system
);

@include core.styles;
```

# Light or Dark Mode

If the application allows the user to select the current color scheme, generate
the styles as normal with the default color scheme and create a global class
name with the alternative theme using the `core.light-theme` or
`core.dark-theme` mixins.

Once the styles are generated, the app should be wrapped in the
[LocalStorageColorSchemeProvider](/components/color-scheme-provider#local-storage-example)
or a custom
[Cookie Storage Provider](/components/color-scheme-provider#cookie-storage-example)
and apply the light or dark theme class name to the root html as needed. The
following examples will use the `LocalStorageColorSchemeProvider` to keep it simple.

Start by configuring the default `$color-scheme` and creating a class name for
the other color scheme. This example will default to a `light` theme and allow
the user to configure it to be `dark`.

```scss
@use "@react-md/core";

@include core.styles;

.dark-theme {
@include core.dark-theme;
}

// if you want to default with a dark theme instead:
@use "@react-md/core" with (
$color-scheme: dark
);

@include core.styles;

.light-theme {
@include core.light-theme;
}
```

Wrap the app in the chosen `ColorSchemeProvider` implementation:

```tsx
import { LocalStorageColorSchemeProvider } from "@react-md/core/theme/LocalStorageColorSchemeProvider";

function App() {
return (
<LocalStorageColorSchemeProvider>
<RestOfTheApp />
<ApplyTheme />
</LocalStorageColorSchemeProvider>
);
}
```

Change the styles based on the color scheme:

```tsx
import { useColorScheme } from "@react-md/core/theme/useColorScheme";
import { useHtmlClassName } from "@react-md/core/useHtmlClassName";

function ApplyTheme() {
const { colorScheme, setColorSchemeMode } = useColorScheme();
useHtmlClassName(colorScheme === "dark" ? "dark-theme" : "");

// Whatever UI is desired for this
return (
<Button
onClick={() =>
setColorSchemeMode((prev) => (prev === "light" ? "dark" : "light"))
}
>
Theme
</Button>
);
}
```

# Light or Dark or System Mode

The style setup will be about the same as the previous examples. Start by
defining the default `$color-scheme` and create additional classes for the
other color schemes.

```scss
@use "@react-md/core" with (
$color-scheme: light
);

@include core.styles;

.dark-theme {
@include core.dark-theme;
}

.system-theme {
@media (prefers-color-scheme: dark) {
@include core.dark-theme;
}
}
```

Then apply the `dark-theme` or `system-theme` class name when needed:

```tsx
import { useColorScheme } from "@react-md/core/theme/useColorScheme";
import { useHtmlClassName } from "@react-md/core/useHtmlClassName";
import { cnb } from "cnbuilder";

function ApplyTheme() {
const { colorSchemeMode } = useColorScheme();
useHtmlClassName(
cnb(colorSchemeMode !== "light" && `${colorSchemeMode}-theme`)
);
return null;
}
```

## This Website's Implementation

If a real-world example is useful, here's this website's implementation
with next.js:

```import source="@/components/CookieColorSchemeProvider.tsx" fileName="src/components/CookieColorSchemeProvider.tsx"
```

```import source="@/utils/clientCookies.ts" fileName="src/utils/clientCookies.ts"
```

```tsx fileName="src/components/RootLayout.tsx"
import { CookieColorSchemeProvider } from "@/components/CookieColorSchemeProvider.jsx";
import { LoadThemeStyles } from "@/components/LoadThemeStyles.jsx";
import { COLOR_SCHEME_KEY } from "@/constants/cookies.js";
import { rmdConfig } from "@/constants/rmdConfig.jsx";
import { CoreProviders } from "@react-md/core/CoreProviders";
import { MenuConfigurationProvider } from "@react-md/core/menu/MenuConfigurationProvider";
import { RootHtml } from "@react-md/core/RootHtml";
import { NullSuspense } from "@react-md/core/suspense/NullSuspense";
import { cnb } from "cnbuilder";
import { Roboto_Flex } from "next/font/google";
import { cookies } from "next/headers.js";
import { type ReactElement, type ReactNode } from "react";
import "./layout.scss";
export { metadata } from "@/constants/metadata.js";

const roboto = Roboto_Flex({
subsets: ["latin"],
display: "swap",
variable: "--roboto",
});

export interface RootLayoutProps {
children: ReactNode;
}

export function RootLayout({ children }: RootLayoutProps): ReactElement {
const colorScheme = cookies().get(COLOR_SCHEME_KEY)?.value;
const defaultColorScheme = isColorSchemeMode(colorScheme)
? colorScheme
: "system";

return (
<RootHtml className={cnb(roboto.variable, `${defaultColorScheme}-theme`)}>
<CoreProviders {...rmdConfig}>
<MenuConfigurationProvider renderAsSheet="phone">
<CookieColorSchemeProvider defaultColorScheme={defaultColorScheme}>
<NullSuspense>
<LoadThemeStyles />
</NullSuspense>
<MainLayout>{children}</MainLayout>
</CookieColorSchemeProvider>
</MenuConfigurationProvider>
</CoreProviders>
</RootHtml>
);
}
```

```tsx fileName="src/components/LoadThemeStyles.tsx"
"use client";
import { useColorScheme } from "@react-md/core/theme/useColorScheme";

export function LoadThemeStyles(): null {
useHtmlClassName(`${colorSchemeMode}-theme`);
return null;
}
```
6 changes: 2 additions & 4 deletions apps/docs/src/components/CookieColorSchemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"use client";
import { COLOR_SCHEME_KEY } from "@/constants/cookies.js";
import { setCookie } from "@/utils/clientCookies.js";
import {
ColorSchemeProvider,
type ColorSchemeMode,
} from "@react-md/core/theme/useColorScheme";
import { type ColorSchemeMode } from "@react-md/core/theme/types";
import { ColorSchemeProvider } from "@react-md/core/theme/useColorScheme";
import { useColorSchemeProvider } from "@react-md/core/theme/useColorSchemeProvider";
import { type UseStateSetter } from "@react-md/core/types";
import {
Expand Down
6 changes: 2 additions & 4 deletions apps/docs/src/components/MainLayout/ConfigureColorScheme.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { SegmentedButtonGroup } from "@/components/SegmentedButtonGroup.jsx";
import {
useColorScheme,
type ColorSchemeMode,
} from "@react-md/core/theme/useColorScheme";
import { type ColorSchemeMode } from "@react-md/core/theme/types";
import { useColorScheme } from "@react-md/core/theme/useColorScheme";
import DarkModeOutlinedIcon from "@react-md/material-icons/DarkModeOutlinedIcon";
import DevicesOutlinedIcon from "@react-md/material-icons/DevicesOutlinedIcon";
import LightModeOutlinedIcon from "@react-md/material-icons/LightModeOutlinedIcon";
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/src/utils/serverState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { PRISM_THEMES, type PrismTheme } from "@/constants/prismThemes.js";
import { DISABLE_DEFAULT_SYSTEM_THEME } from "@/constants/rmdConfig.jsx";
import { getCookie } from "@/utils/serverCookies.js";
import { type PackageManager } from "@react-md/code/PackageManagerProvider";
import { type ColorSchemeMode } from "@react-md/core/theme/useColorScheme";
import { isColorSchemeMode } from "@react-md/core/theme/isColorScheme";
import { type ColorSchemeMode } from "@react-md/core/theme/types";
import { cookies } from "next/headers.js";
import "server-only";
import { pascalCase } from "./strings.js";
Expand All @@ -26,8 +27,7 @@ export function getAppCookies(): AppCookies {
const instance = cookies();
const defaultColorSchemeMode = getCookie({
name: COLOR_SCHEME_KEY,
isValid: (value): value is ColorSchemeMode =>
value === "light" || value === "dark" || value === "system",
isValid: isColorSchemeMode,
defaultValue: "system",
instance,
});
Expand Down
20 changes: 12 additions & 8 deletions packages/codemod/transforms/v5-to-v6/coreExportMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,8 @@ export const VARIABLES: ReadonlySet<string> = new Set([
"isAudioFile",
"isCaseInsensitiveMatch",
"isChangeableHTMLElement",
"isColorScheme",
"isColorSchemeMode",
"isContrastCompliant",
"isElementDisabled",
"isElementVisible",
Expand Down Expand Up @@ -2069,11 +2071,11 @@ export const EXPORT_MAP: Record<string, string> = {
"@react-md/core/transition/useCollapseTransition",
CollapsibleNavGroup: "@react-md/core/navigation/CollapsibleNavGroup",
CollapsibleNavGroupProps: "@react-md/core/navigation/CollapsibleNavGroup",
ColorScheme: "@react-md/core/theme/useColorScheme",
ColorSchemeContext: "@react-md/core/theme/useColorScheme",
ColorScheme: "@react-md/core/theme/types",
ColorSchemeContext: "@react-md/core/theme/types",
ColorSchemeMetaTagOptions: "@react-md/core/theme/useColorSchemeMetaTag",
ColorSchemeMode: "@react-md/core/theme/useColorScheme",
ColorSchemeModeBehavior: "@react-md/core/theme/useColorScheme",
ColorSchemeMode: "@react-md/core/theme/types",
ColorSchemeModeBehavior: "@react-md/core/theme/types",
ColorSchemeProvider: "@react-md/core/theme/useColorScheme",
ColorSchemeProviderOptions: "@react-md/core/theme/useColorSchemeProvider",
CombinedCheckboxGroupReturnValue: "@react-md/core/form/useCheckboxGroup",
Expand Down Expand Up @@ -2156,9 +2158,9 @@ export const EXPORT_MAP: Record<string, string> = {
CSSTransitionProps: "@react-md/core/transition/CSSTransition",
cssUtils: "@react-md/core/cssUtils",
CssUtilsOptions: "@react-md/core/cssUtils",
CSSVariable: "@react-md/core/theme/useCSSVariables",
CSSVariable: "@react-md/core/theme/types",
CSSVariableName: "@react-md/core/theme/types",
CSSVariablesProperties: "@react-md/core/theme/useCSSVariables",
CSSVariablesProperties: "@react-md/core/theme/types",
CurrentToastActions: "@react-md/core/snackbar/useCurrentToastActions",
CurrentToastActionsProvider: "@react-md/core/snackbar/useCurrentToastActions",
CustomAppBarComponent: "@react-md/core/app-bar/AppBar",
Expand Down Expand Up @@ -2631,6 +2633,8 @@ export const EXPORT_MAP: Record<string, string> = {
isCaseInsensitiveMatch: "@react-md/core/searching/caseInsensitive",
IsCaseInsensitiveMatchOptions: "@react-md/core/searching/caseInsensitive",
isChangeableHTMLElement: "@react-md/core/form/utils",
isColorScheme: "@react-md/core/theme/isColorScheme",
isColorSchemeMode: "@react-md/core/theme/isColorScheme",
isContrastCompliant: "@react-md/core/theme/utils",
isElementDisabled: "@react-md/core/movement/utils",
isElementVisible: "@react-md/core/utils/isElementVisible",
Expand Down Expand Up @@ -3091,7 +3095,7 @@ export const EXPORT_MAP: Record<string, string> = {
RangeSliderValue: "@react-md/core/form/useRangeSlider",
RangeStepsOptions: "@react-md/core/utils/getRangeSteps",
ReactMDCoreConfiguration: "@react-md/core/CoreProviders",
ReadonlyCSSVariableList: "@react-md/core/theme/useCSSVariables",
ReadonlyCSSVariableList: "@react-md/core/theme/types",
recalculateFocusIndex: "@react-md/core/movement/utils",
RecalculateOptions: "@react-md/core/movement/utils",
RECOMMENDED_NUMBER_STATE_KEYS: "@react-md/core/form/validation",
Expand Down Expand Up @@ -3623,7 +3627,7 @@ export const EXPORT_MAP: Record<string, string> = {
useOrientation: "@react-md/core/useOrientation",
usePageInactive: "@react-md/core/usePageInactive",
usePortalContainer: "@react-md/core/portal/PortalContainerProvider",
usePrefersDarkTheme: "@react-md/core/theme/usePrefersColorScheme",
usePrefersDarkTheme: "@react-md/core/theme/usePrefersDarkScheme",
useRadioGroup: "@react-md/core/form/useRadioGroup",
UserAgentAutocompleteProps: "@react-md/core/form/types",
useRangeSlider: "@react-md/core/form/useRangeSlider",
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/layout/useLayoutAppBarHeight.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client";
import { useMemo, type Ref, type RefCallback } from "react";
import { type DefinedCSSVariableName } from "../theme/types.js";
import { type CSSVariable } from "../theme/useCSSVariables.js";
import {
type CSSVariable,
type DefinedCSSVariableName,
} from "../theme/types.js";
import { useElementSize } from "../useElementSize.js";

declare module "react" {
Expand Down
Loading

0 comments on commit ec16375

Please sign in to comment.