From 8c21cbafa6921d90cb1b7f10052bbfece4d79c3c Mon Sep 17 00:00:00 2001
From: Chaojie <hi@chaojie.fun>
Date: Thu, 21 Dec 2023 18:47:26 +0800
Subject: [PATCH 1/3] Support dark mode

---
 demo/shared.py                                |  3 ++
 src/npm-fastui-bootstrap/src/DarkMode.tsx     | 48 +++++++++++++++++++
 src/npm-fastui-bootstrap/src/index.tsx        |  3 ++
 src/npm-fastui/src/components/DarkMode.tsx    | 12 +++++
 src/npm-fastui/src/components/index.tsx       |  5 ++
 .../fastui/components/__init__.py             |  6 +++
 .../tests/react-fastui-json-schema.json       | 16 +++++++
 7 files changed, 93 insertions(+)
 create mode 100644 src/npm-fastui-bootstrap/src/DarkMode.tsx
 create mode 100644 src/npm-fastui/src/components/DarkMode.tsx

diff --git a/demo/shared.py b/demo/shared.py
index 731d54ff..05b9cc57 100644
--- a/demo/shared.py
+++ b/demo/shared.py
@@ -32,6 +32,9 @@ def demo_page(*components: AnyComponent, title: str | None = None) -> list[AnyCo
                     on_click=GoToEvent(url='/forms/login'),
                     active='startswith:/forms',
                 ),
+                c.Link(
+                    components=[c.DarkMode()],
+                ),
             ],
         ),
         c.Page(
diff --git a/src/npm-fastui-bootstrap/src/DarkMode.tsx b/src/npm-fastui-bootstrap/src/DarkMode.tsx
new file mode 100644
index 00000000..86455128
--- /dev/null
+++ b/src/npm-fastui-bootstrap/src/DarkMode.tsx
@@ -0,0 +1,48 @@
+import { FC, useEffect, useState } from 'react'
+import { components, useClassName } from 'fastui'
+
+export const DarkMode: FC<components.DarkModeProps> = (props) => {
+  const [darkMode, setDarkMode] = useState(() => {
+    const localData = localStorage.getItem('fastui-dark-mode')
+    return localData ? JSON.parse(localData) : false
+  })
+
+  useEffect(() => {
+    localStorage.setItem('fastui-dark-mode', JSON.stringify(darkMode))
+    document.documentElement.setAttribute('data-bs-theme', darkMode ? 'dark' : 'light')
+  }, [darkMode])
+
+  const handleDarkMode = (darkMode: boolean) => {
+    document.documentElement.setAttribute('data-bs-theme', darkMode ? 'dark' : 'light')
+    setDarkMode(darkMode)
+  }
+
+  return (
+    <span className={useClassName(props)} onClick={() => handleDarkMode(!darkMode)}>
+      {darkMode ? (
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          width="18"
+          height="18"
+          fill="currentColor"
+          className="bi bi-moon-stars-fill"
+          viewBox="0 0 16 16"
+        >
+          <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278" />
+          <path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
+        </svg>
+      ) : (
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          width="18"
+          height="18"
+          fill="currentColor"
+          className="bi bi-brightness-high-fill"
+          viewBox="0 0 16 16"
+        >
+          <path d="M12 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
+        </svg>
+      )}
+    </span>
+  )
+}
diff --git a/src/npm-fastui-bootstrap/src/index.tsx b/src/npm-fastui-bootstrap/src/index.tsx
index 8eb7c20a..e3219ce5 100644
--- a/src/npm-fastui-bootstrap/src/index.tsx
+++ b/src/npm-fastui-bootstrap/src/index.tsx
@@ -5,6 +5,7 @@ import type { ClassNameGenerator, CustomRender, ClassName } from 'fastui'
 import { Modal } from './modal'
 import { Navbar } from './navbar'
 import { Pagination } from './pagination'
+import { DarkMode } from './DarkMode'
 
 export const customRender: CustomRender = (props) => {
   const { type } = props
@@ -15,6 +16,8 @@ export const customRender: CustomRender = (props) => {
       return () => <Modal {...props} />
     case 'Pagination':
       return () => <Pagination {...props} />
+    case 'DarkMode':
+      return () => <DarkMode {...props} />
   }
 }
 
diff --git a/src/npm-fastui/src/components/DarkMode.tsx b/src/npm-fastui/src/components/DarkMode.tsx
new file mode 100644
index 00000000..9f6b5a33
--- /dev/null
+++ b/src/npm-fastui/src/components/DarkMode.tsx
@@ -0,0 +1,12 @@
+import { FC } from 'react'
+
+import { ClassName } from '../hooks/className'
+
+export interface DarkModeProps {
+  type: 'DarkMode'
+  className?: ClassName
+}
+
+export const DarkModeComp: FC<DarkModeProps> = (props: DarkModeProps) => {
+  return <>`${props.type} are not implemented by pure FastUI, implement a component for 'DarkModeProps'.`</>
+}
diff --git a/src/npm-fastui/src/components/index.tsx b/src/npm-fastui/src/components/index.tsx
index ad6d5f4b..ebcf2716 100644
--- a/src/npm-fastui/src/components/index.tsx
+++ b/src/npm-fastui/src/components/index.tsx
@@ -43,6 +43,7 @@ import { IframeComp, IframeProps } from './Iframe'
 import { VideoComp, VideoProps } from './video'
 import { FireEventComp, FireEventProps } from './FireEvent'
 import { CustomComp, CustomProps } from './Custom'
+import { DarkModeComp, DarkModeProps } from './DarkMode'
 
 export type {
   TextProps,
@@ -73,6 +74,7 @@ export type {
   VideoProps,
   FireEventProps,
   CustomProps,
+  DarkModeProps,
 }
 
 // TODO some better way to export components
@@ -106,6 +108,7 @@ export type FastProps =
   | VideoProps
   | FireEventProps
   | CustomProps
+  | DarkModeProps
 
 export type FastClassNameProps = Exclude<FastProps, TextProps | AllDisplayProps | ServerLoadProps | PageTitleProps>
 
@@ -194,6 +197,8 @@ export const AnyComp: FC<FastProps> = (props) => {
         return <FireEventComp {...props} />
       case 'Custom':
         return <CustomComp {...props} />
+      case 'DarkMode':
+        return <DarkModeComp {...props} />
       default:
         unreachable('Unexpected component type', type, props)
         return <DisplayError title="Invalid Server Response" description={`Unknown component type: "${type}"`} />
diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py
index 9466f47b..7bc6e691 100644
--- a/src/python-fastui/fastui/components/__init__.py
+++ b/src/python-fastui/fastui/components/__init__.py
@@ -251,6 +251,11 @@ class Custom(_p.BaseModel, extra='forbid'):
     type: _t.Literal['Custom'] = 'Custom'
 
 
+class DarkMode(_p.BaseModel, extra='forbid'):
+    class_name: _class_name.ClassNameField = None
+    type: _t.Literal['DarkMode'] = 'DarkMode'
+
+
 AnyComponent = _te.Annotated[
     _t.Union[
         Text,
@@ -273,6 +278,7 @@ class Custom(_p.BaseModel, extra='forbid'):
         Video,
         FireEvent,
         Custom,
+        DarkMode,
         Table,
         Pagination,
         Display,
diff --git a/src/python-fastui/tests/react-fastui-json-schema.json b/src/python-fastui/tests/react-fastui-json-schema.json
index 6990db17..7ca6fdf3 100644
--- a/src/python-fastui/tests/react-fastui-json-schema.json
+++ b/src/python-fastui/tests/react-fastui-json-schema.json
@@ -151,6 +151,19 @@
       "required": ["data", "subType", "type"],
       "type": "object"
     },
+    "DarkModeProps": {
+      "properties": {
+        "className": {
+          "$ref": "#/definitions/ClassName"
+        },
+        "type": {
+          "const": "DarkMode",
+          "type": "string"
+        }
+      },
+      "required": ["type"],
+      "type": "object"
+    },
     "DetailsProps": {
       "properties": {
         "className": {
@@ -426,6 +439,9 @@
         },
         {
           "$ref": "#/definitions/CustomProps"
+        },
+        {
+          "$ref": "#/definitions/DarkModeProps"
         }
       ]
     },

From c6d4dbc6fd29172cea5fc1fc8b2cc22081626955 Mon Sep 17 00:00:00 2001
From: Chaojie <hi@chaojie.fun>
Date: Thu, 28 Dec 2023 15:25:57 +0800
Subject: [PATCH 2/3] Add demo for this

---
 demo/components_list.py | 8 ++++++++
 demo/main.py            | 1 +
 demo/shared.py          | 3 ---
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/demo/components_list.py b/demo/components_list.py
index 6e3622c1..c47e2294 100644
--- a/demo/components_list.py
+++ b/demo/components_list.py
@@ -199,6 +199,14 @@ class Delivery(BaseModel):
             ],
             class_name='border-top mt-3 pt-1',
         ),
+        c.Div(
+            components=[
+                c.Heading(text='DarkMode', level=2),
+                c.Markdown(text='`DarkMode` can be used to toggle dark mode on and off.'),
+                c.DarkMode(),
+            ],
+            class_name='border-top mt-3 pt-1',
+        ),
         c.Div(
             components=[
                 c.Heading(text='Custom', level=2),
diff --git a/demo/main.py b/demo/main.py
index b2fa16fe..118d1707 100644
--- a/demo/main.py
+++ b/demo/main.py
@@ -33,6 +33,7 @@ def api_index() -> list[AnyComponent]:
 * `Image` - example [here](/components#image)
 * `Iframe` - example [here](/components#iframe)
 * `Video` - example [here](/components#video)
+* `DarkMode` — example [here](/components#darkmode)
 * `Table` — See [cities table](/table/cities) and [users table](/table/users)
 * `Pagination` — See the bottom of the [cities table](/table/cities)
 * `ModelForm` — See [forms](/forms/login)
diff --git a/demo/shared.py b/demo/shared.py
index 05b9cc57..731d54ff 100644
--- a/demo/shared.py
+++ b/demo/shared.py
@@ -32,9 +32,6 @@ def demo_page(*components: AnyComponent, title: str | None = None) -> list[AnyCo
                     on_click=GoToEvent(url='/forms/login'),
                     active='startswith:/forms',
                 ),
-                c.Link(
-                    components=[c.DarkMode()],
-                ),
             ],
         ),
         c.Page(

From 10ac780f49e22ac6b139edc94df371320509f3ee Mon Sep 17 00:00:00 2001
From: Chaojie <hi@chaojie.fun>
Date: Fri, 27 Sep 2024 23:02:25 +0800
Subject: [PATCH 3/3] fix

---
 docs/api/python_components.md                   | 1 +
 src/npm-fastui/src/components/DarkMode.tsx      | 2 +-
 src/npm-fastui/src/models.d.ts                  | 2 +-
 src/python-fastui/fastui/components/__init__.py | 5 ++++-
 4 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/docs/api/python_components.md b/docs/api/python_components.md
index 1d8b5663..081e530f 100644
--- a/docs/api/python_components.md
+++ b/docs/api/python_components.md
@@ -28,6 +28,7 @@
         - Error
         - Spinner
         - Toast
+        - DarkMode
         - Custom
         - Table
         - Pagination
diff --git a/src/npm-fastui/src/components/DarkMode.tsx b/src/npm-fastui/src/components/DarkMode.tsx
index aca06d86..636403c3 100644
--- a/src/npm-fastui/src/components/DarkMode.tsx
+++ b/src/npm-fastui/src/components/DarkMode.tsx
@@ -3,5 +3,5 @@ import { FC } from 'react'
 import { DarkMode } from '../models'
 
 export const DarkModeComp: FC<DarkMode> = (props: DarkMode) => {
-  return <>`${props.type} are not implemented by pure FastUI, implement a component for 'DarkModeProps'.`</>
+  return <>`${props.type} are not implemented by pure FastUI, implement a component for &apos;DarkModeProps&apos;.`</>
 }
diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts
index b3f4c4c2..1e718bfd 100644
--- a/src/npm-fastui/src/models.d.ts
+++ b/src/npm-fastui/src/models.d.ts
@@ -341,7 +341,7 @@ export interface Custom {
   type: 'Custom'
 }
 /**
- * DarkMode Component.
+ * DarkMode component
  */
 export interface DarkMode {
   className?: ClassName
diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py
index 7755b472..4b25213e 100644
--- a/src/python-fastui/fastui/components/__init__.py
+++ b/src/python-fastui/fastui/components/__init__.py
@@ -50,6 +50,7 @@
     'Spinner',
     'Toast',
     'Custom',
+    'DarkMode',
     # then we include components from other files
     'Table',
     'Pagination',
@@ -597,7 +598,9 @@ class Custom(BaseModel, extra='forbid'):
     """The type of the component. Always 'Custom'."""
 
 
-class DarkMode(_p.BaseModel, extra='forbid'):
+class DarkMode(BaseModel, extra='forbid'):
+    """DarkMode component"""
+
     class_name: _class_name.ClassNameField = None
     type: _t.Literal['DarkMode'] = 'DarkMode'