├── hyperdiv_docs ├── __init__.py ├── demos │ ├── __init__.py │ ├── counter_plugin │ │ ├── __init__.py │ │ ├── counter.py │ │ └── assets │ │ │ ├── counter.css │ │ │ └── counter.js │ ├── leaflet_plugin │ │ ├── __init__.py │ │ ├── leaflet.py │ │ └── assets │ │ │ └── leaflet-plugin.js │ └── app_template_demo.py ├── pages │ ├── __init__.py │ ├── guide │ │ ├── __init__.py │ │ ├── static_assets.py │ │ ├── loops.py │ │ ├── conditional_rendering.py │ │ ├── interactivity.py │ │ ├── getting_started.py │ │ ├── state.py │ │ ├── components.py │ │ ├── matplotlib_charts.py │ │ ├── modular_apps.py │ │ ├── using_the_app_template.py │ │ ├── layout.py │ │ ├── pages_and_navigation.py │ │ ├── component_props.py │ │ ├── tasks.py │ │ └── deploying.py │ ├── reference │ │ ├── __init__.py │ │ ├── cli.py │ │ ├── icons.py │ │ ├── prop_types.py │ │ ├── env_variables.py │ │ ├── design_tokens.py │ │ └── components.py │ ├── introduction │ │ ├── __init__.py │ │ ├── docs_overview.py │ │ └── overview.py │ └── extending_hyperdiv │ │ ├── __init__.py │ │ ├── custom_assets.py │ │ ├── overview.py │ │ ├── new_components.py │ │ └── built_in_components.py ├── extractor │ ├── __init__.py │ ├── hyperdiv_module_path.py │ ├── README.md │ ├── dirutils.py │ ├── types.py │ ├── main.py │ ├── class_attribute_docs.py │ ├── docstring_extractor.py │ ├── top_level_docs.py │ └── extractor.py ├── utils.py ├── router.py ├── docs_metadata.py ├── main.py ├── menu.py ├── code_examples.py └── page.py ├── .gitignore ├── assets ├── kitten.jpg ├── kitten-bw.jpg ├── iframe-example.html ├── hd-logo-black.svg └── hd-logo-white.svg ├── start.py ├── README.md └── LICENSE /hyperdiv_docs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/reference/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/introduction/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/extending_hyperdiv/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/counter_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from .counter import counter 2 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/leaflet_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from .leaflet import leaflet 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | .DS_Store 4 | hyperdiv_docs/extractor/extracted.json 5 | -------------------------------------------------------------------------------- /assets/kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdiv/hyperdiv-docs/HEAD/assets/kitten.jpg -------------------------------------------------------------------------------- /assets/kitten-bw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdiv/hyperdiv-docs/HEAD/assets/kitten-bw.jpg -------------------------------------------------------------------------------- /assets/iframe-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello, this is an iframe! 4 | 5 | 6 | -------------------------------------------------------------------------------- /hyperdiv_docs/utils.py: -------------------------------------------------------------------------------- 1 | def render_value(v): 2 | if isinstance(v, str): 3 | return repr(v) 4 | return str(v) 5 | 6 | 7 | def render_value_list(vs): 8 | return ", ".join([render_value(v) for v in vs]) 9 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/counter_plugin/counter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hyperdiv as hd 3 | 4 | 5 | class counter(hd.Plugin): 6 | _assets_root = os.path.join(os.path.dirname(__file__), "assets") 7 | _assets = ["*"] 8 | 9 | count = hd.Prop(hd.Int, 0) 10 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/counter_plugin/assets/counter.css: -------------------------------------------------------------------------------- 1 | button { 2 | padding: 0.5rem; 3 | background-color: var(--sl-color-blue-100); 4 | border: none; 5 | border-radius: var(--sl-border-radius-large); 6 | cursor: pointer; 7 | font-size: 1rem; 8 | width: fit-content; 9 | } 10 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from hyperdiv_docs.main import main 3 | 4 | index_page = hd.index_page( 5 | title="Hyperdiv Docs", 6 | description="Learn how to use the Hyperdiv web framework", 7 | keywords=("hyperdiv", "python", "web framework", "rapid development"), 8 | favicon="/assets/hd-logo-white.svg", 9 | ) 10 | 11 | 12 | hd.run(main, index_page=index_page) 13 | -------------------------------------------------------------------------------- /hyperdiv_docs/router.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from .page import page 3 | 4 | router = hd.router() 5 | 6 | 7 | @router.not_found 8 | def not_found(): 9 | with page() as p: 10 | p.title("# Not Found") 11 | hd.alert( 12 | "Oops, it doesn't look like there is anything here.", 13 | opened=True, 14 | variant="danger", 15 | ) 16 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/hyperdiv_module_path.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | 4 | 5 | def get_hyperdiv_module_path(): 6 | """ 7 | We assume that the `hyperdiv` repo exists alongside the 8 | `hyperdiv-docs` repo. 9 | """ 10 | return ( 11 | pathlib.Path(os.path.dirname(__file__)) 12 | / ".." 13 | / ".." 14 | / ".." 15 | / "hyperdiv" 16 | / "hyperdiv" 17 | ).resolve() 18 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/README.md: -------------------------------------------------------------------------------- 1 | # Docs Extractor 2 | 3 | This module parses the hyperdiv repo and generates a JSON-encodable data structure that contains all the necessary metadata to render the docs components and prop type pages, without having to further reflect on hyperdiv code at runtime. 4 | 5 | The JSON data structure can be statically generated and stored, so the code that extracts the docs metadata doesn't have to run until the file has to be re-generated. 6 | -------------------------------------------------------------------------------- /assets/hd-logo-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/hd-logo-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/leaflet_plugin/leaflet.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hyperdiv as hd 3 | 4 | 5 | class leaflet(hd.Plugin): 6 | _assets_root = os.path.join(os.path.dirname(__file__), "assets") 7 | _assets = [ 8 | "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css", 9 | "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js", 10 | "leaflet-plugin.js", 11 | ] 12 | 13 | zoom = hd.Prop(hd.Int, 13) 14 | lat = hd.Prop(hd.Float, 51.505) 15 | lng = hd.Prop(hd.Float, -0.09) 16 | 17 | def __init__(self, height=20, **kwargs): 18 | super().__init__(height=height, **kwargs) 19 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/reference/cli.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/reference/cli") 8 | def cli(): 9 | with page() as p: 10 | 11 | p.title("# The Hyperdiv CLI") 12 | 13 | hd.markdown( 14 | """ 15 | 16 | When you install Hyperdiv, a command-line utility named 17 | `hyperdiv` will be automatically installed. 18 | 19 | Currently, the CLI provides only one command, which opens 20 | this documentation app when run: 21 | 22 | ```sh 23 | hyperdiv docs 24 | ``` 25 | 26 | The CLI will provide additional commands in the future. 27 | 28 | """ 29 | ) 30 | -------------------------------------------------------------------------------- /hyperdiv_docs/docs_metadata.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import pathlib 4 | 5 | metadata = None 6 | json_path = pathlib.Path(os.path.dirname(__file__), "docs_metadata.json") 7 | 8 | 9 | def get_docs_metadata(): 10 | global metadata 11 | 12 | if metadata: 13 | return metadata 14 | 15 | if json_path.exists(): 16 | with open(json_path) as f: 17 | metadata = json.loads(f.read()) 18 | return metadata 19 | else: 20 | from .extractor.main import extract 21 | 22 | metadata = extract() 23 | return metadata 24 | 25 | 26 | def create_docs_metadata(): 27 | """ 28 | (Re)-creates the stored JSON file containing docs metadata. 29 | """ 30 | from .extractor.main import extract 31 | 32 | if json_path.exists(): 33 | os.unlink(json_path) 34 | data = extract() 35 | with open(json_path, "w") as f: 36 | f.write(json.dumps(data, indent=2)) 37 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/app_template_demo.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | 3 | router = hd.router() 4 | 5 | # Define the pages of the app: 6 | 7 | 8 | @router.route("/app-template-demo") 9 | def home(): 10 | hd.markdown("# Welcome") 11 | 12 | 13 | @router.route("/app-template-demo/users") 14 | def users(): 15 | hd.markdown("# Users") 16 | 17 | 18 | def main(): 19 | template = hd.template(logo="/assets/hd-logo-white.svg", title="My App") 20 | 21 | # Sidebar menu linking to the app's pages: 22 | template.add_sidebar_menu( 23 | { 24 | "Home": {"icon": "house", "href": home.path}, 25 | "Users": {"icon": "people", "href": users.path}, 26 | } 27 | ) 28 | 29 | # A topbar contact link: 30 | template.add_topbar_links( 31 | {"Contact": {"icon": "envelope", "href": "mailto:hello@hyperdiv.io"}} 32 | ) 33 | 34 | # Render the active page in the body: 35 | with template.body: 36 | router.run() 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperdiv Docs 2 | 3 | This repo contains the Hyperdiv documentation app, which is written in Hyperdiv. 4 | 5 | ![Docs Gif](https://github.com/hyperdiv/hyperdiv-docs/assets/5980501/34a9b312-ae0c-4af2-9889-42b52db63439) 6 | 7 | # Opening the Documentation App 8 | 9 | The documentation app ships with Hyperdiv when installing Hyperdiv from PyPI. After installing Hyperdiv, run this command to open the docs app in a browser tab: 10 | ``` 11 | hyperdiv docs 12 | ``` 13 | 14 | # For Developers 15 | 16 | After cloning the Hyperdiv repo and installing its dependencies, you can simply clone this repo and run its `start.py` script within Hyperdiv's Poetry virtualenv: 17 | ```sh 18 | cd hyperdiv 19 | # Install Python dependencies 20 | poetry install 21 | # Install frontend dependencies 22 | cd frontend 23 | npm install 24 | npm run build 25 | cd .. 26 | # Enter the virtualenv 27 | poetry shell 28 | # Run the docs app in the virtualenv 29 | cd ../hyperdiv-docs 30 | python start.py 31 | ``` 32 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/extending_hyperdiv/custom_assets.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import docs_markdown 5 | 6 | 7 | @router.route("/extending-hyperdiv/custom-assets") 8 | def custom_assets(): 9 | with page() as p: 10 | p.title("# Loading Custom Assets") 11 | 12 | docs_markdown( 13 | """ 14 | 15 | Hyperdiv can be extended by loading custom JS and CSS at 16 | the top level of the app. For example, you can [set up 17 | Google 18 | Analytics](https://www.w3schools.com/howto/howto_google_analytics.asp) 19 | for your app, which involves adding Google's custom 20 | Javascript tags to the app's top-level. Or, load your own 21 | custom Javascript that runs every time the app is loaded. 22 | 23 | To learn how to load custom assets at the top level, see 24 | the documentation for @component(index_page). 25 | 26 | """ 27 | ) 28 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/dirutils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | 5 | def get_files_recursively(path): 6 | paths = path.iterdir() 7 | files = [] 8 | for path in paths: 9 | if os.path.isdir(path): 10 | files.extend(get_files_recursively(path)) 11 | elif path.name.endswith(".py") and os.path.exists(path): 12 | files.append(str(path)) 13 | return files 14 | 15 | 16 | def get_modules_recursively(path, module_name): 17 | paths = path.iterdir() 18 | modules = [] 19 | for path in paths: 20 | if os.path.isdir(path): 21 | modules.extend( 22 | get_modules_recursively(path, f"{module_name}.{os.path.split(path)[1]}") 23 | ) 24 | elif ( 25 | path.name.endswith(".py") 26 | and path.name != "__init__.py" 27 | and os.path.exists(path) 28 | ): 29 | nested_module_name = re.split(r"\.py$", path.name)[0] 30 | modules.append(f"{module_name}.{nested_module_name}") 31 | return modules 32 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/counter_plugin/assets/counter.js: -------------------------------------------------------------------------------- 1 | window.hyperdiv.registerPlugin("counter", (ctx) => { 2 | let count = 0; 3 | const button = document.createElement("button"); 4 | button.innerText = "Increment"; 5 | const countDiv = document.createElement("div"); 6 | 7 | const updateCount = (newCount) => { 8 | count = newCount; 9 | countDiv.innerText = count; 10 | }; 11 | 12 | // On click, increment the count and send the updated 13 | // prop value to Python: 14 | button.addEventListener("click", () => { 15 | updateCount(count + 1); 16 | ctx.updateProp("count", count); 17 | }); 18 | 19 | // Handle incoming prop updates from Python. We ignore 20 | // `propValue` because there is only one prop, "count". 21 | ctx.onPropUpdate((propName, propValue) => { 22 | updateCount(propValue); 23 | }); 24 | 25 | // Add the dom elements to the shadow root. 26 | ctx.domElement.appendChild(button); 27 | ctx.domElement.appendChild(countDiv); 28 | 29 | // Initialize the plugin with the initial count. 30 | updateCount(ctx.initialProps.count); 31 | }); 32 | -------------------------------------------------------------------------------- /hyperdiv_docs/main.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | import hyperdiv as hd 3 | from .router import router 4 | from .menu import menu 5 | from .demos.app_template_demo import main as demo_main 6 | 7 | 8 | def render_title(slot=None): 9 | with hd.hbox(gap=0.5, font_size=1, align="center", slot=slot): 10 | hd.text("Hyperdiv", font_weight="bold") 11 | hd.text("Docs") 12 | hd.badge( 13 | f'v{version("hyperdiv")}', 14 | background_color="neutral-200", 15 | font_color="neutral-700", 16 | font_size=0.7, 17 | ) 18 | 19 | 20 | def main(): 21 | loc = hd.location() 22 | if loc.path.startswith("/app-template-demo"): 23 | demo_main() 24 | return 25 | 26 | t = hd.theme() 27 | app = hd.template( 28 | logo=f'/assets/hd-logo-{"black" if t.is_light else "white"}.svg', 29 | ) 30 | app.add_sidebar_menu(menu) 31 | with app.app_title: 32 | render_title() 33 | with app.drawer_title: 34 | render_title() 35 | app.body.padding = 0 36 | with app.body: 37 | router.run() 38 | -------------------------------------------------------------------------------- /hyperdiv_docs/demos/leaflet_plugin/assets/leaflet-plugin.js: -------------------------------------------------------------------------------- 1 | window.hyperdiv.registerPlugin("leaflet", ctx => { 2 | const L = window.L; 3 | 4 | const props = {...ctx.initialProps}; 5 | 6 | const mapContainer = document.createElement("div"); 7 | mapContainer.style.width = "100%"; 8 | mapContainer.style.height = "100%"; 9 | ctx.domElement.appendChild(mapContainer); 10 | 11 | const map = L.map(mapContainer); 12 | map.setView([props.lat, props.lng], props.zoom); 13 | 14 | L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { 15 | maxZoom: 19, 16 | attribution: 17 | '© OpenStreetMap', 18 | }).addTo(map); 19 | 20 | map.on("zoomend", () => { 21 | props.zoom = map.getZoom(); 22 | ctx.updateProp("zoom", props.zoom); 23 | }); 24 | 25 | map.on("moveend", () => { 26 | const center = map.getCenter(); 27 | props.lat = center.lat; 28 | props.lng = center.lng; 29 | ctx.updateProp("lat", props.lat); 30 | ctx.updateProp("lng", props.lng); 31 | }); 32 | 33 | ctx.onPropUpdate((propName, propValue) => { 34 | props[propName] = propValue; 35 | map.setView([props.lat, props.lng], props.zoom); 36 | }); 37 | 38 | const resizeObserver = new ResizeObserver(() => map.invalidateSize()); 39 | resizeObserver.observe(mapContainer); 40 | }); 41 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/extending_hyperdiv/overview.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | 5 | 6 | @router.route("/extending-hyperdiv") 7 | def overview(): 8 | with page() as p: 9 | p.title("# Extending Hyperdiv") 10 | 11 | hd.markdown( 12 | """ 13 | 14 | Hyperdiv can be extended in multiple ways, with new 15 | functionality that isn't already built-in. 16 | 17 | The sub-sections of this part of the documentation explore 18 | the various ways Hyperdiv can be extended with new 19 | functionality: 20 | 21 | * [Building Plugins](/extending-hyperdiv/plugins) - 22 | building new, self-contained, reusable components with 23 | custom Javascript, CSS, and other assets. 24 | 25 | * [Loading Custom 26 | Assets](/extending-hyperdiv/custom-assets) - Loading 27 | custom Javascript and CSS assets into the top-level 28 | application. 29 | 30 | * [Extending Built-In 31 | Components](/extending-hyperdiv/built-in-components) - 32 | Subclassing and extending existing Hyperdiv components. 33 | 34 | * [Creating New 35 | Components](/extending-hyperdiv/new-components) - An 36 | alternative way to define new, simple components without 37 | building plugins. 38 | 39 | """ 40 | ) 41 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/types.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import importlib 3 | import hyperdiv 4 | from .dirutils import get_modules_recursively 5 | from .hyperdiv_module_path import get_hyperdiv_module_path 6 | 7 | 8 | def get_types(): 9 | """ 10 | Returns a dict type_name -> type of all Hyperdiv types, by 11 | dynamically importing all the relevant Hyperdiv modules, iterating 12 | over their variables, and collecting the ones with type 13 | HyperdivType. 14 | """ 15 | 16 | types = dict() 17 | 18 | hyperdiv_path = get_hyperdiv_module_path() 19 | 20 | modules = ( 21 | get_modules_recursively( 22 | hyperdiv_path / "component_mixins", 23 | "hyperdiv.component_mixins", 24 | ) 25 | + get_modules_recursively( 26 | hyperdiv_path / "prop_types", 27 | "hyperdiv.prop_types", 28 | ) 29 | + get_modules_recursively( 30 | hyperdiv_path / "components", 31 | "hyperdiv.components", 32 | ) 33 | ) 34 | 35 | for module_string in modules: 36 | m = importlib.import_module(module_string) 37 | for name, typ in sorted(vars(m).items()): 38 | if isinstance(typ, hyperdiv.prop_types.HyperdivType): 39 | types[name] = typ 40 | elif ( 41 | inspect.isclass(typ) 42 | and issubclass(typ, hyperdiv.prop_types.HyperdivType) 43 | and not typ.__name__.endswith("Def") 44 | ): 45 | types[name] = typ 46 | 47 | return types 48 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/main.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import hyperdiv as hd 3 | from hyperdiv.prop_types import HyperdivType 4 | from .extractor import Extractor 5 | 6 | 7 | def extract(): 8 | """ 9 | Extracts metadata from the Hyperdiv repo, which is used to 10 | dynamically render the docs components and prop types pages. 11 | """ 12 | 13 | ctx = Extractor() 14 | 15 | # Iterate over all the attributes exported by `hyperdiv` 16 | for name, attr in vars(hd).items(): 17 | # Skip internal attributes and modules 18 | if name.startswith("__") or inspect.ismodule(attr): 19 | continue 20 | 21 | # If it's a type, extract the type. 22 | is_type = False 23 | 24 | try: 25 | if isinstance(attr, HyperdivType): 26 | is_type = True 27 | except Exception: 28 | pass 29 | 30 | try: 31 | if issubclass(attr, HyperdivType): 32 | is_type = True 33 | except Exception: 34 | pass 35 | 36 | if is_type: 37 | ctx.extract_prop_type(attr, toplevel=True) 38 | continue 39 | 40 | # Skip definitions like hbox, vbox which are functools.partial 41 | # aliases. 42 | try: 43 | attr.__name__ 44 | except AttributeError: 45 | continue 46 | 47 | # The rest of the attributes must be component classes or 48 | # functions. 49 | ctx.extract_component(attr) 50 | 51 | # Extract all the types in Hyperdiv 52 | for typ in ctx.types.values(): 53 | ctx.extract_prop_type(typ) 54 | 55 | return ctx.output 56 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/static_assets.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/static-assets") 8 | def static_assets(): 9 | with page() as p: 10 | 11 | p.title("# Static Assets") 12 | 13 | hd.markdown( 14 | """ 15 | 16 | Apps will in general want to display images or provide 17 | links to static documents. 18 | 19 | To make static assets available to your app, you can 20 | create a directory called `assets` in the same directory 21 | as the application's start script. If you store your 22 | application in a directory called `my_app`, you can create 23 | a directory structure like this: 24 | 25 | ```sh 26 | my_app/ 27 | assets/ 28 | my-image.png 29 | a-text-file.txt 30 | ... 31 | start.py 32 | ... 33 | ``` 34 | 35 | Then, the app whose entrypoint is `start.py` will be able 36 | to refer to these static assets using paths prefixed by 37 | `"/assets"`. For example: 38 | 39 | ```py 40 | hd.image("/assets/my-image.png") 41 | hd.link("A text file", href="/assets/a-text-file.txt") 42 | ``` 43 | 44 | Note that the assets directory must be named `assets`, and 45 | must have the same parent directory as the app script, in 46 | order for Hyperdiv to detect it. 47 | 48 | """ 49 | ) 50 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/introduction/docs_overview.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import docs_markdown 5 | 6 | 7 | @router.route("/introduction/docs-overview") 8 | def docs_overview(): 9 | with page() as p: 10 | p.title("# This Documentation App") 11 | 12 | hd.markdown( 13 | """ 14 | The rest of this app is organized in the following sections, which 15 | can be accessed via the sidebar: 16 | """ 17 | ) 18 | 19 | hd.markdown("### Guide", font_color="blue") 20 | 21 | hd.markdown( 22 | """ 23 | 24 | The [Guide](/guide/getting-started) section is a guided 25 | walk through some of Hyperdiv's core features. It is not 26 | an exhaustive explanation of Hyperdiv. Instead, it tries 27 | to impart a high level taste of what's currently possible 28 | with Hyperdiv. 29 | 30 | """ 31 | ) 32 | 33 | hd.markdown("### Extending Hyperdiv", font_color="blue") 34 | 35 | hd.markdown( 36 | """ 37 | 38 | The [Extending Hyperdiv](/extending-hyperdiv) section 39 | dives into various ways to customize and extend Hyperdiv 40 | components, and build new components with custom 41 | Javascript, HTML, and CSS. 42 | 43 | """ 44 | ) 45 | 46 | hd.markdown("### Reference", font_color="blue") 47 | 48 | hd.markdown( 49 | """ 50 | 51 | The [Reference](/reference/components) section documents 52 | each Hyperdiv component and function in detail. 53 | 54 | """ 55 | ) 56 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/loops.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/loops") 8 | def loops(): 9 | with page() as p: 10 | p.title("# Rendering in Loops") 11 | 12 | docs_markdown( 13 | """ 14 | 15 | When rendering components in loops, we have to take a bit 16 | of extra precaution. For example, if we want to render 5 17 | sliders in a loop, this code will not work: 18 | 19 | ```py 20 | for i in range(5): 21 | slider = hd.slider() 22 | hd.text(slider.value) 23 | ``` 24 | 25 | The reason for the failure is that Hyperdiv internally 26 | assigns each component a unique ID (key), based roughly on 27 | the position in the code where that component is 28 | instantiated. 29 | 30 | The fix is to wrap the loop body in @component(scope), and 31 | pass to each scope a value that is unique per loop 32 | iteration. In this case `i` is unique per loop iteration: 33 | 34 | ```py 35 | for i in range(5): 36 | with hd.scope(i): 37 | slider = hd.slider() 38 | hd.text(slider.value) 39 | ``` 40 | 41 | Passing `i` to `scope` gives Hyperdiv the extra 42 | information it needs to generate unique IDs for all 43 | components. 44 | 45 | """ 46 | ) 47 | 48 | with hd.alert( 49 | opened=True, 50 | background_color="neutral-0", 51 | border="1px solid neutral-100", 52 | ): 53 | hd.markdown( 54 | """ 55 | 56 | In general, when rendering lists of components in 57 | loops, always wrap the loop body in `with 58 | hd.scope(loop_id)`, where `loop_id` is unique per loop 59 | iteration. 60 | 61 | When rendering a list of database records by iterating 62 | over the list, the `loop_id` is naturally the primary 63 | key of the record. 64 | 65 | """ 66 | ) 67 | 68 | docs_markdown( 69 | """ 70 | 71 | `scope` is covered more extensively in the [scope reference 72 | documentation](/reference/components/scope). 73 | 74 | """ 75 | ) 76 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/conditional_rendering.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/conditional-rendering") 8 | def conditional_rendering(): 9 | with page() as p: 10 | p.title("# Conditional Rendering") 11 | 12 | hd.markdown( 13 | """ 14 | 15 | So far, we've seen how to render UIs where all 16 | instantiated components are on the screen all the 17 | time. But in general, UIs are rendered differently based 18 | on how the states of components evolve. 19 | 20 | """ 21 | ) 22 | 23 | p.heading("## Basic Example") 24 | 25 | docs_markdown( 26 | """ 27 | 28 | To render UI conditionally, you just use built-in Python 29 | conditionals, like so: 30 | 31 | ```py 32 | ch = hd.checkbox("Check Me") 33 | if ch.checked: 34 | hd.text("It is checked") 35 | ``` 36 | 37 | In this example, the @component(checkbox) component 38 | exposes a `checked` prop that lets us determine if the 39 | checkbox is checked. The value of this prop changes as we 40 | check and uncheck the checkbox in the UI. The value of the 41 | prop effectively *stays synchronized* with the state of 42 | the checkbox in the UI. 43 | 44 | Then, we can render UI conditionally based on the current 45 | value of this prop. 46 | 47 | """ 48 | ) 49 | 50 | p.heading("## Arbitrary UI Under Conditionals") 51 | 52 | docs_markdown( 53 | """ 54 | 55 | We can render any Hyperdiv UI under conditionals: 56 | 57 | ```py 58 | ch = hd.checkbox("Check Me") 59 | if ch.checked: 60 | radios = hd.radios("One", "Two", "Three") 61 | hd.text(radios.value) 62 | else: 63 | with hd.box( 64 | border="1px solid neutral-100", 65 | padding=2, 66 | gap=1, 67 | ): 68 | slider = hd.slider() 69 | hd.text(slider.value) 70 | ``` 71 | 72 | Notice how Hyperdiv remembers the states of the components 73 | under conditionals, across runs in which they may be 74 | absent from the UI. 75 | 76 | """ 77 | ) 78 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/extending_hyperdiv/new_components.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import docs_markdown 5 | 6 | 7 | @router.route("/extending-hyperdiv/new-components") 8 | def new_components(): 9 | with page() as p: 10 | p.title("# Creating New Components") 11 | 12 | docs_markdown( 13 | """ 14 | 15 | In addition to being able to extend existing components 16 | with custom CSS props, you can create simple components 17 | targeting HTML tags that are currently unsupported by core 18 | Hyperdiv. Note that this technique only works when you 19 | don't need to run custom Javascript. If you need custom 20 | Javascript, build a [plugin](/extending-hyperdiv/plugins). 21 | 22 | For example, Hyperdiv does not include a built-in 23 | [`iframe`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) 24 | component, but we can create one: 25 | 26 | ```py 27 | class iframe(hd.Component, hd.Styled): 28 | _tag = "iframe" 29 | 30 | src = hd.Prop(hd.PureString) 31 | 32 | iframe( 33 | src="/assets/iframe-example.html", 34 | border="1px solid neutral", 35 | height=10, 36 | ) 37 | ``` 38 | 39 | We create a new component by inheriting from Hyperdiv's 40 | component base class, @component(Component). Hyperdiv 41 | expects a component's HTML tag to be set using the `_tag` 42 | class variable. In this case the tag is `iframe`, causing 43 | Hyperdiv to render this component in the browser using an 44 | ` 58 | ``` 59 | 60 | Obviously, additional props can be defined, targeting 61 | additional HTML attributes supported by `iframe`. 62 | 63 | """ 64 | ) 65 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/interactivity.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/interactivity") 8 | def interactivity(): 9 | with page() as p: 10 | 11 | p.title("# Interactivity Basics") 12 | 13 | hd.markdown( 14 | """ 15 | 16 | So far we've seen how to render basic components into 17 | non-interactive apps: apps that do nothing when you 18 | interact with the rendered components. 19 | 20 | A core Hyperdiv feature, however, is making the building 21 | of interactive UIs as simple as possible. 22 | 23 | """ 24 | ) 25 | 26 | p.heading("## Basic Examples") 27 | 28 | docs_markdown( 29 | """ 30 | 31 | Here's an interactive app that renders different things 32 | based on the state of a checkbox: 33 | 34 | ```py 35 | checkbox = hd.checkbox("Check Me") 36 | if checkbox.checked: 37 | hd.text("It is checked.") 38 | ``` 39 | 40 | Hyperdiv works by *re-running the app function* whenever a 41 | prop changes. In this case, when the user clicks the 42 | checkbox, the `checked` prop is mutated. Hyperdiv then 43 | re-runs the app function that you passed to 44 | @component(run), which may then generate an updated UI. 45 | 46 | Here's another example: 47 | 48 | ```py 49 | slider = hd.slider() 50 | hd.text(slider.value) 51 | ``` 52 | 53 | When you move the slider, the slider's `value` prop is 54 | mutated accordingly. Hyperdiv re-runs the app, generating 55 | a UI with the slider's new value in the text component. 56 | 57 | """ 58 | ) 59 | 60 | p.heading("## Responding to Events") 61 | 62 | docs_markdown( 63 | """ 64 | 65 | Here's an app that toggles a checkbox when a button is clicked: 66 | 67 | ```py 68 | checkbox = hd.checkbox("Check Me") 69 | if hd.button("Toggle").clicked: 70 | checkbox.checked = not checkbox.checked 71 | ``` 72 | 73 | When the button is clicked, `clicked` is set to 74 | `True`. The app re-runs, generating an updated UI, and 75 | when the run is done the `clicked` prop is automatically 76 | reset back to `False`. 77 | 78 | `clicked` is an "event prop" and we will learn more about 79 | it in the next section. 80 | 81 | """ 82 | ) 83 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/class_attribute_docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | A custom class attribute doc parser that extracts documentation 3 | from all the classes in a set of given directories. This is in lieu of 4 | an acceptable Python built-in solution for documenting class attributes. 5 | 6 | If we have a class definition like: 7 | 8 | class Foo: 9 | 10 | # The foo attribute 11 | foo = 1 12 | 13 | ''' 14 | The bar attribute. 15 | It does bar things. 16 | ''' 17 | bar = 2 18 | 19 | This custom parser will create a mapping like: 20 | 21 | { 22 | Foo: { 23 | 'foo': 'The foo attribute', 24 | 'bar': 'The bar attribute\n It does bar things.' 25 | } 26 | } 27 | 28 | (give or take whitespace). 29 | """ 30 | 31 | import ast 32 | from .docstring_extractor import extract_docstring 33 | from .dirutils import get_files_recursively 34 | from .hyperdiv_module_path import get_hyperdiv_module_path 35 | 36 | 37 | class ClassAttributeVisitor(ast.NodeVisitor): 38 | def __init__(self, source_lines): 39 | self.source_lines = source_lines 40 | self.docs = {} 41 | self.current_class = None 42 | 43 | def visit_ClassDef(self, node): 44 | self.current_class = node.name 45 | self.generic_visit(node) 46 | self.current_class = None 47 | 48 | def visit_Assign(self, node): 49 | if self.current_class: # Only consider class-level attributes 50 | docstring = extract_docstring(self.source_lines, node.lineno - 2) 51 | if docstring: 52 | for item in node.targets: 53 | if isinstance(item, ast.Name): 54 | if self.current_class not in self.docs: 55 | self.docs[self.current_class] = {} 56 | self.docs[self.current_class][item.id] = docstring 57 | 58 | 59 | def extract_class_attribute_docs_from_file(file_path): 60 | with open(file_path, "r", encoding="utf-8") as f: 61 | source_code = f.read() 62 | source_lines = source_code.splitlines() 63 | tree = ast.parse(source_code) 64 | visitor = ClassAttributeVisitor(source_lines) 65 | visitor.visit(tree) 66 | return visitor.docs 67 | 68 | 69 | def extract_class_attribute_docs_from_files(file_paths): 70 | all_docs = {} 71 | for file_path in file_paths: 72 | docs = extract_class_attribute_docs_from_file(file_path) 73 | if docs: # Only add entries for classes that have docs 74 | all_docs.update(docs) 75 | return all_docs 76 | 77 | 78 | def get_class_attribute_docs(): 79 | """ 80 | A dict name -> doc mapping prop names to their docs. Similar to the 81 | above, but restricted to class attributes of type `Prop`. 82 | """ 83 | hyperdiv_path = get_hyperdiv_module_path() 84 | 85 | mixins_path = hyperdiv_path / "component_mixins" 86 | components_path = hyperdiv_path / "components" 87 | 88 | file_paths = get_files_recursively(mixins_path) + get_files_recursively( 89 | components_path 90 | ) 91 | 92 | return extract_class_attribute_docs_from_files(file_paths) 93 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/docstring_extractor.py: -------------------------------------------------------------------------------- 1 | def extract_docstring(source_lines, start_line): 2 | """ 3 | Walks backward from start_line, collecting either the contents of a 4 | multi-line string, or the contents of a possibly multi-line 5 | '#' comment block. In the latter case, the comment block has 6 | to be contiguous, with each line starting with '#'. If it 7 | encounters a blank line, the comment stops there. 8 | """ 9 | comment_lines = [] 10 | 11 | inside_multiline_comment = False 12 | end_char = None # Expected end char for a multi-line comment ('"""' or "'''") 13 | 14 | while start_line >= 0: 15 | line = source_lines[start_line] 16 | 17 | if inside_multiline_comment: 18 | if line.strip().startswith(end_char): 19 | # We encountered the start of a triple-quote 20 | # multi-line string. We append the chunk after the 21 | # triple-quotes. 22 | comment_lines.append(line[line.find(end_char) + len(end_char) :]) 23 | break 24 | else: 25 | # We are inside a multi-line triple-quote 26 | # string. We append the line verbatim. 27 | comment_lines.append(line) 28 | 29 | else: 30 | stripped_line = line.strip() 31 | 32 | if stripped_line == '"""' or stripped_line == "'''": 33 | # We encountered the end of a multi-line 34 | # triple-quote string, where the final 35 | # triple-quote is on a line by itself. 36 | inside_multiline_comment = True 37 | end_char = stripped_line 38 | elif stripped_line.endswith('"""') or stripped_line.endswith("'''"): 39 | # We encountered the end of a multi-line string 40 | # that has content before the ending triple-quote. 41 | end_char = '"""' if stripped_line.endswith('"""') else "'''" 42 | 43 | if stripped_line.startswith(end_char): 44 | # In this case, we have a triple-quote string 45 | # that starts and ends on the same line. 46 | line = line[ 47 | line.find(end_char) + len(end_char) : line.rfind(end_char) 48 | ] 49 | 50 | if line.strip() != "": 51 | comment_lines.append(line) 52 | break 53 | else: 54 | # We encountered the end line of a multi-line 55 | # triple-quote string. We append the content 56 | # before the triple-quote. 57 | inside_multiline_comment = True 58 | comment_lines.append(line[: line.rfind(end_char)]) 59 | elif stripped_line.startswith("#"): 60 | # A normal comment line. 61 | comment_lines.append(line[line.find("#") + 1 :]) 62 | else: 63 | break 64 | 65 | start_line -= 1 66 | 67 | docstring = "\n".join(reversed(comment_lines)) 68 | return docstring 69 | -------------------------------------------------------------------------------- /hyperdiv_docs/menu.py: -------------------------------------------------------------------------------- 1 | from .pages.introduction.overview import overview as introduction_overview 2 | from .pages.introduction.docs_overview import docs_overview 3 | 4 | from .pages.guide.getting_started import getting_started 5 | from .pages.guide.components import components as guide_components 6 | from .pages.guide.interactivity import interactivity 7 | from .pages.guide.component_props import component_props 8 | from .pages.guide.layout import layout 9 | from .pages.guide.conditional_rendering import conditional_rendering 10 | from .pages.guide.loops import loops 11 | from .pages.guide.state import state 12 | from .pages.guide.modular_apps import modular_apps 13 | from .pages.guide.tasks import tasks 14 | from .pages.guide.pages_and_navigation import pages_and_navigation 15 | from .pages.guide.using_the_app_template import using_the_app_template 16 | from .pages.guide.static_assets import static_assets 17 | from .pages.guide.deploying import deploying 18 | from .pages.guide.matplotlib_charts import matplotlib_charts 19 | 20 | from .pages.extending_hyperdiv.overview import overview as extending_overview 21 | from .pages.extending_hyperdiv.plugins import plugins 22 | from .pages.extending_hyperdiv.custom_assets import custom_assets 23 | from .pages.extending_hyperdiv.built_in_components import built_in_components 24 | from .pages.extending_hyperdiv.new_components import new_components 25 | 26 | from .pages.reference.components import components 27 | from .pages.reference.env_variables import env_variables 28 | from .pages.reference.design_tokens import design_tokens 29 | from .pages.reference.prop_types import prop_types 30 | from .pages.reference.icons import icons 31 | from .pages.reference.cli import cli 32 | 33 | menu = { 34 | "Introduction": { 35 | "Hyperdiv Overview": {"href": introduction_overview.path}, 36 | "This Documentation App": {"href": docs_overview.path}, 37 | }, 38 | "Guide": { 39 | "Getting Started": {"href": getting_started.path}, 40 | "Component Basics": {"href": guide_components.path}, 41 | "Interactivity Basics": {"href": interactivity.path}, 42 | "Component Props": {"href": component_props.path}, 43 | "Style & Layout": {"href": layout.path}, 44 | "Conditional Rendering": {"href": conditional_rendering.path}, 45 | "Rendering in Loops": {"href": loops.path}, 46 | "Custom State": {"href": state.path}, 47 | "Modular Apps": {"href": modular_apps.path}, 48 | "Asynchronous Tasks": {"href": tasks.path}, 49 | "Pages & Navigation": {"href": pages_and_navigation.path}, 50 | "Using The App Template": {"href": using_the_app_template.path}, 51 | "Matplotlib Charts": {"href": matplotlib_charts.path}, 52 | "Static Assets": {"href": static_assets.path}, 53 | "Deploying Hyperdiv": {"href": deploying.path}, 54 | }, 55 | "Extending Hyperdiv": { 56 | "Overview": {"href": extending_overview.path}, 57 | "Building Plugins": {"href": plugins.path}, 58 | "Loading Custom Assets": {"href": custom_assets.path}, 59 | "Extending Built-In Components": {"href": built_in_components.path}, 60 | "Creating New Components": {"href": new_components.path}, 61 | }, 62 | "Reference": { 63 | "Hyperdiv API": {"href": components.path}, 64 | "Design Tokens": {"href": design_tokens.path}, 65 | "Prop Types": {"href": prop_types.path}, 66 | "Icons": {"href": icons.path}, 67 | "Environment Variables": {"href": env_variables.path}, 68 | "The Hyperdiv CLI": {"href": cli.path}, 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /hyperdiv_docs/extractor/top_level_docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | A custom global definition doc parser that extracts documentation from 3 | all the global definitions in a set of given directories. 4 | 5 | If we define global variables like: 6 | 7 | # x is the sum of 1 and 2 8 | x = 1 + 2 9 | 10 | ''' 11 | y is the product 12 | of 4 and 5 13 | ''' 14 | y = 4 * 5 15 | 16 | This custom parser will create a mapping like: 17 | 18 | { 19 | 'x': 'x is the sum of 1 and 2', 20 | 'y': 'y is the product\nof 4 and 5' 21 | } 22 | """ 23 | 24 | import symtable 25 | import ast 26 | from .docstring_extractor import extract_docstring 27 | from .dirutils import get_files_recursively 28 | from .hyperdiv_module_path import get_hyperdiv_module_path 29 | 30 | 31 | class TopLevelAssignmentVisitor(ast.NodeVisitor): 32 | def __init__(self, source_lines, sym_table): 33 | self.source_lines = source_lines 34 | self.docs = {} 35 | self.sym_table = sym_table 36 | 37 | def visit_Assign(self, node): 38 | docstring = extract_docstring(self.source_lines, node.lineno - 2) 39 | if docstring: 40 | for item in node.targets: 41 | if isinstance(item, ast.Name): 42 | self.docs[item.id] = docstring 43 | 44 | def generic_visit(self, node): 45 | # Using symtable to detect scoped nodes 46 | if hasattr(node, "name") and isinstance(node.name, str): 47 | try: 48 | if self.sym_table.lookup(node.name).is_namespace(): 49 | # Skip over nodes that introduce a new scope 50 | return 51 | except KeyError: 52 | return 53 | super().generic_visit(node) 54 | 55 | 56 | def extract_top_level_docs_from_file(file_path): 57 | with open(file_path, "r", encoding="utf-8") as f: 58 | source_code = f.read() 59 | sym_table = symtable.symtable(source_code, "", "exec") 60 | tree = ast.parse(source_code) 61 | visitor = TopLevelAssignmentVisitor(source_code.splitlines(), sym_table) 62 | visitor.visit(tree) 63 | 64 | return visitor.docs 65 | 66 | 67 | def extract_top_level_docs_from_files(file_paths): 68 | all_docs = {} 69 | for file_path in file_paths: 70 | docs = extract_top_level_docs_from_file(file_path) 71 | if docs: # Only add entries for classes that have docs 72 | all_docs.update(docs) 73 | return all_docs 74 | 75 | 76 | def get_top_level_docs(): 77 | """ 78 | A dict name -> doc mapping top-level names to their docs in all 79 | Hyperdiv component files. For example if there's a definition like 80 | this: 81 | 82 | # This is a doc 83 | x = 1 84 | 85 | The dict will contain {'x': 'This is a doc'}. 86 | 87 | This dict is used to get the prop type docs from top-level type 88 | definitions like: 89 | 90 | # This is the color type 91 | Color = Optional(DesignToken(tokens.Color)) 92 | 93 | But since it's hard to statically determine if the type of a 94 | top_level definition is HyperdivType, this dict includes all 95 | top-level definitions. 96 | """ 97 | 98 | hyperdiv_path = get_hyperdiv_module_path() 99 | 100 | file_paths = ( 101 | get_files_recursively(hyperdiv_path / "prop_types") 102 | + get_files_recursively(hyperdiv_path / "component_mixins") 103 | + get_files_recursively(hyperdiv_path / "components") 104 | ) 105 | 106 | return extract_top_level_docs_from_files(file_paths) 107 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/reference/icons.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from hyperdiv.icons import icon_names as hyperdiv_icon_names 3 | from ...router import router 4 | from ...page import page 5 | from ...code_examples import docs_markdown 6 | 7 | 8 | @router.route("/reference/icons") 9 | def icons(): 10 | per_page = 100 11 | s = hd.state(page=0) 12 | 13 | with page() as p: 14 | p.title("# Icons") 15 | 16 | docs_markdown( 17 | """ 18 | The following is the list of icons available to a Hyperdiv 19 | app. A prop with type @prop_type(Icon) will accept an icon's 20 | name as a string. The most common use of icons is via 21 | the [icon](/reference/components/icon) and 22 | [icon_button](/reference/components/icon_button) 23 | components, which you can use to render an icon by 24 | calling `icon(icon_name)` or `icon_button(icon_name)`. 25 | 26 | Hover over an icon to see its name in a tooltip. Click 27 | the icon to copy its name to the clipboard. 28 | 29 | Use the search box to narrow to the list of icons whose names 30 | match the search text. 31 | """ 32 | ) 33 | 34 | with hd.hbox(gap=1, wrap="wrap"): 35 | search_box = hd.text_input( 36 | clearable=True, 37 | prefix_icon="search", 38 | grow=10, 39 | ) 40 | icon_family = hd.select( 41 | options=("Outline", "Solid", "All Icons"), 42 | value="Outline", 43 | grow=1, 44 | ) 45 | 46 | if search_box.changed: 47 | s.page = 0 48 | 49 | if icon_family.changed: 50 | s.page = 0 51 | 52 | icon_names = [ 53 | icon_name 54 | for icon_name in hyperdiv_icon_names 55 | if search_box.value in icon_name 56 | and ( 57 | "fill" in icon_name 58 | if icon_family.value == "Solid" 59 | else "fill" not in icon_name 60 | if icon_family.value == "Outline" 61 | else True 62 | ) 63 | ] 64 | 65 | num_pages = (len(icon_names) / per_page) - 1 66 | low = s.page * per_page 67 | high = min(len(icon_names), (s.page + 1) * per_page) 68 | 69 | with hd.hbox(wrap="wrap"): 70 | for icon_name in icon_names[low:high]: 71 | with hd.scope(icon_name): 72 | with hd.tooltip(icon_name) as tooltip: 73 | if hd.icon_button( 74 | icon_name, 75 | cursor="pointer", 76 | padding=0.5, 77 | font_size=1.5, 78 | font_color="neutral-800", 79 | ).clicked: 80 | hd.clipboard().write(icon_name) 81 | tooltip.content = "Copied!" 82 | tooltip.reset_prop_delayed("content", 1) 83 | 84 | with hd.hbox(justify="space-between", align="center"): 85 | with hd.button(disabled=s.page <= 0) as prev_button: 86 | hd.icon("chevron-left") 87 | if prev_button.clicked: 88 | s.page -= 1 89 | 90 | hd.text(f"{s.page+1}/{(len(icon_names)//per_page)+1}") 91 | 92 | with hd.button(disabled=s.page >= num_pages) as next_button: 93 | hd.icon("chevron-right") 94 | if next_button.clicked: 95 | s.page += 1 96 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/extending_hyperdiv/built_in_components.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import docs_markdown 5 | 6 | 7 | @router.route("/extending-hyperdiv/built-in-components") 8 | def built_in_components(): 9 | with page() as p: 10 | p.title("# Extending Built-In Components") 11 | 12 | docs_markdown( 13 | """ 14 | 15 | Hyperdiv built-in components can be extended with new 16 | style props. Also, if a built-in style prop doesn't expose 17 | CSS attributes that you need, you can override/replace 18 | that prop with a prop that does. 19 | 20 | Hyperdiv's prop and type system enables defining props 21 | that are compiled to CSS and applied to the component in 22 | the browser. For example, we can create a new component 23 | type that extends @component(box) with an 24 | [opacity](https://developer.mozilla.org/en-US/docs/Web/CSS/opacity) 25 | prop: 26 | 27 | ```py 28 | class opacity_box(hd.box): 29 | opacity = hd.Prop( 30 | hd.CSSField( 31 | "opacity", 32 | hd.Optional(hd.ClampedFloat(0, 1)) 33 | ), 34 | None 35 | ) 36 | 37 | with opacity_box( 38 | opacity=0.3, 39 | border="1px solid green", 40 | padding=1, 41 | gap=1, 42 | ) as box: 43 | hd.text("A low-opacity box.") 44 | hd.button("A button") 45 | 46 | ``` 47 | 48 | Hyperdiv provides a type, @prop_type(CSSField), that 49 | enables defining new CSS attributes. In the example above, 50 | we define a new prop, `opacity`. Its type, 51 | `CSSField("opacity", hd.Optional(hd.ClampedFloat(0, 1)))`, 52 | specifies that 53 | 54 | 1. this prop will be compiled to CSS 55 | 2. the CSS attribute name is `"opacity"` 56 | 3. the allowed values are floats between 0 and 1. The 57 | value of the prop will be rendered directly in the CSS 58 | `opacity` field. 59 | 60 | The CSS `opacity: 0.3;` will be added to instances of this 61 | component. 62 | 63 | CSS props work like normal Hyperdiv props and can be read 64 | and mutated by Python code. For example, we can control 65 | the box opacity with a slider: 66 | 67 | ```py 68 | class opacity_box(hd.box): 69 | opacity = hd.Prop( 70 | hd.CSSField( 71 | "opacity", 72 | hd.Optional(hd.ClampedFloat(0, 1)) 73 | ), 74 | None 75 | ) 76 | 77 | opacity = hd.slider( 78 | min_value=0, 79 | max_value=1, 80 | value=1, 81 | step=0.01 82 | ) 83 | 84 | with opacity_box( 85 | opacity=opacity.value, 86 | border="1px solid green", 87 | padding=1, 88 | gap=1, 89 | ) as box: 90 | hd.text("Drag the slider to change the opacity.") 91 | hd.button("A button") 92 | 93 | ``` 94 | 95 | Note that when the value of a CSS prop is `None`, its 96 | corresponding CSS field will not be sent to the browser at 97 | all. `None` corresponds to default browser behavior. 98 | 99 | The props defined by @component(Styled), from which most 100 | Hyperdiv components inherit, are defined in this way. 101 | 102 | """ 103 | ) 104 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/reference/prop_types.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...code_examples import docs_markdown 4 | from ...page import page 5 | from ...docs_metadata import get_docs_metadata 6 | 7 | 8 | @router.route("/reference/prop-types") 9 | def prop_types(): 10 | data = get_docs_metadata() 11 | 12 | top_level_types = [] 13 | concrete_types = [] 14 | 15 | for pt in data["prop_types"].values(): 16 | if pt["toplevel"]: 17 | top_level_types.append(pt) 18 | else: 19 | concrete_types.append(pt) 20 | 21 | with page() as p: 22 | p.title("# Prop Types") 23 | 24 | hd.markdown( 25 | """ 26 | Hyperdiv has a custom dynamic type system (unrelated to 27 | Python's [type 28 | annotations](https://docs.python.org/3/library/typing.html)) 29 | for controlling the values that are accepted by component 30 | props. When you assign a value into a prop, and the value 31 | does not match the prop's type, an error will be 32 | raised. This helps catch typos immediately instead of 33 | propagating incorrect values into the browser and causing 34 | hard-to-debug behaviors. 35 | 36 | Hyperdiv types may accept values in multiple formats. The 37 | various formats are transformed into a *canonical form* 38 | that is ultimately stored in the prop. For example, the 39 | type `Size` may accept values like `10`, `"10rem"` or `(10, 40 | "rem")`, but the stored value will always look like `(10, 41 | "rem")`. 42 | """ 43 | ) 44 | 45 | p.heading("## User-Facing State Types") 46 | 47 | docs_markdown( 48 | """ 49 | 50 | You can use the following types to define your own typed 51 | @component(BaseState) components, or adding additional props 52 | to component subclasses. 53 | 54 | """ 55 | ) 56 | 57 | with hd.box(gap=0.5): 58 | for pt in top_level_types: 59 | with hd.scope(pt["name"]): 60 | hd.link( 61 | pt["name"], 62 | href=f"/reference/prop-types/{pt['name']}", 63 | width="fit-content", 64 | ) 65 | 66 | p.heading("## Other Types") 67 | 68 | hd.markdown( 69 | """ 70 | The following is a list of prop types specific to 71 | built-in Hyperdiv components. 72 | """ 73 | ) 74 | 75 | with hd.box(gap=0.5): 76 | for pt in concrete_types: 77 | with hd.scope(pt["name"]): 78 | hd.link( 79 | pt["name"], 80 | href=f"/reference/prop-types/{pt['name']}", 81 | width="fit-content", 82 | ) 83 | 84 | 85 | @router.route("/reference/prop-types/{prop_type}") 86 | def prop_type(prop_type_name): 87 | if prop_type_name == "Icon": 88 | with page() as p: 89 | p.title("# `Icon`") 90 | hd.markdown( 91 | """ 92 | The `Icon` type accepts icon names. 93 | 94 | Head to the [icons reference page](/reference/icons) 95 | to browse the available icons. 96 | """ 97 | ) 98 | return 99 | 100 | data = get_docs_metadata() 101 | if prop_type_name not in data["prop_types"]: 102 | router.render_not_found() 103 | return 104 | 105 | prop_type = data["prop_types"][prop_type_name] 106 | 107 | with page() as p: 108 | p.title(f"# `{prop_type_name}`") 109 | 110 | if prop_type["doc"]: 111 | docs_markdown(prop_type["doc"]) 112 | 113 | if prop_type["is_alias"]: 114 | with hd.box(gap=1): 115 | hd.markdown("### Type Definition:") 116 | hd.markdown( 117 | f"{prop_type_name} = {prop_type['markdown']}", font_family="mono" 118 | ) 119 | -------------------------------------------------------------------------------- /hyperdiv_docs/code_examples.py: -------------------------------------------------------------------------------- 1 | import re 2 | from textwrap import dedent as dedent_text 3 | import hyperdiv as hd 4 | from .demos.counter_plugin import counter 5 | from .demos.leaflet_plugin import leaflet 6 | 7 | 8 | def parse_doc(doc): 9 | chunks = [] 10 | current_chunk = None 11 | lines = dedent_text(doc).split("\n") 12 | 13 | inside_string = False 14 | 15 | def process_current_chunk(): 16 | if current_chunk and current_chunk["content"]: 17 | chunks.append(current_chunk) 18 | 19 | for line in lines: 20 | stripped = line.strip() 21 | if inside_string: 22 | if '"""' in stripped or "'''" in stripped: 23 | inside_string = False 24 | else: 25 | if '"""' in stripped or "'''" in stripped: 26 | inside_string = True 27 | 28 | if stripped.startswith("```py-nodemo"): 29 | process_current_chunk() 30 | current_chunk = dict(type="code-nodemo", content="") 31 | continue 32 | elif stripped.startswith("```py"): 33 | process_current_chunk() 34 | current_chunk = dict(type="code", content="") 35 | continue 36 | elif ( 37 | line.strip().startswith("```") 38 | and current_chunk 39 | and current_chunk["type"] in ("code", "code-nodemo") 40 | ): 41 | process_current_chunk() 42 | current_chunk = dict(type="text", content="") 43 | continue 44 | if not current_chunk: 45 | current_chunk = dict(type="text", content="") 46 | current_chunk["content"] += line + "\n" 47 | 48 | process_current_chunk() 49 | 50 | return chunks 51 | 52 | 53 | def code_example(code, code_to_execute=None): 54 | state = hd.state(error=None) 55 | if code_to_execute is None: 56 | code_to_execute = code 57 | with hd.hbox(wrap="wrap", border="1px solid neutral-50", border_radius="large"): 58 | with hd.box(horizontal_scroll=False, grow=1, basis=0, min_width=18): 59 | hd.code(code, height="100%") 60 | with hd.box(grow=1, basis=0, min_width=18): 61 | with hd.box(padding=1, gap=1): 62 | if state.error: 63 | hd.markdown(f"`Error: {state.error}`", font_color="red") 64 | if hd.button("Reset", size="small").clicked: 65 | state.error = None 66 | else: 67 | try: 68 | exec( 69 | dedent_text(code_to_execute), 70 | globals(), 71 | dict( 72 | counter=counter, 73 | leaflet=leaflet, 74 | ), 75 | ) 76 | except Exception as e: 77 | state.error = str(e) 78 | 79 | 80 | def render_doc_chunks(doc_chunks): 81 | with hd.box(gap=1.5): 82 | for i, doc_chunk in enumerate(doc_chunks): 83 | with hd.scope(i): 84 | if doc_chunk["content"].strip() == "": 85 | continue 86 | if doc_chunk["type"] == "text": 87 | component_pattern = r"@component\((\w+)\)" 88 | component_replacement = r"[`\1`](/reference/components/\1)" 89 | 90 | prop_type_pattern = r"@prop_type\((\w+)\)" 91 | prop_type_replacement = r"[`\1`](/reference/prop-types/\1)" 92 | 93 | design_token_pattern = r"@design_token\((\w+)\)" 94 | design_token_replacement = r"[`\1`](/reference/design-tokens/\1)" 95 | 96 | new_text = re.sub( 97 | component_pattern, component_replacement, doc_chunk["content"] 98 | ) 99 | new_text = re.sub( 100 | prop_type_pattern, prop_type_replacement, new_text 101 | ) 102 | new_text = re.sub( 103 | design_token_pattern, design_token_replacement, new_text 104 | ) 105 | hd.markdown(new_text) 106 | elif doc_chunk["type"] == "code-nodemo": 107 | hd.code(doc_chunk["content"]) 108 | else: 109 | code_example(doc_chunk["content"]) 110 | 111 | 112 | def docs_markdown(doc): 113 | return render_doc_chunks(parse_doc(doc)) 114 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/getting_started.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/getting-started", redirect_from=("/guide",)) 8 | def getting_started(): 9 | with page() as p: 10 | 11 | p.title("# Getting Started") 12 | 13 | hd.markdown( 14 | """ 15 | 16 | This Guide section of the documentation aims to give a 17 | high level taste for how Hyperdiv works and how Hyperdiv 18 | apps are structured. It is not a complete description of 19 | Hyperdiv. The Hyperdiv API is documented in detail in the 20 | Reference section. 21 | 22 | """ 23 | ) 24 | 25 | p.heading("## Hello World") 26 | 27 | hd.markdown("The simplest possible Hyperdiv app:") 28 | 29 | code_example( 30 | """ 31 | import hyperdiv as hd 32 | 33 | def main(): 34 | hd.text("Hello, World!") 35 | 36 | hd.run(main) 37 | """, 38 | code_to_execute=( 39 | """ 40 | hd.text("Hello World") 41 | """ 42 | ), 43 | ) 44 | 45 | hd.markdown( 46 | """ 47 | 48 | The running app is rendered to the right of the code block 49 | (or below, on small screens). This code example pattern is 50 | used throughout this documentation app. 51 | 52 | """ 53 | ) 54 | 55 | with hd.alert( 56 | opened=True, 57 | background_color="neutral-0", 58 | border="1px solid neutral-100", 59 | ): 60 | docs_markdown( 61 | """ 62 | 63 | Note that to avoid repetition, the rest of this 64 | documentation app will often only show the body of the app 65 | function, and assume you'll fill in the rest. Like this: 66 | 67 | ```py 68 | hd.text("Hello World") 69 | ``` 70 | 71 | """ 72 | ) 73 | 74 | p.heading("### Running the App") 75 | 76 | hd.markdown( 77 | """ 78 | 79 | You can click the clipboard icon to the top-right of the 80 | code block to copy the code, and paste it into a new file 81 | called `hello.py`. Then, you can run the app at the 82 | command line with: 83 | 84 | ```sh 85 | python hello.py 86 | ``` 87 | 88 | Hyperdiv will print the local URL where the app is 89 | running, which is typically `http://localhost:8888`. You 90 | can open this URL in a browser to view the running app. 91 | 92 | As you make modifications to the source code, the app will 93 | automatically reload when you save the file. 94 | 95 | """ 96 | ) 97 | 98 | with hd.alert( 99 | opened=True, 100 | background_color="neutral-0", 101 | border="1px solid neutral-100", 102 | ): 103 | hd.markdown( 104 | """ 105 | 106 | When you ship an app to be run locally by users, or 107 | deploy it on a server, you can put it in [production 108 | mode](/reference/env-variables), so that a browser tab 109 | automatically opens with the app in it when you run 110 | the app script. 111 | 112 | """ 113 | ) 114 | 115 | p.heading("### Changing the Port") 116 | 117 | docs_markdown( 118 | """ 119 | 120 | By default, Hyperdiv uses port `8888`, and will fail to 121 | start if this port is occupied. To run the app on a 122 | different port, use the `HD_PORT` environment variable: 123 | 124 | ```sh 125 | HD_PORT=9000 python hello.py 126 | ``` 127 | 128 | """ 129 | ) 130 | 131 | p.heading("## App Structure") 132 | 133 | hd.markdown( 134 | """ 135 | 136 | Getting started with writing a Hyperdiv app has three simple steps: 137 | 1. Import the `hyperdiv` module. 138 | 2. Define the app function. Here, it is called `main`. 139 | 3. Run the app function with `hyperdiv.run(main)`. 140 | 141 | Inside the app function, you lay out the app's UI and 142 | logic, using components exported by the `hyperdiv` 143 | module. 144 | 145 | In this example, when you call `hd.text("Hello, World!")`, 146 | a text component with the contents `"Hello World"` is 147 | automatically rendered in the browser. 148 | 149 | """ 150 | ) 151 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/reference/env_variables.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import docs_markdown 5 | 6 | 7 | @router.route("/reference/env-variables") 8 | def env_variables(): 9 | with page() as p: 10 | p.title("# Environment Variables") 11 | 12 | docs_markdown( 13 | """ 14 | 15 | Hyperdiv's behavior can be controlled with the following 16 | environment variables. 17 | 18 | With the exception of `HD_HOST` and `HD_PORT`, these 19 | environment variables are all boolean variables and take 20 | the values `"1"` or `"true"`, or `"0"` or `"false"`. `"0"` 21 | or `"false"` is equivalent to leaving the environment 22 | variable unset. 23 | 24 | """ 25 | ) 26 | 27 | p.heading("## Core Config") 28 | 29 | docs_markdown( 30 | """ 31 | 32 | ### `HD_HOST` 33 | 34 | This environment variable allows setting the host that a 35 | Hyperdiv app runs on. The value should be a string 36 | representing a valid IP address or hostname, e.g. 37 | `"0.0.0.0"` (default value=`"localhost"`). 38 | 39 | ### `HD_PORT` 40 | 41 | This environment variable allows setting the port that a 42 | Hyperdiv app runs on. The value should be an integer in 43 | valid port range, e.g. `8000`. 44 | 45 | ### `HD_PRODUCTION` 46 | 47 | When set to a true value, this environment disables "debug 48 | mode" in the internal Tornado server, which normally 49 | watches for file changes and auto-reloads the app when a 50 | dependent file is modified, and limits logging output. 51 | 52 | Setting this environment variable causes all the 53 | environment variables in the [Development 54 | Mode](#development-mode) section below to be ignored, 55 | regardless of their values. 56 | 57 | ### `HD_PRODUCTION_LOCAL` 58 | 59 | Works exactly like `HD_PRODUCTION` but in addition: 60 | 61 | 1. It causes @component(run) to automatically open a 62 | browser tab with the app running in it. 63 | 64 | 2. Hyperdiv automatically finds an open port on which to 65 | run the app when `HD_PORT` is left unset. The port 66 | search starts and `8988` and goes upward. 67 | 68 | This environment variable is meant to be set when shipping 69 | apps that users can run locally on their computers. For 70 | example, when distributing an app on 71 | [PyPI](https://pypi.org). 72 | 73 | """ 74 | ) 75 | 76 | p.heading("## Development Mode") 77 | 78 | docs_markdown( 79 | """ 80 | 81 | These environment variables are useful when developing 82 | Hyperdiv itself, and may be useful when improving the 83 | performance of apps. 84 | 85 | ### `HD_DEBUG` 86 | 87 | When set, this environment variable causes Hyperdiv to log 88 | a lot of debugging statements, useful when developing 89 | Hyperdiv itself. This output may be inscrutible to 90 | developers who aren't working on Hyperdiv itself. 91 | 92 | Automatically disabled if `HD_PRODUCTION` is enabled. 93 | 94 | ### `HD_PRINT_OUTPUT` 95 | 96 | Causes Hyperdiv to log each message sent to the 97 | browser. Note that some of these messages can be very 98 | large. In particular, when connecting to an app, the 99 | entire dom is logged to the console. 100 | 101 | Automatically disabled if `HD_DEBUG` is disabled. 102 | 103 | """ 104 | ) 105 | 106 | p.heading("## Setting Environment Variables") 107 | 108 | docs_markdown( 109 | """ 110 | 111 | In bash-like shells, you can set environment variables like this: 112 | 113 | ```sh 114 | export HD_PORT=9000 115 | export HD_PRODUCTION=1 116 | python app.py 117 | ``` 118 | 119 | Using `export` will set the environment variable for the 120 | rest of the terminal session, or until set again. 121 | 122 | You can also set the variable for a single execution of 123 | the app, without exporting it to the session. 124 | 125 | ```sh 126 | HD_PORT=9000 HD_PRODUCTION=1 python start.py 127 | ``` 128 | 129 | In non-bash-like shells, you can use `setenv`, which works 130 | like the `export` command from bash-like shells: 131 | 132 | ```sh 133 | setenv HD_PORT 9000 134 | setenv HD_PRODUCTION 1 135 | python app.py 136 | ``` 137 | 138 | """ 139 | ) 140 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/introduction/overview.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | 5 | 6 | def feature(title): 7 | with hd.box( 8 | gap=1, 9 | padding=1, 10 | border="1px solid neutral-100", 11 | border_radius="large", 12 | background_color="neutral-50", 13 | ) as box: 14 | hd.markdown(f"### {title}", font_color="blue") 15 | return box 16 | 17 | 18 | @router.route("/introduction/overview", redirect_from=("/", "/introduction")) 19 | def overview(): 20 | with page() as p: 21 | p.title("# Hyperdiv Overview") 22 | 23 | hd.markdown( 24 | """ 25 | 26 | Hyperdiv is a Python framework that aims to minimize tool 27 | complexity and the amount of code you have to type when 28 | building a reactive browser UI app. 29 | 30 | """ 31 | ) 32 | 33 | hd.link( 34 | "Jump to Getting Started", 35 | href="/guide/getting-started", 36 | font_color="neutral-100", 37 | padding=1, 38 | align="center", 39 | justify="center", 40 | border="1px solid neutral-100", 41 | border_radius="large", 42 | background_gradient=(30, "pink", "blue"), 43 | ) 44 | 45 | hd.markdown("## Features") 46 | 47 | with feature("Pure Python"): 48 | hd.markdown( 49 | """ 50 | 51 | A Hyperdiv app is written in pure Python, in a single 52 | Python program that seamlessly blends UI and backend 53 | logic. There are no extraneous tools or build steps, 54 | and you can run your app with `python my-app.py`. 55 | 56 | """ 57 | ) 58 | 59 | with feature("Batteries Included"): 60 | hd.markdown( 61 | """ 62 | 63 | Hyperdiv ships with a rich set of built-in UI 64 | components based on the excellent Web Components 65 | library [Shoelace](https://shoelace.style), which 66 | includes forms with a full set of input components, 67 | dialogs, drawers, menus, and more. Thanks to Shoelace, 68 | Hyperdiv apps automatically support light and dark 69 | modes and follow the user's system mode by default. 70 | 71 | Hyperdiv also ships with data table components, charts 72 | based on [Chart.js](https://www.chartjs.org/), and 73 | Markdown support based on 74 | [Mistune](https://mistune.lepture.com) and 75 | [Pygments](https://pygments.org/). 76 | 77 | """ 78 | ) 79 | 80 | hd.markdown("## Technical Features") 81 | 82 | with feature("Immediate Mode"): 83 | hd.markdown( 84 | """ 85 | 86 | Hyperdiv syntax is inspired by immediate mode GUI 87 | frameworks like [Dear 88 | ImGui](https://github.com/ocornut/imgui). Immediate 89 | mode provides a very minimal and terse syntax for 90 | expressing UIs and handling UI events. 91 | 92 | """ 93 | ) 94 | 95 | with feature("Reactive"): 96 | hd.markdown( 97 | """ 98 | 99 | Hyperdiv is implicitly reactive. When application 100 | state changes, Hyperdiv automatically re-runs your app 101 | and updates the UI. 102 | 103 | A core innovation of Hyperdiv's design is the seamless 104 | blending of immediate mode with a reactive state model 105 | that borrows from [React 106 | hooks](https://react.dev/reference/react/hooks) and 107 | [Preact 108 | signals](https://preactjs.com/guide/v10/signals/). 109 | 110 | """ 111 | ) 112 | 113 | with feature("Managed"): 114 | hd.markdown( 115 | """ 116 | 117 | Hyperdiv automatically manages frontend-backend 118 | communication under the hood. You are not exposed to 119 | handling HTTP requests or interacting with a 120 | Websocket. 121 | 122 | Hyperdiv also does not expose users to frontend 123 | technologies like HTML, Javascript, node, rollup/vite, 124 | etc., and has no dependencies on the Javascript 125 | ecosystem. 126 | 127 | """ 128 | ) 129 | 130 | with feature("Over-The-Wire Dom Patching"): 131 | hd.markdown( 132 | """ 133 | 134 | Hyperdiv uses Virtual Dom diffing and sends 135 | minimal virtual Dom patches to the browser, which 136 | applies them to the actual in-browser Dom. 137 | 138 | """, 139 | ) 140 | 141 | with feature("Fixed JS"): 142 | hd.markdown( 143 | """ 144 | 145 | The Javascript code that runs in the browser is a 146 | piece of fixed, generic boilerplate that applies 147 | incoming diffs to the Dom and sends UI events back 148 | to Python. Hyperdiv does not expose the internal 149 | logic of your app, or your raw data, to the 150 | browser. The in-browser JS only receives virtual 151 | Dom diffs and translates them into real Dom 152 | modifications. 153 | 154 | """ 155 | ) 156 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/state.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/state") 8 | def state(): 9 | with page() as p: 10 | p.title("# Custom State") 11 | 12 | hd.markdown( 13 | """ 14 | 15 | So far, we've seen how to interact with the states of 16 | built-in components, but most apps will need to manage 17 | custom user-defined state and data. 18 | 19 | """ 20 | ) 21 | 22 | p.heading("## Using `hd.state`") 23 | 24 | docs_markdown( 25 | """ 26 | 27 | Hyperdiv provides @component(state) for quickly defining 28 | custom state components: 29 | 30 | ```py 31 | state = hd.state(count=0) 32 | hd.text("Count:", state.count) 33 | if hd.button("Increment").clicked: 34 | state.count += 1 35 | ``` 36 | 37 | In this example, we define a custom state component with a 38 | `count` prop that is initially set to `0`. When the button 39 | is clicked, we increment the count. 40 | 41 | @component(state) works by dynamically converting its 42 | kwargs into props, which can be initialized, read, and 43 | mutated just like normal component props. You can define 44 | as many props as you want on a state component: 45 | 46 | ```py 47 | state = hd.state(count=0, name="Steve") 48 | 49 | hd.text("Count:", state.count) 50 | hd.text("Name:", state.name) 51 | 52 | if hd.button("Increment").clicked: 53 | state.count += 1 54 | 55 | if hd.button("Toggle Name").clicked: 56 | state.name = ( 57 | "George" 58 | if state.name == "Steve" 59 | else "Steve" 60 | ) 61 | ``` 62 | 63 | """ 64 | ) 65 | 66 | p.heading("## Change Detection") 67 | 68 | docs_markdown( 69 | """ 70 | 71 | As mentioned earlier, Hyperdiv re-runs the app when a 72 | prop's value is mutated to a different value. Hyperdiv 73 | intercepts all assignments into props and compares the 74 | prop's current value with the new, incoming value, in 75 | order to detect a change. 76 | 77 | When you work with custom state, and you update a custom 78 | state prop, you should always assign a *new value* into 79 | the state, as opposed to internally modifying a current 80 | value. For example: 81 | 82 | ```py 83 | state = hd.state(users=["Steve", "Bob"]) 84 | hd.text(state.users) 85 | if hd.button("Add New User").clicked: 86 | state.users.append("Mary") 87 | ``` 88 | 89 | In this example, Hyperdiv won't be able to detect a change 90 | in the prop `state.users`, because we didn't mutate the 91 | prop. Instead, we internally modified the list stored in 92 | the prop. Even though the list contents changed, the list 93 | itself is the same list. 94 | 95 | This way of modifying a list works: 96 | 97 | ```py 98 | state = hd.state(users=["Steve", "Bob"]) 99 | hd.text(state.users) 100 | if hd.button("Add New User").clicked: 101 | state.users = state.users + ["Mary"] 102 | ``` 103 | 104 | In this example, `state.users + ["Mary"]` creates a *new 105 | list* and assigns it into the prop. When Hyperdiv compares 106 | the old list to the new list, it detects a change because 107 | they are different lists. 108 | 109 | """ 110 | ) 111 | 112 | p.heading("## State Initial Values") 113 | 114 | docs_markdown( 115 | """ 116 | 117 | Note that initial values to state props are recreated 118 | every time Hyperdiv re-runs the app. For example: 119 | 120 | ```py-nodemo 121 | import hyperdiv as hd 122 | 123 | class MyObject: 124 | ... 125 | 126 | def main(): 127 | state = hd.state(my_object=MyObject()) 128 | ... 129 | 130 | hd.run(main) 131 | ``` 132 | 133 | As Hyperdiv re-runs this app, `MyObject()` will be 134 | re-created on every run. 135 | 136 | This is fine in many cases, e.g. when initializing state 137 | with primitive values or basic list/tuple literals, but if 138 | you need the prop value to stay unchanged across runs (for 139 | example if you store a `threading.Lock` object in a prop), 140 | you should use this pattern: 141 | 142 | ```py-nodemo 143 | import hyperdiv as hd 144 | 145 | class MyObject: 146 | ... 147 | 148 | def main(): 149 | state = hd.state(my_object=None) 150 | if state.my_object is None: 151 | state.my_object = MyObject() 152 | ... 153 | 154 | hd.run(main) 155 | ``` 156 | 157 | In this example, `MyObject()` is called only once, on the 158 | first run of the application. On that first run, the 159 | object is created and assigned into `state.my_object`. On 160 | subsequent runs, `if state.my_object is None` will 161 | evaluate to `False` and the object creation will be 162 | skipped. 163 | 164 | """ 165 | ) 166 | 167 | p.heading("## Typed State") 168 | 169 | docs_markdown( 170 | """ 171 | 172 | In addition to @component(state), You can define your own 173 | state classes with typed props. See @component(BaseState). 174 | 175 | """ 176 | ) 177 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/components.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/components") 8 | def components(): 9 | with page() as p: 10 | 11 | p.title("# Component Basics") 12 | 13 | docs_markdown( 14 | """ 15 | 16 | Hyperdiv includes a rich set of [UI 17 | components](/reference/components) from the 18 | [Shoelace](https://shoelace.style) component library, 19 | charts, tables, and markdown. 20 | 21 | """ 22 | ) 23 | 24 | p.heading("## UI Components Overview") 25 | 26 | docs_markdown( 27 | ''' 28 | 29 | To add components to your app, you just instantiate them 30 | with relevant arguments. Here are some examples: 31 | 32 | ```py 33 | hd.checkbox("Check Me", checked=True) 34 | hd.alert("This is an alert", opened=True) 35 | hd.button("Click Me") 36 | hd.radio_buttons("One", "Two", "Three", value="Three") 37 | hd.text_input(placeholder="Enter Some Text") 38 | hd.progress_bar("75%", value=75) 39 | hd.tab_group("Tab 1", "Tab 2", "Tab 3") 40 | ``` 41 | 42 | Hyperdiv provides many more UI components, found in the 43 | [reference](/reference/components). 44 | 45 | Hyperdiv comes bundled with charts and tables, too: 46 | 47 | ```py 48 | hd.bar_chart( 49 | (3, 18, 6), 50 | (20, 1, 24), 51 | x_axis=("Oats", "Corn", "Beef"), 52 | labels=(2022, 2023) 53 | ) 54 | hd.line_chart( 55 | (3, 5, 9, 4, 20, 50, 4, 9), 56 | (1, 8, 4, 31, 30, 15, 80, 20), 57 | labels=("Trend A", "Trend B") 58 | ) 59 | hd.data_table(dict( 60 | Name=("Lisa", "John", "Amy"), 61 | Age=(28, 25, 31) 62 | )) 63 | ``` 64 | 65 | Hyperdiv also includes a Markdown component, based on 66 | [Mistune](https://mistune.lepture.com) and 67 | [Pygments](https://pygments.org). 68 | 69 | ```py 70 | hd.markdown( 71 | """ 72 | ## A Heading 73 | 74 | * Item 1 75 | * Item 2 76 | 77 | ```js 78 | const add = (a, b) => a + b; 79 | ``` 80 | """ 81 | ) 82 | ``` 83 | ''' 84 | ) 85 | 86 | p.heading("## Component Props") 87 | 88 | docs_markdown( 89 | """ 90 | 91 | Props will be covered in subsequent sections of this 92 | guide, but it's worth mentioning here that almost all 93 | Hyperdiv components export *props*. Props are special 94 | component attributes that hold a component's state, and 95 | are at the foundation of how Hyperdiv works. 96 | 97 | For example `checked` is a prop of @component(checkbox), 98 | giving read/write access to the checkbox's checked state. 99 | 100 | Props are initialized by passing a keyword-argument to the 101 | component constructor (e.g. `hd.checkbox(checked=True)` 102 | and accessed via a component attribute with the same name 103 | (e.g. `my_checkbox.checked`). 104 | 105 | """ 106 | ) 107 | 108 | p.heading("## Container Components") 109 | 110 | docs_markdown( 111 | """ 112 | 113 | Some Hyperdiv components, including those based on 114 | Shoelace, are "containers" that can have child components 115 | nested inside them. 116 | 117 | For example: 118 | 119 | ```py 120 | alert = hd.alert(opened=True) 121 | with alert: 122 | hd.plaintext("This is an alert") 123 | ``` 124 | 125 | In this example, @component(alert) is a container, and we 126 | nest a @component(plaintext) component inside it using the 127 | `with` keyword. In Hyperdiv, the `with` keyword is 128 | universally used to nest children inside a container. 129 | 130 | Note that for convenience, @component(alert) accepts a 131 | text label argument and if you pass it, it 132 | automatically creates a child text component with that 133 | label. This pattern is common across Hyperdiv: 134 | 135 | ```py 136 | hd.alert("This is an alert", opened=True) 137 | # Is equivalent to 138 | alert = hd.alert(opened=True) 139 | with alert: 140 | hd.plaintext("This is an alert") 141 | ``` 142 | 143 | """ 144 | ) 145 | 146 | p.heading("### Slots") 147 | 148 | docs_markdown( 149 | """ 150 | 151 | In the example above, when we nested a text component 152 | inside an alert component, we say that we placed the text 153 | component inside the alert's "default slot". 154 | 155 | Many Hyperdiv components export additional, named slots, 156 | which target various internal containers within those 157 | components. 158 | 159 | For example, the @component(button) component exports 160 | `prefix` and `suffix` slots that can be used to nest 161 | prefix and suffix icons within the button: 162 | 163 | ```py 164 | with hd.button() as button: 165 | # Default slot 166 | hd.plaintext("The label") 167 | # 'prefix' slot 168 | hd.icon("gear", slot=button.prefix) 169 | # 'suffix' slot 170 | hd.icon("chevron-right", slot=button.suffix) 171 | ``` 172 | 173 | Child components can be "slotted" into a parent by passing 174 | the parent slot into the child's `slot` prop. 175 | 176 | In many cases, for convenience, components accept keyword 177 | arguments that will automatically fill in those slots. The 178 | example above is equivalent to: 179 | 180 | ```py 181 | hd.button( 182 | "The label", 183 | prefix_icon="gear", 184 | suffix_icon="chevron-right" 185 | ) 186 | ``` 187 | 188 | """ 189 | ) 190 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/matplotlib_charts.py: -------------------------------------------------------------------------------- 1 | from ...router import router 2 | from ...page import page 3 | from ...code_examples import docs_markdown 4 | 5 | 6 | @router.route("/guide/matplotlib-charts") 7 | def matplotlib_charts(): 8 | with page() as p: 9 | p.title("# Matplotlib Charts") 10 | 11 | docs_markdown( 12 | """ 13 | 14 | Hyperdiv does not have a specific component for 15 | [Matplotlib](https://matplotlib.org) charts but a 16 | Matplotlib chart can be added to a Hyperdiv app by 17 | rendering the chart to image bytes and passing the bytes 18 | to the @component(image) component. 19 | 20 | """ 21 | ) 22 | 23 | p.heading("## Basic Use") 24 | 25 | docs_markdown( 26 | """ 27 | 28 | ```py-nodemo 29 | import io 30 | import matplotlib 31 | import matplotlib.pyplot as plt 32 | import hyperdiv as hd 33 | 34 | matplotlib.use("Agg") 35 | 36 | def get_chart_image(fig): 37 | ''' 38 | Renders the chart to png image bytes. 39 | ''' 40 | buf = io.BytesIO() 41 | fig.savefig(buf, format="png") 42 | buf.seek(0) 43 | image_bytes = buf.getvalue() 44 | buf.close() 45 | plt.close(fig) 46 | return image_bytes 47 | 48 | def main(): 49 | # Create a chart: 50 | fig, ax = plt.subplots() 51 | ax.plot((1, 2, 3, 4), (10, 11, 12, 13)) 52 | 53 | # Render the image bytes in the UI: 54 | hd.image(get_chart_image(fig), width=20) 55 | 56 | hd.run(main) 57 | ``` 58 | 59 | Note that `matplotlib.use("Agg")` is important. It tells 60 | Matplotlib to run headless, without depending on a 61 | GUI. Without this setting, attempting to add a Matplotlib 62 | chart to Hyperdiv will fail. 63 | 64 | More on this [here](https://matplotlib.org/stable/users/explain/figure/backends.html#selecting-a-backend). 65 | 66 | """ 67 | ) 68 | 69 | p.heading("## Responding to Theme Mode") 70 | 71 | docs_markdown( 72 | """ 73 | 74 | By default, Matplotlib chart images are rendered on white 75 | background. There's currently no easy way to match the 76 | chart's color scheme to Hyperdiv's theme exactly, but 77 | Matplotlib provides a basic way to render a chart in dark 78 | or light mode. We can then sync the chart's theme mode to 79 | Hyperdiv's theme mode. 80 | 81 | ```py-nodemo 82 | def main(): 83 | theme = hd.theme() 84 | 85 | line_data = ((1, 2, 3, 4), (10, 11, 12, 13)) 86 | 87 | if theme.is_dark: 88 | # Render the matplotlib chart in dark mode: 89 | with plt.style.context("dark_background"): 90 | fig, ax = plt.subplots() 91 | ax.plot(*line_data) 92 | else: 93 | # Render it in light mode: 94 | fig, ax = plt.subplots() 95 | ax.plot(*line_data) 96 | 97 | # Render the image bytes in the UI: 98 | hd.image(get_chart_image(fig), width=20) 99 | ``` 100 | 101 | """ 102 | ) 103 | 104 | p.heading("## Caching Chart Components with `@cached`") 105 | 106 | docs_markdown( 107 | """ 108 | 109 | Using the pattern above, the chart will be re-created on 110 | every unrelated run of the app function. We can use the 111 | @component(cached) decorator to avoid re-creating the 112 | chart on every run. 113 | 114 | ```py-nodemo 115 | @hd.cached 116 | def chart(): 117 | theme = hd.theme() 118 | 119 | line_data = ((1, 2, 3, 4), (10, 11, 12, 13)) 120 | 121 | if theme.is_dark: 122 | with plt.style.context("dark_background"): 123 | fig, ax = plt.subplots() 124 | ax.plot(*line_data) 125 | else: 126 | fig, ax = plt.subplots() 127 | ax.plot(*line_data) 128 | 129 | hd.image(get_chart_image(fig), width=20) 130 | 131 | def main(): 132 | chart() 133 | 134 | state = hd.state(count=0) 135 | if hd.button("Click Me").clicked: 136 | state.count += 1 137 | hd.text(state.count) 138 | ``` 139 | 140 | In this example, when the app first loads, `chart()` is 141 | called and its resulting virtual DOM is cached. 142 | 143 | For demonstration, there's an unrelated click counter on 144 | the page. When we click the `Click Me` button, the app 145 | re-runs but the call to `chart()` does not re-run the 146 | `chart` function, and instead uses its cached virtual DOM. 147 | 148 | Also, when the theme mode is switched between light and 149 | dark, the `chart` function's dependency on theme mode will 150 | be invalidated and the function will re-run, rendering the 151 | chart in the new theme mode. 152 | 153 | """ 154 | ) 155 | 156 | p.heading("## Dynamically Updating Charts") 157 | 158 | docs_markdown( 159 | """ 160 | 161 | We can re-create the chart on demand, with new data: 162 | 163 | ```py-nodemo 164 | @hd.cached 165 | def chart(): 166 | theme = hd.theme() 167 | state = hd.state( 168 | line_data=((1, 2, 3, 4), (10, 11, 12, 13)) 169 | ) 170 | 171 | if theme.is_dark: 172 | with plt.style.context("dark_background"): 173 | fig, ax = plt.subplots() 174 | ax.plot(*state.line_data) 175 | else: 176 | fig, ax = plt.subplots() 177 | ax.plot(*state.line_data) 178 | 179 | hd.image(get_chart_image(fig), width=20) 180 | 181 | if hd.button("Update Chart").clicked: 182 | state.line_data = ((1, 2, 3, 4), (5, 20, 8, 10)) 183 | ``` 184 | 185 | In this example, we store the chart's line data in 186 | @component(state). When the `Update Chart` button is 187 | clicked, an updated chart will be rendered. 188 | """ 189 | ) 190 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/modular_apps.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/modular-apps") 8 | def modular_apps(): 9 | with page() as p: 10 | p.title("# Building Modular Apps") 11 | 12 | docs_markdown( 13 | """ 14 | 15 | If you want to break up your Hyperdiv app into multiple 16 | modular units, you can just use Python functions and 17 | classes. 18 | 19 | """ 20 | ) 21 | 22 | code_example( 23 | """ 24 | import hyperdiv as hd 25 | 26 | def fancy_slider(name, value=0): 27 | with hd.box( 28 | gap=1, 29 | padding=1, 30 | background_color="neutral-50", 31 | border="1px solid neutral-100", 32 | border_radius="large", 33 | ): 34 | hd.markdown(f"### {name}") 35 | slider = hd.slider(value=value) 36 | hd.text(slider.value) 37 | return slider.value 38 | 39 | def my_bar_chart(name1, value1, name2, value2): 40 | hd.bar_chart( 41 | (value1, value2), 42 | x_axis=(name1, name2) 43 | ) 44 | 45 | def main(): 46 | with hd.box(gap=1): 47 | value1 = fancy_slider("A Slider", value=10) 48 | value2 = fancy_slider("Another Slider", value=5) 49 | my_bar_chart("A Slider", value1, "Another Slider", value2) 50 | 51 | hd.run(main) 52 | """, 53 | code_to_execute=( 54 | """ 55 | def fancy_slider(name, value=0): 56 | with hd.box( 57 | gap=1, 58 | padding=1, 59 | background_color="neutral-50", 60 | border="1px solid neutral-100", 61 | border_radius="large", 62 | ): 63 | hd.markdown(f"### {name}") 64 | slider = hd.slider(value=value) 65 | hd.text(slider.value) 66 | return slider.value 67 | 68 | def my_bar_chart(name1, value1, name2, value2): 69 | hd.bar_chart( 70 | (value1, value2), 71 | x_axis=(name1, name2) 72 | ) 73 | 74 | with hd.box(gap=1): 75 | value1 = fancy_slider("A Slider", value=10) 76 | value2 = fancy_slider("Another Slider", value=5) 77 | my_bar_chart("A Slider", value1, "Another Slider", value2) 78 | """ 79 | ), 80 | ) 81 | 82 | hd.markdown( 83 | """ 84 | 85 | In this example, we wrote two functions, `fancy_slider` 86 | and `bar_chart`, that are effectively custom Hyperdiv 87 | components. 88 | 89 | Each call to `fancy_slider()` or `my_bar_chart()` renders 90 | a UI with independent internal state. In the example 91 | above, we call `fancy_slider()` twice, creating two 92 | separate and independent sliders. 93 | 94 | """ 95 | ) 96 | 97 | p.heading("## Encapsulating Custom State") 98 | 99 | hd.markdown( 100 | """ 101 | 102 | Component functions can naturally define their own private 103 | custom state internally: 104 | 105 | """ 106 | ) 107 | 108 | code_example( 109 | """ 110 | import hyperdiv as hd 111 | 112 | def counter(name): 113 | state = hd.state(count=0) 114 | 115 | with hd.box( 116 | padding=1, 117 | gap=1, 118 | border="1px solid neutral-100", 119 | background_color="neutral-50", 120 | border_radius="large" 121 | ): 122 | hd.markdown(f"### {name}") 123 | hd.text("Count:", state.count) 124 | if hd.button("Increment").clicked: 125 | state.count += 1 126 | 127 | def main(): 128 | with hd.box(gap=1): 129 | counter("First Counter") 130 | counter("Second Counter") 131 | 132 | hd.run(main) 133 | """, 134 | code_to_execute=( 135 | """ 136 | 137 | def counter(name): 138 | state = hd.state(count=0) 139 | 140 | with hd.box( 141 | padding=1, 142 | gap=1, 143 | border="1px solid neutral-100", 144 | background_color="neutral-50", 145 | border_radius="large" 146 | ): 147 | hd.markdown(f"### {name}") 148 | hd.text("Count:", state.count) 149 | if hd.button("Increment").clicked: 150 | state.count += 1 151 | 152 | with hd.box(gap=1): 153 | counter("First Counter") 154 | counter("Second Counter") 155 | 156 | """ 157 | ), 158 | ) 159 | 160 | docs_markdown( 161 | """ 162 | 163 | In this example app, we define a component function 164 | `counter` that uses @component(state) to define internal, 165 | custom state. We can call this component function multiple 166 | times, and each call will render a counter UI with its own 167 | independent count. 168 | 169 | """ 170 | ) 171 | 172 | p.heading("## Global State") 173 | 174 | docs_markdown( 175 | """ 176 | 177 | When modularizing our app into several component functions 178 | and classes, and several of those components share custom 179 | state, we normally have to instantiate the state at the 180 | top level and pass it down into all of those components. 181 | 182 | To reduce the burden of having to pass state everywhere it 183 | is needed, Hyperdiv provides the @component(global_state) 184 | decorator, which makes a state component global, so that 185 | all instances of that component share the same underlying 186 | state. 187 | 188 | See @component(global_state) for more. 189 | 190 | """ 191 | ) 192 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/using_the_app_template.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | 5 | 6 | @router.route("/guides/templates") 7 | def using_the_app_template(): 8 | with page() as p: 9 | p.title("# Using the App Template") 10 | 11 | p.heading("## Introduction") 12 | 13 | hd.markdown( 14 | """ 15 | To help quickly build basic apps and tools, Hyperdiv comes 16 | bundled with a template that features: 17 | 18 | * A top bar that can hold 19 | * a top-left logo and title. 20 | * a top-right theme switcher and optional navigation links. 21 | 22 | * A sidebar that can hold an optional navigation menu or any 23 | other components. The sidebar becomes a togglable drawer on 24 | small screens and the responsive threshold can be 25 | customized. 26 | 27 | * A main content area. 28 | 29 | This documentation app is built using the app template. 30 | """ 31 | ) 32 | 33 | p.heading("## Example") 34 | 35 | hd.markdown( 36 | """ 37 | ```py 38 | import hyperdiv as hd 39 | 40 | router = hd.router() 41 | 42 | # Define the pages of the app: 43 | 44 | @router.route("/app-template-demo") 45 | def home(): 46 | hd.markdown("# Welcome") 47 | 48 | 49 | @router.route("/app-template-demo/users") 50 | def users(): 51 | hd.markdown("# Users") 52 | 53 | 54 | def main(): 55 | template = hd.template(logo="/assets/hd-logo-white.svg", title="My App") 56 | 57 | # Sidebar menu linking to the app's pages: 58 | template.add_sidebar_menu( 59 | { 60 | "Home": {"icon": "house", "href": home.path}, 61 | "Users": {"icon": "people", "href": users.path}, 62 | } 63 | ) 64 | 65 | # A topbar contact link: 66 | template.add_topbar_links( 67 | {"Contact": {"icon": "envelope", "href": "mailto:hello@hyperdiv.io"}} 68 | ) 69 | 70 | # Render the active page in the body: 71 | with template.body: 72 | router.run() 73 | 74 | hd.run(main) 75 | ``` 76 | """ 77 | ) 78 | 79 | with hd.link( 80 | href="/app-template-demo", 81 | align="center", 82 | background_color="primary", 83 | font_color="neutral-0", 84 | padding=1, 85 | border_radius="large", 86 | ): 87 | hd.text("Run This App"), 88 | hd.text( 89 | "Click the browser back button to return here.", 90 | font_size="small", 91 | font_color="primary-200", 92 | ) 93 | 94 | p.heading("## Responsive Thresholds") 95 | 96 | hd.markdown( 97 | """ 98 | 99 | In wide browser windows, the sidebar is rendered in-line, 100 | and the topbar links show both the link icon and the link 101 | name. In narrow browser windows, the sidebar is rendered 102 | in a togglable drawer, and the topbar links show only the 103 | link icon, with the link name in a tooltip. 104 | 105 | The threshold when a browser window is considered "wide" 106 | can be controlled with two kwargs: 107 | 108 | * `responsive_threshold`: (Default `1000`.) Controls the 109 | threshold, in pixels, at which the sidebar is rendered 110 | in a drawer vs. inline. 111 | 112 | * `responsive_topbar_links_threshold`: (Default `600`.) 113 | Controls the threshold, in pixels, at which topbar links 114 | are rendered as icons vs. icon + name. 115 | 116 | ```py 117 | template = hd.template( 118 | title="My App", 119 | # When the window is narrower than 1200 pixels, 120 | # render the sidebar in a drawer. 121 | responsive_threshold=1200, 122 | # When the window is narrower than 700 pixels, 123 | # render the topbar links as icon-only. 124 | responsive_topbar_links_threshold=700 125 | ) 126 | ``` 127 | """ 128 | ) 129 | 130 | p.heading("## Custom Sidebar and Topbar Contents") 131 | 132 | hd.markdown( 133 | """ 134 | 135 | The typical usage of the app template is: 136 | 137 | ```py 138 | template = hd.template( 139 | logo="/assets/my-logo.svg", 140 | title="My App", 141 | ) 142 | template.add_sidebar_menu(my_menu) 143 | template.add_topbar_links(my_links) 144 | with template.body: 145 | my_app_body() 146 | ``` 147 | 148 | This will render the logo, along with the app title, in 149 | the top-left of the topbar, and at the top of the sidebar 150 | drawer (on small screens). It will also render a series of 151 | links in the top-right of the topbar. 152 | 153 | However, arbitrary contents can be placed in the sidebar, 154 | topbar title area, and topbar links area. 155 | 156 | ```py 157 | template = hd.template() 158 | # The main app title. 159 | with template.app_title: 160 | hd.text("Custom title") 161 | # The app title at the top of the drawer. 162 | # Relevant only when a drawer is present. 163 | with template.drawer_title: 164 | hd.text("Custom title") 165 | # Custom topbar links content. 166 | with template.topbar_links: 167 | hd.text("Custom topbar") 168 | # Custom sidebar content. 169 | with template.sidebar: 170 | hd.text("Custom sidebar") 171 | ``` 172 | 173 | Note that you can add stuff to the sidebar before or after 174 | you render a menu: 175 | 176 | ```py 177 | template.add_sidebar_menu(my_menu) 178 | with template.sidebar: 179 | hd.text( 180 | ''' 181 | Custom sidebar content, 182 | Rendered, below the menu. 183 | ''' 184 | ) 185 | ``` 186 | """ 187 | ) 188 | 189 | p.heading("## Optional Sidebar") 190 | 191 | hd.markdown( 192 | """ 193 | 194 | The sidebar is optional and you can make a simple app 195 | without a sidebar by passing `sidebar=False`: 196 | 197 | ```py 198 | template = hd.template( 199 | logo="/assets/logo.svg", 200 | title="My App", 201 | sidebar=False 202 | ) 203 | with template.body: 204 | hd.markdown("# Hello") 205 | ``` 206 | 207 | Without a sidebar, there will be no drawer or drawer 208 | toggle button rendered on small screens. 209 | """ 210 | ) 211 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/layout.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import code_example, docs_markdown 5 | 6 | 7 | @router.route("/guide/layout") 8 | def layout(): 9 | with page() as p: 10 | 11 | p.title("# Style & Layout") 12 | 13 | docs_markdown( 14 | """ 15 | 16 | Hyperdiv components are pre-styled and look good out of 17 | the box. In general you don't need to worry about styling 18 | in order to achieve a good-looking app. That said, 19 | Hyperdiv components can be styled using a limited set of 20 | style props based on CSS. 21 | 22 | Hyperdiv also includes a @component(box) component that 23 | can be used to create nested layouts, with control for how 24 | the box's children are laid out. 25 | 26 | """ 27 | ) 28 | 29 | p.heading("## Component Style") 30 | 31 | docs_markdown( 32 | """ 33 | 34 | Hyperdiv components support a limited form of visual 35 | customization, controlling the font size and family, 36 | colors, borders, and sizes of components. For example: 37 | 38 | ```py 39 | hd.text( 40 | "Hello, Green World", 41 | padding=2, 42 | width="fit-content", 43 | font_color="green-700", 44 | background_color="green-200", 45 | border="1px solid green-400", 46 | border_radius="large" 47 | ) 48 | ``` 49 | 50 | The style props are defined by the @component(Styled) 51 | class and any component that inherits from 52 | @component(Styled) can be styled. Almost all Hyperdiv 53 | components inherit from @component(Styled). 54 | 55 | The color constants you see in this example are 56 | documented here: @design_token(Color). 57 | 58 | The syntax of the values accepted by each style prop is 59 | determined by that prop's type. For example, the prop 60 | `border` has type @prop_type(Border), and `width` has 61 | type @prop_type(Size). 62 | 63 | The degree to which components can be styled can vary. For 64 | built-in [Shoelace](https://shoelace.style) components, 65 | the style props may only affect the "outer" container 66 | of those components, as they may consist of an outer 67 | container containing multiple inner containers. 68 | 69 | An approach to fully controlling the styles of Shoelace 70 | components, including the styles of internal containers, 71 | is currently being investigated. 72 | 73 | See @component(Styled) for more. 74 | 75 | """ 76 | ) 77 | 78 | p.heading("## The `box` Component") 79 | 80 | docs_markdown( 81 | """ 82 | 83 | Hyperdiv includes a @component(box) component that 84 | supports nested layouts. We can create a `box` and place 85 | "child" components inside it, using a Python `with` block: 86 | 87 | ```py 88 | with hd.box(): 89 | hd.text("Hello") 90 | hd.text("World") 91 | ``` 92 | 93 | `box` stacks it children vertically. `hbox` is a `box` 94 | whose children are laid out horizontally. (`hbox()` is 95 | equivalent to `box(direction="horizontal")`). 96 | 97 | ```py 98 | with hd.hbox(): 99 | hd.text("Hello") 100 | hd.text("World") 101 | ``` 102 | 103 | """ 104 | ) 105 | 106 | docs_markdown( 107 | """ 108 | 109 | We can insert a gap between the children using the `gap` prop: 110 | 111 | ```py 112 | with hd.hbox(gap=1): 113 | hd.text("Hello") 114 | hd.text("World") 115 | ``` 116 | 117 | """ 118 | ) 119 | 120 | p.heading("## Styling Boxes") 121 | 122 | docs_markdown( 123 | """ 124 | 125 | The @component(box) component inherits from 126 | @component(Styled) and can be styled using those style 127 | props. 128 | 129 | ```py 130 | with hd.box( 131 | width=8, 132 | height=5, 133 | border="1px solid yellow", 134 | background_color="yellow-50", 135 | border_radius=1, 136 | align="center", 137 | justify="center", 138 | ): 139 | hd.text("Hello") 140 | ``` 141 | 142 | """ 143 | ) 144 | 145 | p.heading("## Box Alignment") 146 | 147 | docs_markdown( 148 | """ 149 | 150 | @component(box) inherits from @component(Boxy), which 151 | provides props that let you control how the children of 152 | the box are aligned inside the box. 153 | 154 | A right-justified vertical box: 155 | 156 | ```py 157 | with hd.box( 158 | padding=1, 159 | gap=1, 160 | border="1px solid yellow", 161 | align="end" 162 | ): 163 | hd.button("Button 1") 164 | hd.button("Button 2") 165 | ``` 166 | 167 | A horizontal box with equal space between its children: 168 | 169 | ```py 170 | with hd.hbox( 171 | padding=1, 172 | gap=1, 173 | border="1px solid yellow", 174 | justify="space-between" 175 | ): 176 | hd.button("Button 1") 177 | hd.button("Button 2") 178 | hd.button("Button 3") 179 | ``` 180 | 181 | See @component(box) and @component(Boxy) for more. 182 | 183 | """ 184 | ) 185 | 186 | p.heading("## Nesting") 187 | 188 | docs_markdown( 189 | """ 190 | 191 | Almost any Hyperdiv UI component can be placed inside a 192 | box (except overlay components, like @component(drawer) 193 | and @component(dialog), which are naturally parentless), 194 | and boxes can be nested arbitrarily: 195 | 196 | ```py 197 | with hd.hbox( 198 | gap=1, 199 | padding=1, 200 | justify="center", 201 | border="1px solid red", 202 | ): 203 | with hd.box( 204 | gap=1, 205 | padding=1, 206 | border="1px solid green" 207 | ): 208 | hd.button("Button 1") 209 | hd.button("Button 2") 210 | hd.button("Button 3") 211 | with hd.box( 212 | gap=1, 213 | padding=1, 214 | border="1px solid blue" 215 | ): 216 | hd.button("Button 4") 217 | hd.button("Button 5") 218 | hd.button("Button 6") 219 | ``` 220 | 221 | In this example, we created two side by side columns of 222 | buttons, by nesting two child boxes within a parent 223 | horizontal box, and nesting buttons inside the two child 224 | boxes. 225 | 226 | """ 227 | ) 228 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/reference/design_tokens.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from hyperdiv.design_tokens import ( 3 | Spacing, 4 | Shadow, 5 | BorderRadius, 6 | FontFamily, 7 | FontSize, 8 | FontWeight, 9 | LetterSpacing, 10 | LineHeight, 11 | Color, 12 | ) 13 | from ...router import router 14 | from ...page import page 15 | from ...code_examples import docs_markdown 16 | 17 | token_enums = [ 18 | Spacing, 19 | Shadow, 20 | BorderRadius, 21 | FontFamily, 22 | FontSize, 23 | FontWeight, 24 | LetterSpacing, 25 | LineHeight, 26 | Color, 27 | ] 28 | 29 | 30 | def render_token_enum(token_enum, example_fn): 31 | with page() as p: 32 | p.title(f"# {token_enum.__name__}") 33 | 34 | if token_enum.__doc__: 35 | hd.markdown(token_enum.__doc__) 36 | 37 | with hd.box(gap=1): 38 | for c in list(token_enum): 39 | with hd.scope(c.name): 40 | with hd.hbox( 41 | border_radius="medium", 42 | align="center", 43 | ): 44 | hd.text( 45 | f'"{c.value}"', 46 | font_family="mono", 47 | min_width=8, 48 | ) 49 | with hd.box(): 50 | example_fn(c) 51 | 52 | 53 | @router.route("/reference/design-tokens") 54 | def design_tokens(): 55 | with page() as p: 56 | p.title("# Design Tokens") 57 | 58 | docs_markdown( 59 | """ 60 | Design tokens are sets of constants that can be used to style various 61 | aspects of a component. 62 | 63 | If a prop type includes the type 64 | [DesignToken](/reference/prop-types/DesignToken)(TokenEnum), 65 | it accepts values from the token enum TokenEnum. 66 | 67 | For example, if a prop type includes the type `DesignToken(Spacing)`, 68 | it will accept values like `"small"`, `"x-small"`, `"x-large"`, etc. 69 | 70 | Design tokens are enums exported by the `hyperdiv` 71 | module, and you can alternatively access the token values 72 | via the enum instead of using the string values. 73 | 74 | For example, in a prop with type `DesignToken(Color)`, you 75 | can pass either the string `"red-50"` or the enum field 76 | `hyperdiv.Color.red_50`. For values whose string starts 77 | with a number, for example `"2x-large"`, `"3x-large"` of 78 | @design_token(FontSize), the corresponding enum items are 79 | `FontSize.two_x_large`, `FontSize.three_x_large`, etc. 80 | """ 81 | ) 82 | 83 | with hd.box(gap=1): 84 | for klass in token_enums: 85 | with hd.scope(klass.__name__): 86 | hd.link( 87 | klass.__name__, 88 | href=f"/reference/design-tokens/{klass.__name__}", 89 | width="fit-content", 90 | ) 91 | 92 | 93 | @router.route("/reference/design-tokens/Color") 94 | def design_tokens_color(): 95 | groups = {} 96 | 97 | for color in list(hd.Color): 98 | if "-" in color.value: 99 | name, num = color.value.split("-") 100 | groups.setdefault(name, []) 101 | groups[name].append(num) 102 | 103 | with page() as p: 104 | p.title("# Color") 105 | hd.markdown(hd.Color.__doc__) 106 | 107 | hd.markdown( 108 | """ 109 | The table below lists the Hyperdiv colors and their variations. 110 | 111 | The first, numberless box represents the color without any 112 | variation specified. For example, `"red"` or 113 | `"primary"`. To get a variation on the color, you join its 114 | name and the variation number by a dash (`"-"`). For 115 | example `"red-100"`. 116 | 117 | Colors with unspecified variation, like `"red"` are 118 | typically equivalent to the `600` variation. 119 | """ 120 | ) 121 | 122 | for name, nums in groups.items(): 123 | with hd.scope(name): 124 | with hd.hbox(gap=0.5): 125 | hd.text(f'"{name}"', width=5, shrink=False, font_family="mono") 126 | with hd.hbox(gap=0.5, wrap="wrap"): 127 | hd.box(width=3, background_color=name, shrink=False) 128 | for n in nums: 129 | with hd.scope(n): 130 | font_color = 900 if int(n) < 500 else 100 131 | with hd.box( 132 | shrink=False, 133 | align="center", 134 | width=3, 135 | background_color=f"{name}-{n}", 136 | font_color=f"neutral-{font_color}", 137 | ): 138 | hd.text(n, font_family="mono") 139 | 140 | 141 | @router.route("/reference/design-tokens/Spacing") 142 | def design_tokens_spacing(): 143 | def example_fn(c): 144 | hd.box(width=c.value, height=c.value, background_color="primary") 145 | 146 | render_token_enum(Spacing, example_fn) 147 | 148 | 149 | @router.route("/reference/design-tokens/Shadow") 150 | def design_tokens_shadow(): 151 | def example_fn(c): 152 | hd.box( 153 | width=3, 154 | height=3, 155 | background_color="neutral-50", 156 | shadow=c.value, 157 | ) 158 | 159 | render_token_enum(Shadow, example_fn) 160 | 161 | 162 | @router.route("/reference/design-tokens/BorderRadius") 163 | def design_tokens_border_radius(): 164 | def example_fn(c): 165 | hd.box( 166 | width=5, 167 | height=3, 168 | background_color="primary", 169 | border_radius=c.value, 170 | ) 171 | 172 | render_token_enum(BorderRadius, example_fn) 173 | 174 | 175 | @router.route("/reference/design-tokens/FontFamily") 176 | def design_tokens_font_family(): 177 | def example_fn(c): 178 | hd.text("This is some example text.", font_family=c.value) 179 | 180 | render_token_enum(FontFamily, example_fn) 181 | 182 | 183 | @router.route("/reference/design-tokens/FontSize") 184 | def design_tokens_font_size(): 185 | def example_fn(c): 186 | hd.text("Hello!", font_size=c.value) 187 | 188 | render_token_enum(FontSize, example_fn) 189 | 190 | 191 | @router.route("/reference/design-tokens/FontWeight") 192 | def design_tokens_font_weight(): 193 | def example_fn(c): 194 | with hd.box(): 195 | hd.text("This is sans-serif text.", font_weight=c.value) 196 | hd.text("This is mono text.", font_family="mono", font_weight=c.value) 197 | hd.text("This is serif text.", font_family="serif", font_weight=c.value) 198 | 199 | render_token_enum(FontWeight, example_fn) 200 | 201 | 202 | @router.route("/reference/design-tokens/LetterSpacing") 203 | def design_tokens_letter_spacing(): 204 | def example_fn(c): 205 | hd.text("This is some text.", letter_spacing=c.value) 206 | 207 | render_token_enum(LetterSpacing, example_fn) 208 | 209 | 210 | @router.route("/reference/design-tokens/LineHeight") 211 | def design_tokens_line_height(): 212 | def example_fn(c): 213 | with hd.box(width=5): 214 | hd.text("Some text that wraps around.", line_height=c.value) 215 | 216 | render_token_enum(LineHeight, example_fn) 217 | -------------------------------------------------------------------------------- /hyperdiv_docs/pages/guide/pages_and_navigation.py: -------------------------------------------------------------------------------- 1 | import hyperdiv as hd 2 | from ...router import router 3 | from ...page import page 4 | from ...code_examples import docs_markdown 5 | 6 | 7 | @router.route("/guides/routing") 8 | def pages_and_navigation(): 9 | with page() as p: 10 | p.title("# Pages & Navigation") 11 | 12 | p.heading("## Location-Based Rendering") 13 | 14 | hd.markdown( 15 | """ 16 | 17 | Hyperdiv includes a 18 | [`location`](/reference/components/location) component, 19 | which is used to inspect the current location in the 20 | browser's location bar and render different things based 21 | on location. It can also be used to change the location, 22 | though this is generally not recommended (see below). 23 | 24 | For example, to render different things based on the 25 | location path: 26 | 27 | ```py 28 | loc = hd.location() 29 | 30 | if loc.path == "/foo": 31 | render_foo_page() 32 | elif loc.path == "/bar": 33 | render_bar_page() 34 | ``` 35 | 36 | """ 37 | ) 38 | 39 | docs_markdown( 40 | """ 41 | 42 | To change the location, use the `hyperdiv.location.go()` 43 | method: 44 | 45 | ```py 46 | loc = hd.location() 47 | 48 | if hd.button("Go to /users").clicked: 49 | loc.go("/users") 50 | ``` 51 | """ 52 | ) 53 | 54 | p.heading("## Links") 55 | 56 | docs_markdown( 57 | """ 58 | Note that modifying the location like in the example above 59 | is not typically recommended, because it makes it 60 | difficult for screen readers and other accessibility 61 | devices to detect that the button is a navigation 62 | button. Instead you should use links, either by making 63 | markdown links or using the 64 | [`link`](/reference/components/link) component. 65 | 66 | ```py 67 | hd.link("Users", href="/users") 68 | # Or, a link embedded in markdown: 69 | hd.markdown("[Users](/users)") 70 | ``` 71 | """ 72 | ) 73 | 74 | p.heading("## Router") 75 | 76 | hd.markdown( 77 | """ 78 | Hyperdiv comes with a basic [`router`](/reference/components/router), built on top of 79 | [`location`](/reference/components/location), that makes 80 | it easy to build apps with multiple pages. When using 81 | Router, you write one function for each page, decorating 82 | each function with its path, and then let the router 83 | render the right page based on the current location: 84 | 85 | ```py 86 | import hyperdiv as hd 87 | 88 | router = hd.router() 89 | 90 | # Define the app's pages and their paths: 91 | 92 | @router.route("/") 93 | def home(): 94 | hd.markdown("# Home Page") 95 | 96 | @router.route("/users") 97 | def users(): 98 | hd.markdown("# Users Page") 99 | 100 | def app(): 101 | with hd.box(gap=1): 102 | # A simple navigation menu: 103 | with hd.hbox(gap=1): 104 | hd.link("Home", href="/") 105 | hd.link("Users", href="/users") 106 | 107 | # Let the router render the right page: 108 | router.run() 109 | 110 | hd.run(app) 111 | ``` 112 | """ 113 | ) 114 | 115 | p.heading("## Accessible Navigation") 116 | 117 | hd.markdown( 118 | """ 119 | When building navigation menus using links, they should be 120 | wrapped in [`nav`](/reference/components/nav), to provide 121 | a hint to accessibility devices that the contents of this 122 | component contain a navigation menu. 123 | 124 | To improve the navigation menu in the example above, we 125 | use a `nav` instead of an `hbox`. 126 | 127 | ```py 128 | # A simple navigation menu: 129 | with hd.nav(direction="horizontal", gap=1): 130 | hd.link("Home", href="/") 131 | hd.link("Users", href="/users") 132 | ``` 133 | 134 | `nav` takes the same arguments as 135 | [`box`](/reference/components/box) and behaves identically 136 | to a box but generates a `