Selaa lähdekoodia

Merge commit 'd7a2fb3d45fd56cc1f5ad0f4d52937a1ad8a8f8b' into v1_reverse_proxy

Rodja Trappe 2 vuotta sitten
vanhempi
säilyke
58edaba90f

+ 5 - 0
examples/custom_fastapi_app/main.py

@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 #!/usr/bin/env python3
 import frontend
 import frontend
+import uvicorn
 from fastapi import FastAPI
 from fastapi import FastAPI
 
 
 app = FastAPI()
 app = FastAPI()
@@ -9,3 +10,7 @@ frontend.init(app)
 @app.get('/')
 @app.get('/')
 def read_root():
 def read_root():
     return {'Hello': 'World'}
     return {'Hello': 'World'}
+
+
+if __name__ == '__main__':
+    uvicorn.run(app, host="0.0.0.0", port=8000)

+ 150 - 27
main.py

@@ -6,10 +6,8 @@ from pygments.formatters import HtmlFormatter
 
 
 from nicegui import Client, ui
 from nicegui import Client, ui
 from website import demo_card, reference
 from website import demo_card, reference
-
-ACCENT_COLOR = '#428BF5'
-HEADER_HEIGHT = '70px'
-STATIC = Path(__file__).parent / 'website' / 'static'
+from website.constants import ACCENT_COLOR, HEADER_HEIGHT, STATIC
+from website.example import bash_window, python_window
 
 
 ui.add_static_files('/favicon', Path(__file__).parent / 'website' / 'favicon')
 ui.add_static_files('/favicon', Path(__file__).parent / 'website' / 'favicon')
 
 
@@ -29,11 +27,18 @@ def add_head_html() -> None:
         <meta name="msapplication-config" content="favicon/browserconfig.xml">
         <meta name="msapplication-config" content="favicon/browserconfig.xml">
         <meta name="theme-color" content="#ffffff">
         <meta name="theme-color" content="#ffffff">
     ''')  # https://realfavicongenerator.net/
     ''')  # https://realfavicongenerator.net/
-    ui.add_head_html(r'''
+    ui.add_head_html(f'''
         <style>
         <style>
-        body {
+        html {{
+            scroll-behavior: smooth;
+        }}
+        body {{
             background-color: #f8f8f8;
             background-color: #f8f8f8;
-        }
+        }}
+        em {{
+            font-style: normal;
+            color: {ACCENT_COLOR};
+        }}
         </style>
         </style>
     ''')
     ''')
 
 
@@ -46,12 +51,10 @@ def add_header() -> None:
         with ui.link(target=index_page):
         with ui.link(target=index_page):
             ui.html((STATIC / 'nicegui_word.svg').read_text()).classes('w-24')
             ui.html((STATIC / 'nicegui_word.svg').read_text()).classes('w-24')
         with ui.row().classes('items-center ml-auto'):
         with ui.row().classes('items-center ml-auto'):
-            ui.link('Features').classes('text-lg').style('color: white!important')
-            ui.link('Installation').classes('text-lg').style('color: white!important')
-            ui.link('Documentation').classes('text-lg').style('color: white!important')
-            ui.link('API Reference', reference_page).classes('text-lg').style('color: white!important')
-            ui.link('Docker').classes('text-lg').style('color: white!important')
-            ui.link('Deployment').classes('text-lg').style('color: white!important')
+            ui.link('Features', '/#features').classes(replace='text-lg text-white')
+            ui.link('Installation', '/#installation').classes(replace='text-lg text-white')
+            ui.link('Examples', '/#examples').classes(replace='text-lg text-white')
+            ui.link('API Reference', reference_page).classes(replace='text-lg text-white')
             with ui.link(target='https://github.com/zauberzeug/nicegui/'):
             with ui.link(target='https://github.com/zauberzeug/nicegui/'):
                 ui.html((STATIC / 'github.svg').read_text()).classes('fill-white scale-125 m-1')
                 ui.html((STATIC / 'github.svg').read_text()).classes('fill-white scale-125 m-1')
 
 
@@ -67,7 +70,7 @@ async def index_page(client: Client):
             .style(f'height: calc(100vh - {HEADER_HEIGHT}); transform: translateX(-250px)'):
             .style(f'height: calc(100vh - {HEADER_HEIGHT}); transform: translateX(-250px)'):
         ui.html((STATIC / 'happy_face.svg').read_text()).classes('stroke-black').style('width: 500px')
         ui.html((STATIC / 'happy_face.svg').read_text()).classes('stroke-black').style('width: 500px')
         with ui.column().classes('gap-8'):
         with ui.column().classes('gap-8'):
-            ui.markdown('The NiceGUI you really\n\nneed in your life.') \
+            ui.html('Meet the <em>NiceGUI</em>.') \
                 .style('font-size: 400%; line-height: 0.9; font-weight: 500')
                 .style('font-size: 400%; line-height: 0.9; font-weight: 500')
             ui.markdown('An easy-to-use Python-based UI framework\n\nwhich shows up in your web browser.') \
             ui.markdown('An easy-to-use Python-based UI framework\n\nwhich shows up in your web browser.') \
                 .style('font-size: 200%; line-height: 0.9')
                 .style('font-size: 200%; line-height: 0.9')
@@ -75,33 +78,153 @@ async def index_page(client: Client):
     with ui.row() \
     with ui.row() \
             .classes('w-full q-pa-md items-center gap-28 p-32 no-wrap') \
             .classes('w-full q-pa-md items-center gap-28 p-32 no-wrap') \
             .style(f'height: calc(100vh - {HEADER_HEIGHT}); background: {ACCENT_COLOR}'):
             .style(f'height: calc(100vh - {HEADER_HEIGHT}); background: {ACCENT_COLOR}'):
-        with ui.column().classes('gap-8'):
+        with ui.column().classes('gap-6'):
             ui.markdown('Create buttons, dialogs, markdown,\n\n3D scenes, plots and much more at ease.') \
             ui.markdown('Create buttons, dialogs, markdown,\n\n3D scenes, plots and much more at ease.') \
-                .style('font-size: 300%; color: white; line-height: 0.9; font-weight: 500')
+                .style('font-size: 300%; color: white; line-height: 0.9; font-weight: 500').classes('mb-4')
             ui.label('''
             ui.label('''
                 It is great for micro web apps, dashboards, robotics projects, smart home solutions
                 It is great for micro web apps, dashboards, robotics projects, smart home solutions
                 and similar use cases. You can also use it in development, for example when
                 and similar use cases. You can also use it in development, for example when
                 tweaking/configuring a machine learning algorithm or tuning motor controllers.
                 tweaking/configuring a machine learning algorithm or tuning motor controllers.
-                NiceGUl is available as PyPl package, Docker image and on GitHub
-            ''').style('font-size: 150%; color: white')
-        with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
-            with ui.card().classes('no-shadow') \
-                    .style(f'min-width: 360px; height: 380px; clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)'):
-                demo_card.create_content()
-
-    with ui.row().classes('w-full q-pa-md'):
+            ''').style('font-size: 150%; color: white').classes('leading-tight')
+            with ui.row().style('font-size: 150%; color: white').classes('leading-tight gap-2'):
+                ui.html('''
+                    NiceGUI is available as
+                    <a href="https://pypi.org/project/nicegui/"><strong>PyPI package</strong><span class="material-icons">north_east</span></a>,
+                    <a href="https://hub.docker.com/r/zauberzeug/nicegui"><strong>Docker image</strong><span class="material-icons">north_east</span></a> and on
+                    <a href="https://github.com/zauberzeug/nicegui"><strong>GitHub</strong><span class="material-icons">north_east</span></a>.
+                ''')
+
+        demo_card.create()
+
+    ui.link_target('features').style(f'position: relative; top: -{HEADER_HEIGHT}')
+    with ui.column().classes('w-full q-pa-xl'):
+        ui.label('Features').classes('text-bold text-lg')
+        ui.html('What has <em>NiceGUI</em> to offer?') \
+            .style('font-size: 300%; font-weight: 500; margin-top: -20px')
+        with ui.row().classes('w-full no-wrap text-lg leading-tight justify-between'):
+            with ui.column().classes('gap-1'):
+                ui.label('User Interface').classes('text-bold mb-4')
+                ui.markdown('- common elements like label, button, checkbox, switch, slider, input, ...')
+                ui.markdown('- layouting with rows, columns, cards and dialogs')
+                ui.markdown('- HTML and markdown elements')
+                ui.markdown('- high-level elements like charts, tables, trees, 3D scenes, joystick, ...')
+                ui.markdown('- built-in timer to refresh data in intervals')
+                ui.markdown('- notifications, dialogs and menus')
+                ui.markdown('- keyboard input')
+            with ui.column().classes('gap-1'):
+                ui.label('Under the hood').classes('text-bold mb-4')
+                ui.markdown('- browser-based graphical user interface')
+                ui.markdown('- based on FastAPI and Uvicorn')
+                ui.markdown('- live-cycle events and session data')
+                ui.markdown('- customizable page layout and colors')
+            with ui.column().classes('gap-1'):
+                ui.label('Development').classes('text-bold mb-4')
+                ui.markdown('- implicit reload on code change')
+                ui.markdown('- straight-forward data binding')
+
+    ui.link_target('installation').style(f'position: relative; top: -{HEADER_HEIGHT}')
+    with ui.column().classes('w-full q-pa-xl'):
+        ui.label('Installation').classes('text-bold text-lg')
+        ui.html('Getting <em>started</em>') \
+            .style('font-size: 300%; font-weight: 500; margin-top: -20px')
+        with ui.row().classes('w-full no-wrap text-lg leading-tight'):
+            with ui.column().classes('w-1/3 gap-2'):
+                ui.html('<em>1.</em>').classes('text-3xl text-bold')
+                ui.markdown('Install').classes('text-lg')
+                with bash_window().classes('w-full h-52'):
+                    ui.markdown('```bash\npython3 -m pip install nicegui\n```')
+            with ui.column().classes('w-1/3 gap-2'):
+                ui.html('<em>2.</em>').classes('text-3xl text-bold')
+                ui.markdown('Write file __main.py__').classes('text-lg')
+                with python_window().classes('w-full h-52'):
+                    ui.markdown('''```python\n
+from nicegui import ui
+
+ui.label('Hello NiceGUI!')
+
+ui.run()
+```''')
+            with ui.column().classes('w-1/3 gap-2'):
+                ui.html('<em>3.</em>').classes('text-3xl text-bold')
+                ui.markdown('Launch it with').classes('text-lg')
+                with bash_window().classes('w-full h-52'):
+                    ui.markdown('```bash\npython3 main.py\n```')
+
+    ui.link_target('examples').style(f'position: relative; top: -{HEADER_HEIGHT}')
+    with ui.column().classes('w-full q-pa-xl'):
+        ui.label('Documentation').classes('text-bold text-lg')
+        ui.html('Interactive <em>Examples</em>') \
+            .style('font-size: 300%; font-weight: 500; margin-top: -20px')
         reference.create_intro()
         reference.create_intro()
 
 
     with ui.row() \
     with ui.row() \
             .classes('w-full items-center gap-28 px-32 py-16 no-wrap') \
             .classes('w-full items-center gap-28 px-32 py-16 no-wrap') \
             .style(f'background: {ACCENT_COLOR}'):
             .style(f'background: {ACCENT_COLOR}'):
         with ui.column().classes('gap-4'):
         with ui.column().classes('gap-4'):
-            ui.markdown('Go to the API reference to see a ton of live examples') \
+            ui.markdown('Go to the API reference to see a ton of live examples.') \
                 .style('font-size: 220%; color: white; line-height: 0.9; font-weight: 500')
                 .style('font-size: 220%; color: white; line-height: 0.9; font-weight: 500')
-            ui.label('The whole content of https://nicegui.io/ is implemented with NiceGUI itself.') \
+            ui.html('The whole content of <a href="https://nicegui.io/">nicegui.io</a> is implemented with NiceGUI itself.') \
                 .style('font-size: 150%; color: white')
                 .style('font-size: 150%; color: white')
         ui.link('API reference', '/reference') \
         ui.link('API reference', '/reference') \
-            .classes('rounded-full px-12 py-2 text-xl text-bold bg-white')
+            .classes('rounded-full mx-auto px-12 py-2 text-xl text-bold bg-white')
+
+    with ui.column().classes('w-full q-pa-xl'):
+        ui.label('In-depth demonstration').classes('text-bold text-lg')
+        ui.html('What else can you do with <em>NiceGUI</em>?') \
+            .style('font-size: 300%; font-weight: 500; margin-top: -20px')
+        with ui.row().classes('w-full no-wrap text-lg leading-tight'):
+            with ui.column().classes('w-1/3'):
+                ui.markdown(
+                    'You may also have a look at the following examples for in-depth demonstrations of what you can do with NiceGUI:')
+                example_link('Slideshow', 'implements a keyboard-controlled image slideshow')
+                example_link('Authentication', 'shows how to use sessions to build a login screen')
+                example_link(
+                    'Modularization',
+                    'provides an example of how to modularize your application into multiple files and create a specialized page decorator')
+            with ui.column().classes('w-1/3'):
+                example_link('Map', 'uses the JavaScript library leaflet to display a map at specific locations')
+                example_link(
+                    'AI Interface',
+                    'utilizes the great but non-async API from <https://replicate.com> to perform voice-to-text transcription and generate images from prompts with Stable Diffusion')
+                example_link('3D Scene', 'creates a 3D scene and loads an STL mesh illuminated with a spotlight')
+            with ui.column().classes('w-1/3'):
+                example_link('Custom Vue Component', 'shows how to write and integrate a custom vue component')
+                example_link('Image Mask Overlay', 'shows how to overlay an image with a mask')
+                example_link('Infinite Scroll', 'shows an infinitely scrolling image gallery')
+
+    with ui.row() \
+            .classes('w-full q-pa-md items-center gap-28 p-32 no-wrap') \
+            .style(f'height: calc(100vh - {HEADER_HEIGHT}); background: {ACCENT_COLOR}'):
+        with ui.column().classes('gap-6'):
+            ui.markdown('Why?') \
+                .style('font-size: 300%; color: white; line-height: 0.9; font-weight: 500').classes('mb-4')
+            ui.html('''
+                We like
+                <strong><a href="https://streamlit.io/">Streamlit</a></strong>
+                but find it does
+                <strong><a href="https://github.com/zauberzeug/nicegui/issues/1#issuecomment-847413651">too much magic</a></strong>
+                when it comes to state handling.
+                In search for an alternative nice library to write simple graphical user interfaces in Python we discovered
+                <strong><a href="https://justpy.io/">JustPy</a></strong>.
+                Although we liked the approach, it is too "low-level HTML" for our daily usage.
+
+                Therefore we created NiceGUI on top of
+                <strong><a href="https://fastapi.tiangolo.com/">FastAPI</a></strong>,
+                which itself is based on the ASGI framework
+                <strong><a href="https://www.starlette.io/">Starlette</a></strong>,
+                and the ASGI webserver
+                <strong><a href="https://www.uvicorn.org/">Uvicorn</a></strong>.
+            ''').style('font-size: 150%; color: white').classes('leading-tight')
+
+        ui.html((STATIC / 'happy_face.svg').read_text()).classes('stroke-white').style('width: 1500px')
+
+
+def example_link(title: str, description: str) -> None:
+    name = title.lower().replace(' ', '_')
+    with ui.column().classes('gap-0'):
+        ui.link(title, f'https://github.com/zauberzeug/nicegui/tree/main/examples/{name}/main.py') \
+            .classes(replace='text-black text-bold')
+        ui.markdown(description)
 
 
 
 
 @ui.page('/reference')
 @ui.page('/reference')

+ 1 - 1
nicegui/elements/colors.py

@@ -7,7 +7,7 @@ vue.register_component('colors', __file__, 'colors.js')
 class Colors(Element):
 class Colors(Element):
 
 
     def __init__(self, *,
     def __init__(self, *,
-                 primary='#1976d2',
+                 primary='#5A99FF',
                  secondary='#26a69a',
                  secondary='#26a69a',
                  accent='#9c27b0',
                  accent='#9c27b0',
                  positive='#21ba45',
                  positive='#21ba45',

+ 1 - 1
nicegui/nicegui.py

@@ -50,7 +50,7 @@ def on_startup() -> None:
     [safe_invoke(t) for t in globals.startup_handlers]
     [safe_invoke(t) for t in globals.startup_handlers]
     create_task(binding.loop())
     create_task(binding.loop())
     globals.state = globals.State.STARTED
     globals.state = globals.State.STARTED
-    #print(f'NiceGUI ready to go on http://{globals.host}:{globals.port}')
+    print(f'NiceGUI ready to go on http://{globals.host}:{globals.port}')
 
 
 
 
 @app.on_event('shutdown')
 @app.on_event('shutdown')

+ 10 - 1
nicegui/templates/index.html

@@ -88,7 +88,16 @@
           window.socket.on("notify", (msg) => Quasar.Notify.create(msg));
           window.socket.on("notify", (msg) => Quasar.Notify.create(msg));
           window.socket.on("disconnect", () => window.location.reload());
           window.socket.on("disconnect", () => window.location.reload());
         },
         },
-      }).use(Quasar);
+      }).use(Quasar, {
+        config: {
+          brand: {
+            primary: '#5A99FF',
+          },
+          loadingBar: {
+            color: 'primary'
+          },
+        }
+      });
 
 
       {{ vue_scripts | safe }}
       {{ vue_scripts | safe }}
       {{ js_imports | safe }}
       {{ js_imports | safe }}

+ 1 - 1
test_startup.sh

@@ -28,7 +28,7 @@ check() {
 }
 }
 
 
 error=0
 error=0
-check website/main.py || error=1
+check main.py || error=1
 for path in examples/*
 for path in examples/*
 do
 do
     check $path/main.py || error=1
     check $path/main.py || error=1

+ 5 - 0
website/constants.py

@@ -0,0 +1,5 @@
+from pathlib import Path
+
+ACCENT_COLOR = '#5A99FF'
+HEADER_HEIGHT = '70px'
+STATIC = Path(__file__).parent / 'static'

+ 23 - 13
website/demo_card.py

@@ -1,18 +1,28 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from .constants import STATIC
 
 
-def create_content():
-    with ui.column().classes('w-5/12'):
-        ui.button('Click me!', on_click=lambda: output.set_text('Click!'))
-        ui.checkbox('Check me!', on_change=lambda e: output.set_text('Checked.' if e.value else 'Unchecked.'))
-        ui.switch('Switch me!', on_change=lambda e: output.set_text('Switched.' if e.value else 'Unswitched.'))
-        ui.input('Text', value='abc', on_change=lambda e: output.set_text(e.value))
-        ui.number('Number', value=3.1415927, format='%.2f', on_change=lambda e: output.set_text(e.value))
 
 
-    with ui.column().classes('w-6/12'):
-        ui.slider(min=0, max=100, value=50, step=0.1, on_change=lambda e: output.set_text(e.value))
-        ui.radio(['A', 'B', 'C'], value='A', on_change=lambda e: output.set_text(e.value)).props('inline')
-        ui.toggle(['1', '2', '3'], value='1', on_change=lambda e: output.set_text(e.value))
-        ui.select({1: 'One', 2: 'Two', 3: 'Three'}, value=1, on_change=lambda e: output.set_text(e.value))
+def create():
+    with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
+        with ui.card().style(f'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'):
+                ui.html((STATIC / 'happy_face.svg').read_text()) \
+                    .classes('w-16 mx-6 stroke-black').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.checkbox('Check', on_change=lambda e: output.set_text('Checked' if e.value else 'Not checked'))
+                ui.switch('Switch', on_change=lambda e: output.set_text('Switched' if e.value else 'Not switched'))
+                ui.input('Text', value='abc', on_change=lambda e: output.set_text(e.value))
 
 
-    output = ui.label('Try it out!').classes('mt-8 w-44 text-xl text-grey-9 overflow-hidden text-ellipsis')
+            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')
+                ui.slider(min=0, max=100, value=50, step=0.1, on_change=lambda e: output.set_text(e.value)) \
+                    .style('width: 150px')
+                ui.radio(['A', 'B', 'C'], value='A', on_change=lambda e: output.set_text(e.value)).props('inline')
+                ui.toggle(['1', '2', '3'], value='1', on_change=lambda e: output.set_text(e.value))
+                with ui.row().classes('mt-1'):
+                    ui.number('Number', value=3.1415927, format='%.2f', on_change=lambda e: output.set_text(e.value)) \
+                        .classes('w-20')
+                    ui.select({1: 'One', 2: 'Two', 3: 'Three'}, value=1, on_change=lambda e: output.set_text(e.value))

+ 45 - 33
website/example.py

@@ -22,19 +22,16 @@ class example:
     def __call__(self, f: Callable) -> Callable:
     def __call__(self, f: Callable) -> Callable:
         with ui.row().classes('mb-2 flex w-full'):
         with ui.row().classes('mb-2 flex w-full'):
             if isinstance(self.content, str):
             if isinstance(self.content, str):
-                self._add_html_anchor(ui.markdown(self.content).classes(self.markdown_classes))
+                _add_html_anchor(ui.markdown(self.content).classes(self.markdown_classes))
             else:
             else:
                 doc = self.content.__doc__ or self.content.__init__.__doc__
                 doc = self.content.__doc__ or self.content.__init__.__doc__
                 html: str = docutils.core.publish_parts(doc, writer_name='html')['html_body']
                 html: str = docutils.core.publish_parts(doc, writer_name='html')['html_body']
                 html = html.replace('<p>', '<h4>', 1)
                 html = html.replace('<p>', '<h4>', 1)
                 html = html.replace('</p>', '</h4>', 1)
                 html = html.replace('</p>', '</h4>', 1)
                 html = apply_tailwind(html)
                 html = apply_tailwind(html)
-                self._add_html_anchor(ui.html(html).classes(self.markdown_classes))
+                _add_html_anchor(ui.html(html).classes(self.markdown_classes))
 
 
-            with ui.card() \
-                    .classes(self.rendering_classes) \
-                    .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
-                self._add_dots()
+            with browser_window().classes(self.rendering_classes):
                 f()
                 f()
 
 
             code = inspect.getsource(f).splitlines()
             code = inspect.getsource(f).splitlines()
@@ -57,33 +54,48 @@ class example:
                 code.append('ui.run()')
                 code.append('ui.run()')
             code.append('```')
             code.append('```')
             code = '\n'.join(code)
             code = '\n'.join(code)
-            with ui.card() \
-                    .classes(self.source_classes) \
-                    .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); background: #ebf0fe'):
-                self._add_dots()
+            with python_window().classes(self.source_classes):
                 ui.markdown(code)
                 ui.markdown(code)
         return f
         return f
 
 
-    @staticmethod
-    def _add_html_anchor(element: ui.html) -> None:
-        html = element.content
-        match = REGEX_H4.search(html)
-        if not match:
-            return
-        headline = match.groups()[0].strip()
-        headline_id = SPECIAL_CHARACTERS.sub('_', headline).lower()
-        if not headline_id:
-            return
-
-        icon = '<span class="material-icons">link</span>'
-        anchor = f'<a href="reference#{headline_id}" class="text-gray-300 hover:text-black">{icon}</a>'
-        html = html.replace('<h4', f'<h4 id="{headline_id}"', 1)
-        html = html.replace('</h4>', f' {anchor}</h4>', 1)
-        element.content = html
-
-    @staticmethod
-    def _add_dots() -> None:
-        with ui.row().classes('gap-1').style('transform: translate(-6px, -6px)'):
-            ui.icon('circle').style('font-size: 75%').classes('text-red-400')
-            ui.icon('circle').style('font-size: 75%').classes('text-yellow-400')
-            ui.icon('circle').style('font-size: 75%').classes('text-green-400')
+
+def _add_html_anchor(element: ui.html) -> None:
+    html = element.content
+    match = REGEX_H4.search(html)
+    if not match:
+        return
+    headline = match.groups()[0].strip()
+    headline_id = SPECIAL_CHARACTERS.sub('_', headline).lower()
+    if not headline_id:
+        return
+
+    icon = '<span class="material-icons">link</span>'
+    anchor = f'<a href="reference#{headline_id}" class="text-gray-300 hover:text-black">{icon}</a>'
+    html = html.replace('<h4', f'<h4 id="{headline_id}"', 1)
+    html = html.replace('</h4>', f' {anchor}</h4>', 1)
+    element.content = html
+
+
+def _add_dots() -> None:
+    with ui.row().classes('gap-1').style('transform: translate(-6px, -6px)'):
+        ui.icon('circle').style('font-size: 75%').classes('text-red-400')
+        ui.icon('circle').style('font-size: 75%').classes('text-yellow-400')
+        ui.icon('circle').style('font-size: 75%').classes('text-green-400')
+
+
+def window(color: str) -> ui.card:
+    with ui.card().style(f'box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); background: {color}') as card:
+        _add_dots()
+    return card
+
+
+def python_window() -> ui.card:
+    return window('#eff5ff')
+
+
+def browser_window() -> ui.card:
+    return window('white')
+
+
+def bash_window() -> ui.card:
+    return window('#e8e8e8')

+ 3 - 1
website/reference.py

@@ -37,8 +37,10 @@ Binding values between UI elements or [to data models](http://127.0.0.1:8080/ref
 
 
 
 
 def create_full() -> None:
 def create_full() -> None:
+    ui.html('<em>API</em> Documentation and Examples').classes('mt-8 text-5xl font-weight-500')
+
     def h3(text: str) -> None:
     def h3(text: str) -> None:
-        ui.label(text).classes('w-full mt-16 border-b border-slate-200 text-3xl font-light')
+        ui.html(f'<em>{text}</em>').classes('mt-8 text-3xl font-weight-500')
 
 
     h3('Basic Elements')
     h3('Basic Elements')