浏览代码

Merge pull request #1164 from zauberzeug/dark

Dark mode for NiceGUI documentation
Rodja Trappe 1 年之前
父节点
当前提交
c100482ad4

+ 34 - 9
main.py

@@ -47,6 +47,11 @@ def logo_square() -> FileResponse:
     return FileResponse(svg.PATH / 'logo_square.png', media_type='image/png')
 
 
+@app.post('/dark_mode')
+async def dark_mode(request: Request) -> None:
+    app.storage.browser['dark_mode'] = (await request.json()).get('value')
+
+
 @app.middleware('http')
 async def redirect_reference_to_documentation(request: Request,
                                               call_next: Callable[[Request], Awaitable[Response]]) -> Response:
@@ -74,6 +79,13 @@ def add_header(menu: Optional[ui.left_drawer] = None) -> None:
         'Examples': '/#examples',
         'Why?': '/#why',
     }
+    dark_mode = ui.dark_mode(value=app.storage.browser.get('dark_mode'), on_change=lambda e: ui.run_javascript(f'''
+        fetch('/dark_mode', {{
+            method: 'POST',
+            headers: {{'Content-Type': 'application/json'}},
+            body: JSON.stringify({{value: {e.value}}}),
+        }});
+    ''', respond=False))
     with ui.header() \
             .classes('items-center duration-200 p-0 px-4 no-wrap') \
             .style('box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)'):
@@ -82,19 +94,32 @@ def add_header(menu: Optional[ui.left_drawer] = None) -> None:
         with ui.link(target=index_page).classes('row gap-4 items-center no-wrap mr-auto'):
             svg.face().classes('w-8 stroke-white stroke-2 max-[550px]:hidden')
             svg.word().classes('w-24')
-        with ui.row().classes('max-lg:hidden'):
+
+        with ui.row().classes('max-[1050px]:hidden'):
             for title, target in menu_items.items():
                 ui.link(title, target).classes(replace='text-lg text-white')
+
         search = Search()
         search.create_button()
-        with ui.link(target='https://discord.gg/TEpFeAaF4f').classes('max-[445px]:hidden').tooltip('Discord'):
+
+        with ui.element().classes('max-[360px]:hidden'):
+            ui.button(icon='dark_mode', on_click=lambda: dark_mode.set_value(None)) \
+                .props('flat fab-mini color=white').bind_visibility_from(dark_mode, 'value', value=True)
+            ui.button(icon='light_mode', on_click=lambda: dark_mode.set_value(True)) \
+                .props('flat fab-mini color=white').bind_visibility_from(dark_mode, 'value', value=False)
+            ui.button(icon='brightness_auto', on_click=lambda: dark_mode.set_value(False)) \
+                .props('flat fab-mini color=white').bind_visibility_from(dark_mode, 'value', lambda mode: mode is None)
+
+        with ui.link(target='https://discord.gg/TEpFeAaF4f').classes('max-[455px]:hidden').tooltip('Discord'):
             svg.discord().classes('fill-white scale-125 m-1')
-        with ui.link(target='https://www.reddit.com/r/nicegui/').classes('max-[395px]:hidden').tooltip('Reddit'):
+        with ui.link(target='https://www.reddit.com/r/nicegui/').classes('max-[405px]:hidden').tooltip('Reddit'):
             svg.reddit().classes('fill-white scale-125 m-1')
-        with ui.link(target='https://github.com/zauberzeug/nicegui/').tooltip('GitHub'):
+        with ui.link(target='https://github.com/zauberzeug/nicegui/').classes('max-[305px]:hidden').tooltip('GitHub'):
             svg.github().classes('fill-white scale-125 m-1')
+
         add_star().classes('max-[490px]:hidden')
-        with ui.row().classes('lg:hidden'):
+
+        with ui.row().classes('min-[1051px]:hidden'):
             with ui.button(icon='more_vert').props('flat color=white round'):
                 with ui.menu().classes('bg-primary text-white text-lg'):
                     for title, target in menu_items.items():
@@ -108,7 +133,7 @@ async def index_page(client: Client) -> None:
     add_header()
 
     with ui.row().classes('w-full h-screen items-center gap-8 pr-4 no-wrap into-section'):
-        svg.face(half=True).classes('stroke-black w-[200px] md:w-[230px] lg:w-[300px]')
+        svg.face(half=True).classes('stroke-black dark:stroke-white w-[200px] md:w-[230px] lg:w-[300px]')
         with ui.column().classes('gap-4 md:gap-8 pt-32'):
             title('Meet the *NiceGUI*.')
             subtitle('And let any browser be the frontend of your Python code.') \
@@ -244,8 +269,8 @@ async def index_page(client: Client) -> None:
                     .classes('text-white text-2xl md:text-3xl font-medium')
                 ui.html('Fun-Fact: This whole website is also coded with NiceGUI.') \
                     .classes('text-white text-lg md:text-xl')
-            ui.link('Documentation', '/documentation') \
-                .classes('rounded-full mx-auto px-12 py-2 text-white bg-white font-medium text-lg md:text-xl')
+            ui.link('Documentation', '/documentation').style('color: black !important') \
+                .classes('rounded-full mx-auto px-12 py-2 bg-white font-medium text-lg md:text-xl')
 
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1600px] mx-auto'):
         link_target('examples', '-50px')
@@ -286,7 +311,7 @@ async def index_page(client: Client) -> None:
             example_link('Lightbox', 'A thumbnail gallery where each image can be clicked to enlarge')
             example_link('ROS2', 'Using NiceGUI as web interface for a ROS2 robot')
 
-    with ui.row().classes('bg-primary w-full min-h-screen mt-16'):
+    with ui.row().classes('dark-box min-h-screen mt-16'):
         link_target('why')
         with ui.column().classes('''
                 max-w-[1600px] m-auto

+ 1 - 1
nicegui/elements/dark_mode.js

@@ -9,7 +9,7 @@ export default {
     update() {
       Quasar.Dark.set(this.value === null ? "auto" : this.value);
       if (window.tailwind) {
-        tailwind.config.darkMode = this.auto ? "media" : "class";
+        tailwind.config.darkMode = this.value === null ? "media" : "class";
         if (this.value) document.body.classList.add("dark");
         else document.body.classList.remove("dark");
       }

+ 4 - 3
nicegui/elements/dark_mode.py

@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import Any, Callable, Optional
 
 from .mixins.value_element import ValueElement
 
@@ -6,7 +6,7 @@ from .mixins.value_element import ValueElement
 class DarkMode(ValueElement, component='dark_mode.js'):
     VALUE_PROP = 'value'
 
-    def __init__(self, value: Optional[bool] = False) -> None:
+    def __init__(self, value: Optional[bool] = False, *, on_change: Optional[Callable[..., Any]] = None) -> None:
         """Dark mode
 
         You can use this element to enable, disable or toggle dark mode on the page.
@@ -15,8 +15,9 @@ class DarkMode(ValueElement, component='dark_mode.js'):
         Note that this element overrides the `dark` parameter of the `ui.run` function and page decorators.
 
         :param value: Whether dark mode is enabled. If None, dark mode is set to auto.
+        :param on_change: Callback that is invoked when the value changes.
         """
-        super().__init__(value=value, on_value_change=None)
+        super().__init__(value=value, on_value_change=on_change)
 
     def enable(self) -> None:
         """Enable dark mode."""

+ 4 - 1
nicegui/elements/markdown.py

@@ -23,7 +23,10 @@ class Markdown(ContentElement, component='markdown.js'):
         self.extras = extras
         super().__init__(content=content)
         self._classes = ['nicegui-markdown']
-        self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
+        self._props['codehilite_css'] = (
+            HtmlFormatter(nobackground=True).get_style_defs('.codehilite') +
+            HtmlFormatter(nobackground=True, style='github-dark').get_style_defs('.body--dark .codehilite')
+        )
         if 'mermaid' in extras:
             self._props['use_mermaid'] = True
             self.libraries.append(Mermaid.exposed_libraries[0])

+ 29 - 39
website/demo.py

@@ -1,6 +1,6 @@
 import inspect
 import re
-from typing import Callable, Optional, Union
+from typing import Callable, Literal, Optional, Union
 
 import isort
 
@@ -8,20 +8,20 @@ from nicegui import ui
 
 from .intersection_observer import IntersectionObserver as intersection_observer
 
-PYTHON_BGCOLOR = '#00000010'
-PYTHON_COLOR = '#eef5fb'
-BASH_BGCOLOR = '#00000010'
-BASH_COLOR = '#e8e8e8'
-BROWSER_BGCOLOR = '#00000010'
-BROWSER_COLOR = '#ffffff'
+WindowType = Literal['python', 'bash', 'browser']
 
+UNCOMMENT_PATTERN = re.compile(r'^(\s*)# ?')
 
-uncomment_pattern = re.compile(r'^(\s*)# ?')
+WINDOW_BG_COLORS = {
+    'python': ('#eef5fb', '#2b323b'),
+    'bash': ('#e8e8e8', '#2b323b'),
+    'browser': ('#ffffff', '#181c21'),
+}
 
 
 def uncomment(text: str) -> str:
     """non-executed lines should be shown in the code examples"""
-    return uncomment_pattern.sub(r'\1', text)
+    return UNCOMMENT_PATTERN.sub(r'\1', text)
 
 
 def demo(f: Callable) -> Callable:
@@ -57,10 +57,6 @@ def demo(f: Callable) -> Callable:
     return f
 
 
-def _window_header(bgcolor: str) -> ui.row():
-    return ui.row().classes(f'w-full h-8 p-2 bg-[{bgcolor}]')
-
-
 def _dots() -> None:
     with ui.row().classes('gap-1 relative left-[1px] top-[1px]'):
         ui.icon('circle').classes('text-[13px] text-red-400')
@@ -68,43 +64,37 @@ def _dots() -> None:
         ui.icon('circle').classes('text-[13px] text-green-400')
 
 
-def _title(title: str) -> None:
-    ui.label(title).classes('text-sm text-gray-600 absolute left-1/2 top-[6px]').style('transform: translateX(-50%)')
-
-
-def _tab(content: Union[str, Callable], color: str, bgcolor: str) -> None:
-    with ui.row().classes('gap-0'):
-        with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
-            ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-br-[6px]')
-        with ui.row().classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] bg-[{color}] items-center gap-2'):
-            if callable(content):
-                content()
-            else:
-                ui.label(content)
-        with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
-            ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-bl-[6px]')
-
-
-def window(color: str, bgcolor: str, *,
-           title: str = '', tab: Union[str, Callable] = '', classes: str = '') -> ui.column:
-    with ui.card().classes(f'no-wrap bg-[{color}] rounded-xl p-0 gap-0 {classes}') \
+def window(type: WindowType, *, title: str = '', tab: Union[str, Callable] = '', classes: str = '') -> ui.column:
+    bar_color = ('#00000010', '#ffffff10')
+    color = WINDOW_BG_COLORS[type]
+    with ui.card().classes(f'no-wrap bg-[{color[0]}] dark:bg-[{color[1]}] rounded-xl p-0 gap-0 {classes}') \
             .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
-        with _window_header(bgcolor):
+        with ui.row().classes(f'w-full h-8 p-2 bg-[{bar_color[0]}] dark:bg-[{bar_color[1]}]'):
             _dots()
             if title:
-                _title(title)
+                ui.label(title) \
+                    .classes('text-sm text-gray-600 dark:text-gray-400 absolute left-1/2 top-[6px]') \
+                    .style('transform: translateX(-50%)')
             if tab:
-                _tab(tab, color, bgcolor)
+                with ui.row().classes('gap-0'):
+                    with ui.label().classes(f'w-2 h-[24px] bg-[{color[0]}] dark:bg-[{color[1]}]'):
+                        ui.label().classes(
+                            f'w-full h-full bg-[{bar_color[0]}] dark:bg-[{bar_color[1]}] rounded-br-[6px]')
+                    with ui.row().classes(f'text-sm text-gray-600 dark:text-gray-400 px-6 py-1 h-[24px] rounded-t-[6px] bg-[{color[0]}] dark:bg-[{color[1]}] items-center gap-2'):
+                        tab() if callable(tab) else ui.label(tab)
+                    with ui.label().classes(f'w-2 h-[24px] bg-[{color[0]}] dark:bg-[{color[1]}]'):
+                        ui.label().classes(
+                            f'w-full h-full bg-[{bar_color[0]}] dark:bg-[{bar_color[1]}] rounded-bl-[6px]')
         return ui.column().classes('w-full h-full overflow-auto')
 
 
 def python_window(title: Optional[str] = None, *, classes: str = '') -> ui.card:
-    return window(PYTHON_COLOR, PYTHON_BGCOLOR, title=title or 'main.py', classes=classes).classes('p-2 python-window')
+    return window('python', title=title or 'main.py', classes=classes).classes('p-2 python-window')
 
 
 def bash_window(*, classes: str = '') -> ui.card:
-    return window(BASH_COLOR, BASH_BGCOLOR, title='bash', classes=classes).classes('p-2 bash-window')
+    return window('bash', title='bash', classes=classes).classes('p-2 bash-window')
 
 
 def browser_window(title: Optional[Union[str, Callable]] = None, *, classes: str = '') -> ui.card:
-    return window(BROWSER_COLOR, BROWSER_BGCOLOR, tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')
+    return window('browser', tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')

+ 0 - 6
website/documentation.py

@@ -549,10 +549,6 @@ def create_full() -> None:
 
     load_demo(ui.run)
 
-    # HACK: switch color to white for the next demo
-    demo_BROWSER_BGCOLOR = demo.BROWSER_BGCOLOR
-    demo.BROWSER_BGCOLOR = '#ffffff'
-
     @text_demo('Native Mode', '''
         You can enable native mode for NiceGUI by specifying `native=True` in the `ui.run` function.
         To customize the initial window size and display mode, use the `window_size` and `fullscreen` parameters respectively.
@@ -576,8 +572,6 @@ def create_full() -> None:
         # ui.run(native=True, window_size=(400, 300), fullscreen=False)
         # END OF DEMO
         ui.button('enlarge', on_click=lambda: ui.notify('window will be set to 1000x700 in native mode'))
-    # HACK: restore color
-    demo.BROWSER_BGCOLOR = demo_BROWSER_BGCOLOR
 
     # Show a helpful workaround until issue is fixed upstream.
     # For more info see: https://github.com/r0x0r/pywebview/issues/1078

+ 5 - 5
website/example_card.py

@@ -8,7 +8,7 @@ def create() -> None:
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):
             with ui.column().classes('items-center'):
-                svg.face().classes('w-16 mx-6 stroke-black stroke-2') \
+                svg.face().classes('w-16 mx-6 stroke-black dark:stroke-gray-100 stroke-2') \
                     .on('click', lambda _: output.set_text("That's my face!"), [])
                 ui.button('Click me!', on_click=lambda: output.set_text('Clicked')).classes('w-full')
                 ui.input('Text', value='abc', on_change=lambda e: output.set_text(e.value))
@@ -16,8 +16,8 @@ def create() -> None:
                 ui.switch('Switch', on_change=lambda e: output.set_text('Switched on' if e.value else 'Switched off'))
 
             with ui.column().classes('items-center'):
-                output = ui.label('Try it out!') \
-                    .classes('w-44 my-6 h-8 text-xl text-grey-9 overflow-hidden text-ellipsis text-center')
+                output = ui.label('Try it out!').classes(
+                    'w-44 my-6 h-8 text-xl text-gray-800 dark:text-gray-200 overflow-hidden text-ellipsis text-center')
                 ui.slider(min=0, max=100, value=50, step=0.1, on_change=lambda e: output.set_text(e.value)) \
                     .style('width: 150px; margin-bottom: 2px')
                 with ui.row():
@@ -34,8 +34,8 @@ def create_narrow() -> None:
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):
             with ui.column().classes('items-center'):
                 svg.face().classes('w-16 mx-6 stroke-black stroke-2').on('click', lambda _: output.set_text("That's my face!"))
-                output = ui.label('Try it out!') \
-                    .classes('w-44 my-6 h-8 text-xl text-grey-9 overflow-hidden text-ellipsis text-center')
+                output = ui.label('Try it out!').classes(
+                    'w-44 my-6 h-8 text-xl text-gray-800 dark:text-gray-200 overflow-hidden text-ellipsis text-center')
                 ui.button('Click me!', on_click=lambda: output.set_text('Clicked')).classes('w-full')
                 ui.input('Text', value='abc', on_change=lambda e: output.set_text(e.value))
 

+ 10 - 2
website/more_documentation/dark_mode_documentation.py

@@ -1,5 +1,7 @@
 from nicegui import ui
 
+from ..demo import WINDOW_BG_COLORS
+
 
 def main_demo() -> None:
     # dark = ui.dark_mode()
@@ -9,5 +11,11 @@ def main_demo() -> None:
     # END OF DEMO
     l = ui.label('Switch mode:')
     c = l.parent_slot.parent
-    ui.button('Dark', on_click=lambda: (l.style('color: white'), c.style('background-color: var(--q-dark-page)')))
-    ui.button('Light', on_click=lambda: (l.style('color: default'), c.style('background-color: default')))
+    ui.button('Dark', on_click=lambda: (
+        l.style('color: white'),
+        c.style(f'background-color: {WINDOW_BG_COLORS["browser"][1]}'),
+    ))
+    ui.button('Light', on_click=lambda: (
+        l.style('color: black'),
+        c.style(f'background-color: {WINDOW_BG_COLORS["browser"][0]}'),
+    ))

+ 17 - 0
website/static/style.css

@@ -4,6 +4,10 @@ body {
   overflow-x: hidden;
   background-color: #f8f8f8;
   font-family: "Fira Sans", Roboto, -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;
+  --q-dark-page: #222;
+}
+html:has(.body--dark) {
+  background-color: #222;
 }
 .browser-window {
   font-family: Roboto, -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;
@@ -46,6 +50,12 @@ a:active:not(.browser-window *) {
   background-color: #5898d4d0;
   backdrop-filter: blur(5px);
 }
+.body--dark .q-header {
+  background-color: #3e6a94;
+}
+.body--dark .q-header.fade {
+  background-color: #3e6a94d0;
+}
 
 .scroll-indicator:after {
   content: "";
@@ -61,6 +71,10 @@ a:active:not(.browser-window *) {
   animation: sdb04 1.5s infinite;
   transition-timing-function: ease;
 }
+.body--dark .scroll-indicator:after {
+  border-left: 3px solid #bbb;
+  border-bottom: 3px solid #bbb;
+}
 @-webkit-keyframes sdb04 {
   0% {
     -webkit-transform: rotate(-45deg) translate(0, 0);
@@ -109,6 +123,9 @@ dl.docinfo p {
   background-color: #5898d4;
   width: 100%;
 }
+.body--dark .dark-box {
+  background-color: #3e6a94;
+}
 
 @media only screen and (min-width: 1024px) {
   html {

+ 3 - 3
website/style.py

@@ -35,8 +35,8 @@ def example_link(title: str, description: str) -> None:
     with ui.link(target=f'https://github.com/zauberzeug/nicegui/tree/main/examples/{name}/{filename}') \
             .classes('bg-[#5898d420] p-4 self-stretch rounded flex flex-col gap-2') \
             .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
-        ui.label(title).classes(replace='text-black font-bold')
-        ui.markdown(description).classes(replace='text-black bold-links arrow-links')
+        ui.label(title).classes(replace='font-bold')
+        ui.markdown(description).classes(replace='bold-links arrow-links')
 
 
 def features(icon: str, title: str, items: List[str]) -> None:
@@ -49,5 +49,5 @@ def features(icon: str, title: str, items: List[str]) -> None:
 
 def side_menu() -> ui.left_drawer:
     return ui.left_drawer() \
-        .classes('column no-wrap gap-1 bg-[#eee] mt-[-20px] px-8 py-20') \
+        .classes('column no-wrap gap-1 bg-[#eee] dark:bg-[#1b1b1b] mt-[-20px] px-8 py-20') \
         .style('height: calc(100% + 20px) !important')