From 479b3fe63aa8385d71321d2511a6c1577c83bf29 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 26 Aug 2023 23:40:32 -0700 Subject: [PATCH 01/32] Docs homepage --- docs/overrides/home.html | 27 +++++++++++++++++++ docs/src/assets/css/home.css | 8 ++++++ .../css/extra.css => assets/css/main.css} | 0 .../{static/js/extra.js => assets/js/main.js} | 0 docs/src/index.md | 13 +-------- mkdocs.yml | 4 +-- 6 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 docs/overrides/home.html create mode 100644 docs/src/assets/css/home.css rename docs/src/{static/css/extra.css => assets/css/main.css} (100%) rename docs/src/{static/js/extra.js => assets/js/main.js} (100%) diff --git a/docs/overrides/home.html b/docs/overrides/home.html new file mode 100644 index 00000000..c21f6beb --- /dev/null +++ b/docs/overrides/home.html @@ -0,0 +1,27 @@ +{% extends "main.html" %} + + +{% block content %}{% endblock %} + + +{% block footer %}{% endblock %} + + +{% block tabs %} +{{ super() }} + +
+
+ +

{{ config.site_name }}

+

{{ config.site_description }}

+ + Get Started + + + API Reference + +
+
+{% endblock %} diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css new file mode 100644 index 00000000..2db2c1c9 --- /dev/null +++ b/docs/src/assets/css/home.css @@ -0,0 +1,8 @@ +/* Application header should be static for the landing page */ +.md-header { + position: initial; +} + +img.home-logo { + height: 100px; +} diff --git a/docs/src/static/css/extra.css b/docs/src/assets/css/main.css similarity index 100% rename from docs/src/static/css/extra.css rename to docs/src/assets/css/main.css diff --git a/docs/src/static/js/extra.js b/docs/src/assets/js/main.js similarity index 100% rename from docs/src/static/js/extra.js rename to docs/src/assets/js/main.js diff --git a/docs/src/index.md b/docs/src/index.md index 6b8b3aaa..384ec5b6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,17 +1,6 @@ --- +template: home.html hide: - navigation - toc --- - -![ReactPy Django](https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg){ align=left style=height:40px } - -# ReactPy Django - -{% include-markdown "../../README.md" start="" end="" %} - -{% include-markdown "../../README.md" start="" end="" %} - -## Resources - -{% include-markdown "../../README.md" start="" end="" %} diff --git a/mkdocs.yml b/mkdocs.yml index ae5129bc..7c7a4560 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -93,10 +93,10 @@ extra: provider: mike extra_javascript: - - static/js/extra.js + - assets/js/main.js extra_css: - - static/css/extra.css + - assets/css/main.css watch: - docs From 18ea2ce4564a23482084a145ba126a1fbcd0ddbb Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 27 Aug 2023 03:52:45 -0700 Subject: [PATCH 02/32] add text to homepage --- docs/overrides/home.html | 66 +++++++++++++++++++++++++++++++----- docs/src/assets/css/home.css | 52 ++++++++++++++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 111 insertions(+), 9 deletions(-) diff --git a/docs/overrides/home.html b/docs/overrides/home.html index c21f6beb..62256627 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -10,18 +10,68 @@ {% block tabs %} {{ super() }} -
-
+
+

{{ config.site_name }}

{{ config.site_description }}

- - Get Started - - - API Reference - +
+ + Get Started + + + API Reference + +
+
+ +
+

Create user interfaces from components

+

+ ReactPy lets you build user interfaces out of individual pieces called components. Create your own ReactPy + components like thumbnail, like_button, and video. Then combine + them into entire screens, pages, and apps. +

+

+ Whether you work on your own or with thousands of other developers, using React feels the same. It is + designed to let you seamlessly combine components written by independent people, teams, and + organizations. +

+
+ +
+

Write components with code and markup

+

+ ReactPy components are Python functions. Want to show some content conditionally? Use an + if statement. Displaying a list? Try lists with map(). Learning ReactPy is + learning + programming. +

+
+ +
+

Add interactivity wherever you need it

+

+ ReactPy components receive data and return what should appear on the screen. You can pass them new data in + response to an interaction, like when the user types into an input. ReactPy will then update the screen to + match the new data. +

+

+ You don't have to build your whole page in ReactPy. Add React to your existing HTML page, and render + interactive ReactPy components anywhere on it. +

+
+ +
+

Go full-stack with the Django framework

+

+ ReactPy is a library. It lets you put components together, but it doesn't prescribe how to do routing and + data fetching. To build an entire app with ReactPy, we recommend a backend framework like + Django + or + FastAPI. +

{% endblock %} diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index 2db2c1c9..85ace569 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -3,6 +3,58 @@ position: initial; } +/* Hide markdown area */ +.md-main { + visibility: hidden; + height: 0; +} + +.md-main * { + display: none; +} + +/* Homepage styling */ + img.home-logo { height: 100px; } + +.homepage-row { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 6rem 0; + text-align: center; +} + +.homepage-row.stripe { + background-color: var(--md-primary-fg-color); + color: var(--md-primary-bg-color); +} + +.homepage-row h1 { + max-width: 28rem; + font-size: 52px; + line-height: 1.15; + font-weight: 500; + margin-bottom: 0.55rem; + margin-top: -1rem; +} + +.homepage-row.first h1 { + margin-top: 0.55rem; + margin-bottom: -0.75rem; +} + +.homepage-row p { + max-width: 35rem; + font-size: 21px; + line-height: 1.5; + font-weight: 400; +} + +.homepage-row.first p { + font-size: 32px; + font-weight: 500; +} diff --git a/mkdocs.yml b/mkdocs.yml index 7c7a4560..0936d3a7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -107,7 +107,7 @@ watch: site_name: ReactPy-Django site_author: Archmonger -site_description: React for Django developers. +site_description: It's React, but in Python for Django developers. copyright: Copyright © 2023 Reactive Python repo_url: https://github.com/reactive-python/reactpy-django site_url: https://reactive-python.github.io/reactpy-django From 47760d766f714ff8ea39b0ec34739ef854a579d1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 27 Aug 2023 16:43:44 -0700 Subject: [PATCH 03/32] tons of refactoring --- README.md | 2 +- docs/overrides/home.html | 21 ++- docs/overrides/main.html | 2 +- .../index.md => about/changelog.md} | 0 docs/src/{contribute => about}/code.md | 46 +++++- docs/src/{contribute => about}/docs.md | 0 docs/src/assets/css/home.css | 17 --- docs/src/contribute/running-tests.md | 58 -------- docs/src/get-started/choose-django-app.md | 26 ---- docs/src/get-started/create-component.md | 40 ------ docs/src/get-started/learn-more.md | 17 --- docs/src/get-started/register-view.md | 41 ------ docs/src/get-started/run-webserver.md | 27 ---- docs/src/get-started/use-template-tag.md | 27 ---- .../add-reactpy-to-a-project.md} | 28 ++-- docs/src/learn/your-first-component.md | 133 ++++++++++++++++++ .../src/{features => reference}/components.md | 0 .../src/{features => reference}/decorators.md | 0 docs/src/{features => reference}/hooks.md | 0 docs/src/{features => reference}/settings.md | 2 +- .../{features => reference}/template-tag.md | 0 docs/src/{features => reference}/utils.md | 4 +- mkdocs.yml | 31 ++-- 23 files changed, 227 insertions(+), 295 deletions(-) rename docs/src/{changelog/index.md => about/changelog.md} (100%) rename docs/src/{contribute => about}/code.md (58%) rename docs/src/{contribute => about}/docs.md (100%) delete mode 100644 docs/src/contribute/running-tests.md delete mode 100644 docs/src/get-started/choose-django-app.md delete mode 100644 docs/src/get-started/create-component.md delete mode 100644 docs/src/get-started/learn-more.md delete mode 100644 docs/src/get-started/register-view.md delete mode 100644 docs/src/get-started/run-webserver.md delete mode 100644 docs/src/get-started/use-template-tag.md rename docs/src/{get-started/installation.md => learn/add-reactpy-to-a-project.md} (63%) create mode 100644 docs/src/learn/your-first-component.md rename docs/src/{features => reference}/components.md (100%) rename docs/src/{features => reference}/decorators.md (100%) rename docs/src/{features => reference}/hooks.md (100%) rename docs/src/{features => reference}/settings.md (98%) rename docs/src/{features => reference}/template-tag.md (100%) rename docs/src/{features => reference}/utils.md (85%) diff --git a/README.md b/README.md index 281290e3..31a383e2 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Follow the links below to find out more about this project. - [Documentation](https://reactive-python.github.io/reactpy-django) - [GitHub Discussions](https://github.com/reactive-python/reactpy-django/discussions) - [Discord](https://discord.gg/uNb5P4hA9X) -- [Contributor Guide](https://reactive-python.github.io/reactpy-django/contribute/code/) +- [Contributor Guide](https://reactive-python.github.io/reactpy-django/about/code/) - [Code of Conduct](https://github.com/reactive-python/reactpy-django/blob/main/CODE_OF_CONDUCT.md) diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 62256627..2ff9d19c 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -9,7 +9,22 @@ {% block tabs %} {{ super() }} - +
Go full-stack with the Django framework

ReactPy is a library. It lets you put components together, but it doesn't prescribe how to do routing and data fetching. To build an entire app with ReactPy, we recommend a backend framework like - Django - or - FastAPI. + Django.

diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 0b173292..bc3074e6 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -13,7 +13,7 @@ {% endblock %} {% block outdated %} -You're not viewing the latest version. +You're not viewing the latest official release. Click here to go to latest. diff --git a/docs/src/changelog/index.md b/docs/src/about/changelog.md similarity index 100% rename from docs/src/changelog/index.md rename to docs/src/about/changelog.md diff --git a/docs/src/contribute/code.md b/docs/src/about/code.md similarity index 58% rename from docs/src/contribute/code.md rename to docs/src/about/code.md index a5b52955..e0dfe7d7 100644 --- a/docs/src/contribute/code.md +++ b/docs/src/about/code.md @@ -14,7 +14,7 @@ --- -## Modifying Code +## Creating an environment If you plan to make code changes to this repository, you will need to install the following dependencies first: @@ -38,6 +38,12 @@ Then, by running the command below you can: pip install -e . -r requirements.txt ``` +!!! warning "Pitfall" + + Some of our development dependencies require a C++ compiler, which is not installed by default on Windows. + + If you receive errors related to this during installation, follow the instructions in your console errors. + Finally, to verify that everything is working properly, you can manually run the test webserver. ```bash linenums="0" @@ -47,6 +53,42 @@ python manage.py runserver Navigate to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) to see if the tests are rendering correctly. -## GitHub Pull Request +## Creating a pull request {% include-markdown "../../includes/pr.md" %} + +## Running the full test suite + +!!! note + + This repository uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`. + +By running the command below you can run the full test suite: + +```bash linenums="0" +nox -s test +``` + +Or, if you want to run the tests in the background: + +```bash linenums="0" +nox -s test -- --headless +``` + +## Running Django tests + +If you want to only run our Django tests in your current environment, you can use the following command: + +```bash linenums="0" +cd tests +python manage.py test +``` + +## Running Django test webserver + +If you want to manually run the Django test application, you can use the following command: + +```bash linenums="0" +cd tests +python manage.py runserver +``` diff --git a/docs/src/contribute/docs.md b/docs/src/about/docs.md similarity index 100% rename from docs/src/contribute/docs.md rename to docs/src/about/docs.md diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index 85ace569..5ae08cc1 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -1,20 +1,3 @@ -/* Application header should be static for the landing page */ -.md-header { - position: initial; -} - -/* Hide markdown area */ -.md-main { - visibility: hidden; - height: 0; -} - -.md-main * { - display: none; -} - -/* Homepage styling */ - img.home-logo { height: 100px; } diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md deleted file mode 100644 index fe01be26..00000000 --- a/docs/src/contribute/running-tests.md +++ /dev/null @@ -1,58 +0,0 @@ -## Overview - -

- -You will need to set up a Python environment to run the ReactPy-Django test suite. - -

- ---- - -## Running Tests - -This repository uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`. - -If you plan to run tests, you will need to install the following dependencies first: - -- [Python 3.9+](https://www.python.org/downloads/) -- [Git](https://git-scm.com/downloads) - -Once done, you should clone this repository: - -```bash linenums="0" -git clone https://github.com/reactive-python/reactpy-django.git -cd reactpy-django -pip install -e . -r requirements.txt --upgrade -``` - -## Full Test Suite - -By running the command below you can run the full test suite: - -```bash linenums="0" -nox -s test -``` - -Or, if you want to run the tests in the background: - -```bash linenums="0" -nox -s test -- --headless -``` - -## Django Tests - -If you want to only run our Django tests in your current environment, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py test -``` - -## Django Test Webserver - -If you want to manually run the Django test application, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py runserver -``` diff --git a/docs/src/get-started/choose-django-app.md b/docs/src/get-started/choose-django-app.md deleted file mode 100644 index 1594baa5..00000000 --- a/docs/src/get-started/choose-django-app.md +++ /dev/null @@ -1,26 +0,0 @@ -## Overview - -

- -Set up a **Django Project** with at least one app. - -

- -!!! note - - If you have reached this point, you should have already [installed ReactPy-Django](../get-started/installation.md) through the previous steps. - ---- - -## Deciding which Django App to use - -You will now need to pick at least one **Django app** to start using ReactPy-Django on. - -For the following examples, we will assume the following: - -1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app). -2. You have placed `my_app` directly into your **Django project** folder (`./example_project/my_app`). This is common for small projects. - -??? question "How do I organize my Django project for ReactPy?" - - ReactPy-Django has no project structure requirements. Organize everything as you wish, just like any **Django project**. diff --git a/docs/src/get-started/create-component.md b/docs/src/get-started/create-component.md deleted file mode 100644 index 1f94c308..00000000 --- a/docs/src/get-started/create-component.md +++ /dev/null @@ -1,40 +0,0 @@ -## Overview - -

- -You can let ReactPy know what functions are components by using the `#!python @component` decorator. - -

- ---- - -## Declaring a function as a root component - -You will need a file to start creating ReactPy components. - -We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. - -Within this file, you can define your component functions and then add ReactPy's `#!python @component` decorator. - -=== "components.py" - - {% include-markdown "../../../README.md" start="" end="" %} - -??? question "What should I name my ReactPy files and functions?" - - You have full freedom in naming/placement of your files and functions. - - We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. - - Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](./use-template-tag.md)). So, at minimum your component path needs to be valid to Python's `importlib`. - -??? question "What does the decorator actually do?" - - While not all components need to be decorated, there are a few features this decorator adds to your components. - - 1. The ability to be used as a root component. - - The decorator is required for any component that you want to reference in your Django templates ([_see next step_](./use-template-tag.md)). - 2. The ability to use [hooks](../features/hooks.md). - - The decorator is required on any component where hooks are defined. - 3. Scoped failures. - - If a decorated component generates an exception, then only that one component will fail to render. diff --git a/docs/src/get-started/learn-more.md b/docs/src/get-started/learn-more.md deleted file mode 100644 index 3ee45968..00000000 --- a/docs/src/get-started/learn-more.md +++ /dev/null @@ -1,17 +0,0 @@ -# :confetti_ball: Congratulations :confetti_ball: - -

- -If you followed the previous steps, you have now created a "Hello World" component using ReactPy-Django! - -

- -!!! info "Deep Dive" - - The docs you are reading only covers our Django integration. To learn more, check out one of the following links: - - - [ReactPy-Django Feature Reference](../features/components.md) - - [ReactPy Core Documentation](https://reactpy.dev/docs/guides/creating-interfaces/index.html) - - [Ask Questions on Discord](https://discord.gg/uNb5P4hA9X) - - Additionally, the vast majority of tutorials/guides you find for ReactJS can be applied to ReactPy. diff --git a/docs/src/get-started/register-view.md b/docs/src/get-started/register-view.md deleted file mode 100644 index 472d722b..00000000 --- a/docs/src/get-started/register-view.md +++ /dev/null @@ -1,41 +0,0 @@ -## Overview - -

- -Render your template containing your ReactPy component using a Django view. - -

- -!!! Note - - We assume you have [created a Django View](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) before, but we have included a simple example below. - ---- - -## Creating a Django view and URL path - -Within your **Django app**'s `views.py` file, you will need to create a function to render the HTML template containing your ReactPy components. - -In this example, we will create a view that renders `my-template.html` ([_from the previous step_](./use-template-tag.md)). - -=== "views.py" - - ```python - {% include "../../python/example/views.py" %} - ``` - -We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view). - -=== "urls.py" - - ```python - {% include "../../python/example/urls.py" %} - ``` - -??? question "Which urls.py do I add my views to?" - - For simple **Django projects**, you can easily add all of your views directly into the **Django project's** `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file. - - Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**. - - Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/#include) to link it all together. diff --git a/docs/src/get-started/run-webserver.md b/docs/src/get-started/run-webserver.md deleted file mode 100644 index cb4f87f1..00000000 --- a/docs/src/get-started/run-webserver.md +++ /dev/null @@ -1,27 +0,0 @@ -## Overview - -

- -Run a webserver to display your Django view. - -

- ---- - -## Viewing your component using a webserver - -To test your new Django view, run the following command to start up a development webserver. - -```bash linenums="0" -python manage.py runserver -``` - -Now you can navigate to your **Django project** URL that contains a ReactPy component, such as [`http://127.0.0.1:8000/example/`](http://127.0.0.1:8000/example/) ([_from the previous step_](./register-view.md)). - -If you copy-pasted our example component, you will now see your component display "Hello World". - -!!! warning "Pitfall" - - Do not use `manage.py runserver` for production. - - This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/). diff --git a/docs/src/get-started/use-template-tag.md b/docs/src/get-started/use-template-tag.md deleted file mode 100644 index 8ef210fd..00000000 --- a/docs/src/get-started/use-template-tag.md +++ /dev/null @@ -1,27 +0,0 @@ -## Overview - -

- -Decide where the component will be displayed by using our template tag. - -

- ---- - -## Embedding a component in a template - -{% include-markdown "../../../README.md" start="" end="" %} - -Additionally, you can pass in `args` and `kwargs` into your component function. After reading the code below, pay attention to how the function definition for `hello_world` ([_from the previous step_](./create-component.md)) accepts a `recipient` argument. - -=== "my-template.html" - - {% include-markdown "../../../README.md" start="" end="" %} - -{% include-markdown "../features/template-tag.md" start="" end="" %} - -{% include-markdown "../features/template-tag.md" start="" end="" %} - -??? question "Where is my templates folder?" - - If you do not have a `templates` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). diff --git a/docs/src/get-started/installation.md b/docs/src/learn/add-reactpy-to-a-project.md similarity index 63% rename from docs/src/get-started/installation.md rename to docs/src/learn/add-reactpy-to-a-project.md index be727947..0c9cafa1 100644 --- a/docs/src/get-started/installation.md +++ b/docs/src/learn/add-reactpy-to-a-project.md @@ -2,7 +2,7 @@

-[ReactPy-Django](https://github.com/reactive-python/reactpy-django) can be used to add used to add [ReactPy](https://github.com/reactive-python/reactpy) support to an existing **Django project**. Minimal configuration is required to get started. +If you want to add some interactivity to your existing **Django project**, you don't have to rewrite it in ReactPy. Use [ReactPy-Django](https://github.com/reactive-python/reactpy-django) to add [ReactPy](https://github.com/reactive-python/reactpy) to your existing stack, and render interactive components anywhere.

@@ -16,13 +16,15 @@ ## Step 1: Install from PyPI +Run the following command to install [`reactpy-django`](https://pypi.org/project/reactpy-django/) in your Python environment. + ```bash linenums="0" pip install reactpy-django ``` -## Step 2: Configure [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) +## Step 2: Configure `settings.py` -In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS). +Add `reactpy_django` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) file. === "settings.py" @@ -50,11 +52,11 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt ??? note "Configure ReactPy settings (Optional)" - {% include "../features/settings.md" start="" end="" %} + {% include "../reference/settings.md" start="" end="" %} -## Step 3: Configure [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) +## Step 3: Configure `urls.py` -Add ReactPy HTTP paths to your `urlpatterns`. +Add ReactPy HTTP paths to your `urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) file. === "urls.py" @@ -62,9 +64,9 @@ Add ReactPy HTTP paths to your `urlpatterns`. {% include "../../python/configure-urls.py" %} ``` -## Step 4: Configure [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) +## Step 4: Configure `asgi.py` -Register ReactPy's Websocket using `REACTPY_WEBSOCKET_ROUTE`. +Register ReactPy's WebSocket using `REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) file. === "asgi.py" @@ -92,7 +94,7 @@ Register ReactPy's Websocket using `REACTPY_WEBSOCKET_ROUTE`. ## Step 5: Run database migrations -Run Django's database migrations to initialize ReactPy-Django's database table. +Run Django's [`migrate` command](https://docs.djangoproject.com/en/dev/topics/migrations/) to initialize ReactPy-Django's database table. ```bash linenums="0" python manage.py migrate @@ -100,19 +102,19 @@ python manage.py migrate ## Step 6: Check your configuration -Run Django's check command to verify if ReactPy was set up correctly. +Run Django's [`check` command](https://docs.djangoproject.com/en/dev/ref/django-admin/#check) to verify if ReactPy was set up correctly. ```bash linenums="0" python manage.py check ``` -## Step 7: Create your first component! +## Step 7: Create your first component -The [following steps](./choose-django-app.md) will show you how to create your first ReactPy component. +The [next step](./your-first-component.md) will show you how to create your first ReactPy component. Prefer a quick summary? Read the **At a Glance** section below. -!!! info "At a Glance" +!!! info "At a Glance: Your First Component" **`my_app/components.py`** diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md new file mode 100644 index 00000000..e40c5224 --- /dev/null +++ b/docs/src/learn/your-first-component.md @@ -0,0 +1,133 @@ +## Overview + +

+ +Components are one of the core concepts of ReactPy. They are the foundation upon which you build user interfaces (UI), which makes them the perfect place to start your journey! + +

+ +!!! note + + If you have reached this point, you should have already [installed ReactPy-Django](../get-started/add-reactpy-to-a-project.md) through the previous steps. + +--- + +## Selecting a Django App + +You will now need to pick at least one **Django app** to start using ReactPy-Django on. + +For the following examples, we will assume the following: + +1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app). +2. You have placed `my_app` directly into your **Django project** folder (`./example_project/my_app`). This is common for small projects. + +??? question "How do I organize my Django project for ReactPy?" + + ReactPy-Django has no project structure requirements. Organize everything as you wish, just like any **Django project**. + +## Defining a component + +You will need a file to start creating ReactPy components. + +We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. + +Within this file, you can define your component functions and then add ReactPy's `#!python @component` decorator. + +=== "components.py" + + {% include-markdown "../../../README.md" start="" end="" %} + +??? question "What should I name my ReactPy files and functions?" + + You have full freedom in naming/placement of your files and functions. + + We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. + + Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](#embedding-in-a-template)). So, at minimum your component path needs to be valid to Python's `importlib`. + +??? question "What does the decorator actually do?" + + While not all components need to be decorated, there are a few features this decorator adds to your components. + + 1. The ability to be used as a root component. + - The decorator is required for any component that you want to reference in your Django templates ([_see next step_](#embedding-in-a-template)). + 2. The ability to use [hooks](../reference/hooks.md). + - The decorator is required on any component where hooks are defined. + 3. Scoped failures. + - If a decorated component generates an exception, then only that one component will fail to render. + +## Embedding in a template + +{% include-markdown "../../../README.md" start="" end="" %} + +Additionally, you can pass in `args` and `kwargs` into your component function. After reading the code below, pay attention to how the function definition for `hello_world` ([_from the previous step_](#defining-a-component)) accepts a `recipient` argument. + +=== "my-template.html" + + {% include-markdown "../../../README.md" start="" end="" %} + +{% include-markdown "../reference/template-tag.md" start="" end="" %} + +{% include-markdown "../reference/template-tag.md" start="" end="" %} + +??? question "Where is my templates folder?" + + If you do not have a `templates` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). + +## Setting up a Django view + +Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my-template.html` ([_from the previous step_](#embedding-in-a-template)). + +=== "views.py" + + ```python + {% include "../../python/example/views.py" %} + ``` + +We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) and define what URL it should be accessible at. + +=== "urls.py" + + ```python + {% include "../../python/example/urls.py" %} + ``` + +??? question "Which urls.py do I add my views to?" + + For simple **Django projects**, you can easily add all of your views directly into the **Django project's** `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file. + + Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**. + + Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/#include) to link it all together. + +## Viewing your component + +To test your new Django view, run the following command to start up a development webserver. + +```bash linenums="0" +python manage.py runserver +``` + +Now you can navigate to your **Django project** URL that contains a ReactPy component, such as [`http://127.0.0.1:8000/example/`](http://127.0.0.1:8000/example/) ([_from the previous step_](#setting-up-a-django-view)). + +If you copy-pasted our example component, you will now see your component display "Hello World". + +!!! warning "Pitfall" + + Do not use `manage.py runserver` for production. + + This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/). + +## Learn more + +**Congratulations!** If you followed the previous steps, you have now created a "Hello World" component using ReactPy-Django! + +!!! info "Deep Dive" + + The docs you are reading only covers our Django integration. To learn more, check out one of the following links: + + - [ReactPy-Django Feature Reference](../reference/components.md) + - [ReactPy Core Documentation](https://reactpy.dev/docs/guides/creating-interfaces/index.html) + - [Ask Questions on Discord](https://discord.gg/uNb5P4hA9X) + + Additionally, the vast majority of tutorials/guides you find for ReactJS can be applied to ReactPy. diff --git a/docs/src/features/components.md b/docs/src/reference/components.md similarity index 100% rename from docs/src/features/components.md rename to docs/src/reference/components.md diff --git a/docs/src/features/decorators.md b/docs/src/reference/decorators.md similarity index 100% rename from docs/src/features/decorators.md rename to docs/src/reference/decorators.md diff --git a/docs/src/features/hooks.md b/docs/src/reference/hooks.md similarity index 100% rename from docs/src/features/hooks.md rename to docs/src/reference/hooks.md diff --git a/docs/src/features/settings.md b/docs/src/reference/settings.md similarity index 98% rename from docs/src/features/settings.md rename to docs/src/reference/settings.md index 00662976..1fe669dd 100644 --- a/docs/src/features/settings.md +++ b/docs/src/reference/settings.md @@ -29,7 +29,7 @@ These are ReactPy-Django's default settings values. You can modify these values | `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function. | | `REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `AuthMiddlewareStack` and...
2. You are using Django's `AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `backend` attribute. | | `REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the webserver to process web traffic while during ReactPy rendering.
Vastly improves throughput with web servers such as `hypercorn` and `uvicorn`. | -| `REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing.
You can use the `host` argument in your [template tag](../features/template-tag.md#component) as a manual override. | +| `REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing.
You can use the `host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. | | `REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. | | `REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. | | `REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. | diff --git a/docs/src/features/template-tag.md b/docs/src/reference/template-tag.md similarity index 100% rename from docs/src/features/template-tag.md rename to docs/src/reference/template-tag.md diff --git a/docs/src/features/utils.md b/docs/src/reference/utils.md similarity index 85% rename from docs/src/features/utils.md rename to docs/src/reference/utils.md index 9ba8e312..0b1632ca 100644 --- a/docs/src/features/utils.md +++ b/docs/src/reference/utils.md @@ -60,6 +60,6 @@ You should always call `register_component` within a Django [`AppConfig.ready()` For security reasons, ReactPy does not allow non-registered components to be root components. However, all components contained within Django templates are automatically considered root components. - You only need to use this function if your host application does not contain any HTML templates that [reference](../features/template-tag.md#component) your components. + You only need to use this function if your host application does not contain any HTML templates that [reference](../reference/template-tag.md#component) your components. - A common scenario where this is needed is when you are modifying the [template tag `host = ...` argument](../features/template-tag.md#component) in order to configure a dedicated Django application as a rendering server for ReactPy. On this dedicated rendering server, you would need to manually register your components. + A common scenario where this is needed is when you are modifying the [template tag `host = ...` argument](../reference/template-tag.md#component) in order to configure a dedicated Django application as a rendering server for ReactPy. On this dedicated rendering server, you would need to manually register your components. diff --git a/mkdocs.yml b/mkdocs.yml index 0936d3a7..ec1cc22b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,28 +2,22 @@ nav: - Home: index.md - Get Started: - - Install ReactPy-Django: get-started/installation.md - - Choose a Django App: get-started/choose-django-app.md - - Create a Component: get-started/create-component.md - - Use the Template Tag: get-started/use-template-tag.md - - Register a View: get-started/register-view.md - - Run the Webserver: get-started/run-webserver.md - - Learn More: get-started/learn-more.md + - Add ReactPy to a Project: learn/add-reactpy-to-a-project.md + - Your First Component: learn/your-first-component.md - Reference: - - Components: features/components.md - - Hooks: features/hooks.md - - Decorators: features/decorators.md - - Utilities: features/utils.md - - Template Tag: features/template-tag.md - - Settings: features/settings.md + - Components: reference/components.md + - Hooks: reference/hooks.md + - Decorators: reference/decorators.md + - Utilities: reference/utils.md + - Template Tag: reference/template-tag.md + - Settings: reference/settings.md - About: - Contribute: - - Code: contribute/code.md - - Docs: contribute/docs.md - - Running Tests: contribute/running-tests.md + - Code: about/code.md + - Docs: about/docs.md - GitHub Discussions: https://github.com/reactive-python/reactpy-django/discussions - Discord: https://discord.gg/uNb5P4hA9X - - Changelog: changelog/index.md + - Changelog: about/changelog.md theme: name: material @@ -97,6 +91,7 @@ extra_javascript: extra_css: - assets/css/main.css + - assets/css/home.css watch: - docs @@ -107,7 +102,7 @@ watch: site_name: ReactPy-Django site_author: Archmonger -site_description: It's React, but in Python for Django developers. +site_description: It's React, but in Python. Now for Django developers. copyright: Copyright © 2023 Reactive Python repo_url: https://github.com/reactive-python/reactpy-django site_url: https://reactive-python.github.io/reactpy-django From 1b6236970c21ccfef87cbb16052effb089aa1ff1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 27 Aug 2023 18:19:38 -0700 Subject: [PATCH 04/32] refactor CSS --- docs/overrides/home.html | 8 +- docs/src/assets/css/admonition.css | 124 +++++++++ docs/src/assets/css/button.css | 28 ++ docs/src/assets/css/code.css | 36 +++ docs/src/assets/css/main.css | 322 +--------------------- docs/src/assets/css/sidebar.css | 114 ++++++++ docs/src/assets/css/table-of-contents.css | 37 +++ docs/src/learn/your-first-component.md | 2 +- mkdocs.yml | 5 + 9 files changed, 354 insertions(+), 322 deletions(-) create mode 100644 docs/src/assets/css/admonition.css create mode 100644 docs/src/assets/css/button.css create mode 100644 docs/src/assets/css/code.css create mode 100644 docs/src/assets/css/sidebar.css create mode 100644 docs/src/assets/css/table-of-contents.css diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 2ff9d19c..34bf6989 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -35,7 +35,7 @@

{{ config.site_name }}

Get Started - + API Reference
@@ -59,9 +59,9 @@

Create user interfaces from components

Write components with code and markup

ReactPy components are Python functions. Want to show some content conditionally? Use an - if statement. Displaying a list? Try lists with map(). Learning ReactPy is - learning - programming. + if statement. Displaying a list? Try using + list comprehension. + Learning ReactPy is learning programming.

diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css new file mode 100644 index 00000000..206d637e --- /dev/null +++ b/docs/src/assets/css/admonition.css @@ -0,0 +1,124 @@ +/* General admonition styling */ +/* TODO: Write this in a way that supports the light theme */ +[data-md-color-scheme="slate"] .md-typeset details, +[data-md-color-scheme="slate"] .md-typeset .admonition { + border-color: transparent !important; +} + +.md-typeset :is(.admonition, details) { + margin: 0.55em 0; +} + +.md-typeset .admonition { + font-size: 0.7rem; +} + +.md-typeset .admonition:focus-within, +.md-typeset details:focus-within { + box-shadow: var(--md-shadow-z1) !important; +} + +/* Colors for "summary" admonition */ +[data-md-color-scheme="slate"] .md-typeset .admonition.summary { + background: #353a45; + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +[data-md-color-scheme="slate"] .md-typeset .summary .admonition-title { + font-size: 1rem; + background: transparent; + padding-left: 0.6rem; + padding-bottom: 0; +} + +[data-md-color-scheme="slate"] .md-typeset .summary .admonition-title:before { + display: none; +} + +[data-md-color-scheme="slate"] .md-typeset .admonition.summary { + border-color: #ffffff17 !important; +} + +/* Colors for "note" admonition */ +[data-md-color-scheme="slate"] .md-typeset .admonition.note { + background: rgb(43 110 98/ 0.2); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +[data-md-color-scheme="slate"] .md-typeset .note .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(68 172 153); +} + +[data-md-color-scheme="slate"] .md-typeset .note .admonition-title:before { + font-size: 1.1rem; + background-color: rgb(68 172 153); +} + +.md-typeset .note > .admonition-title:before, +.md-typeset .note > summary:before { + -webkit-mask-image: var(--md-admonition-icon--abstract); + mask-image: var(--md-admonition-icon--abstract); +} + +/* Colors for "warning" admonition */ +[data-md-color-scheme="slate"] .md-typeset .admonition.warning { + background: rgb(182 87 0 / 0.2); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +[data-md-color-scheme="slate"] .md-typeset .warning .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(219 125 39); +} + +[data-md-color-scheme="slate"] .md-typeset .warning .admonition-title:before { + font-size: 1.1rem; + background-color: rgb(219 125 39); +} + +/* Colors for "info" admonition */ +[data-md-color-scheme="slate"] .md-typeset .admonition.info { + background: rgb(43 52 145 / 0.2); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +[data-md-color-scheme="slate"] .md-typeset .info .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(136 145 236); +} + +[data-md-color-scheme="slate"] .md-typeset .info .admonition-title:before { + font-size: 1.1rem; + background-color: rgb(136 145 236); +} + +/* Colors for "example" admonition */ +[data-md-color-scheme="slate"] .md-typeset .admonition.example { + background: rgb(94 104 126); + border-radius: 0.4rem; +} + +[data-md-color-scheme="slate"] .md-typeset .example .admonition-title { + background: rgb(78 87 105); + color: rgb(246 247 249); +} + +[data-md-color-scheme="slate"] .md-typeset .example .admonition-title:before { + background-color: rgb(246 247 249); +} + +[data-md-color-scheme="slate"] .md-typeset .admonition.example code { + background: transparent; + color: #fff; +} diff --git a/docs/src/assets/css/button.css b/docs/src/assets/css/button.css new file mode 100644 index 00000000..83c28a6e --- /dev/null +++ b/docs/src/assets/css/button.css @@ -0,0 +1,28 @@ +.md-typeset .md-button { + border-color: #404756; + border-radius: 9999px; + color: #fff; + transition: color 125ms, background-color 125ms, border-color 125ms, + transform 125ms; +} + +.md-typeset .md-button:focus, +.md-typeset .md-button:hover { + border-color: #404756; + background: rgba(78, 87, 105, 0.05); +} + +.md-typeset .md-button.md-button--primary { + border-color: transparent; + background: var(--reactpy-color-dark); +} + +.md-typeset .md-button.md-button--primary:focus, +.md-typeset .md-button.md-button--primary:hover { + border-color: transparent; + background-color: var(--reactpy-color-darker); +} + +.md-typeset .md-button:focus { + transform: scale(0.98); +} diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css new file mode 100644 index 00000000..4da68750 --- /dev/null +++ b/docs/src/assets/css/code.css @@ -0,0 +1,36 @@ +/* Code blocks */ +.md-typeset pre > code { + border-radius: 16px; +} + +.md-typeset .highlighttable .linenos { + max-height: var(--code-max-height); + overflow: hidden; +} + +.md-typeset .tabbed-block .highlighttable code { + border-radius: 0; +} + +.md-typeset .tabbed-block { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + overflow: hidden; +} + +.js .md-typeset .tabbed-labels { + background: var(--tabbed-labels-color); + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.md-typeset .tabbed-labels > label { + font-weight: 400; + font-size: 0.7rem; + padding-top: 0.55em; + padding-bottom: 0.35em; +} + +.md-typeset pre > code { + max-height: var(--code-max-height); +} diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css index d3967666..72a078bd 100644 --- a/docs/src/assets/css/main.css +++ b/docs/src/assets/css/main.css @@ -16,290 +16,15 @@ --md-typeset-a-color: #00b0f0; --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); --tabbed-labels-color: rgb(52 58 70); + --reactpy-color: #58b962; + --reactpy-color-dark: #42914a; + --reactpy-color-darker: #34743b; } [data-md-color-scheme="default"] { --tabbed-labels-color: #7d829e26; } -/* General admonition styling */ -/* TODO: Write this in a way that supports the light theme */ -[data-md-color-scheme="slate"] .md-typeset details, -[data-md-color-scheme="slate"] .md-typeset .admonition { - border-color: transparent !important; -} - -.md-typeset :is(.admonition, details) { - margin: 0.55em 0; -} - -.md-typeset .admonition { - font-size: 0.7rem; -} - -.md-typeset .admonition:focus-within, -.md-typeset details:focus-within { - box-shadow: var(--md-shadow-z1) !important; -} - -/* Colors for "summary" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.summary { - background: #353a45; - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .summary .admonition-title { - font-size: 1rem; - background: transparent; - padding-left: 0.6rem; - padding-bottom: 0; -} - -[data-md-color-scheme="slate"] .md-typeset .summary .admonition-title:before { - display: none; -} - -[data-md-color-scheme="slate"] .md-typeset .admonition.summary { - border-color: #ffffff17 !important; -} - -/* Colors for "note" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.note { - background: rgb(43 110 98/ 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .note .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(68 172 153); -} - -[data-md-color-scheme="slate"] .md-typeset .note .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(68 172 153); -} - -.md-typeset .note > .admonition-title:before, -.md-typeset .note > summary:before { - -webkit-mask-image: var(--md-admonition-icon--abstract); - mask-image: var(--md-admonition-icon--abstract); -} - -/* Colors for "warning" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.warning { - background: rgb(182 87 0 / 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .warning .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(219 125 39); -} - -[data-md-color-scheme="slate"] .md-typeset .warning .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(219 125 39); -} - -/* Colors for "info" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.info { - background: rgb(43 52 145 / 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .info .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(136 145 236); -} - -[data-md-color-scheme="slate"] .md-typeset .info .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(136 145 236); -} - -/* Colors for "example" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.example { - background: rgb(94 104 126); - border-radius: 0.4rem; -} - -[data-md-color-scheme="slate"] .md-typeset .example .admonition-title { - background: rgb(78 87 105); - color: rgb(246 247 249); -} - -[data-md-color-scheme="slate"] .md-typeset .example .admonition-title:before { - background-color: rgb(246 247 249); -} - -[data-md-color-scheme="slate"] .md-typeset .admonition.example code { - background: transparent; - color: #fff; -} - -/* Move the sidebars to the edges of the page */ -.md-main__inner.md-grid { - margin-left: 0; - margin-right: 0; - max-width: unset; - display: flex; - justify-content: center; -} - -.md-sidebar--primary { - margin-right: auto; -} - -.md-sidebar.md-sidebar--secondary { - margin-left: auto; -} - -.md-content { - max-width: 56rem; -} - -/* Maintain content positioning even if sidebars are disabled */ -@media screen and (min-width: 76.1875em) { - .md-sidebar { - display: block; - } - - .md-sidebar[hidden] { - visibility: hidden; - } -} - -/* Sidebar styling */ -@media screen and (min-width: 76.1875em) { - .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { - text-transform: uppercase; - } - - .md-nav__title[for="__toc"] { - text-transform: uppercase; - margin: 0.5rem; - } - - .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { - color: rgb(133 142 159); - margin: 0.5rem; - } - - .md-nav__item .md-nav__link { - position: relative; - } - - .md-nav__link:is(:focus, :hover):not(.md-nav__link--active) { - color: unset; - } - - .md-nav__item - .md-nav__link:is(:focus, :hover):not(.md-nav__link--active):before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.2; - z-index: -1; - background-color: grey; - } - - .md-nav__item .md-nav__link--active:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.15; - z-index: -1; - background-color: var(--md-typeset-a-color); - } - - .md-nav__link { - padding: 0.5rem 0.5rem 0.5rem 1rem; - margin: 0; - border-radius: 0 10px 10px 0; - font-weight: 600; - overflow: hidden; - } - - .md-sidebar__scrollwrap { - margin: 0; - } - - [dir="ltr"] - .md-nav--lifted - .md-nav[data-md-level="1"] - > .md-nav__list - > .md-nav__item { - padding: 0; - } - - .md-nav__item--nested .md-nav__item .md-nav__item { - padding: 0; - } - - .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { - font-weight: 300; - } - - .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { - font-weight: 400; - padding-left: 1.25rem; - } -} - -/* Table of Contents styling */ -@media screen and (min-width: 60em) { - [data-md-component="sidebar"] .md-nav__title[for="__toc"] { - text-transform: uppercase; - margin: 0.5rem; - margin-left: 0; - } - - [data-md-component="toc"] .md-nav__item .md-nav__link--active { - position: relative; - } - - [data-md-component="toc"] .md-nav__item .md-nav__link--active:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.15; - z-index: -1; - background-color: var(--md-typeset-a-color); - } - - [data-md-component="toc"] .md-nav__link { - padding: 0.5rem 0.5rem; - margin: 0; - border-radius: 10px 0 0 10px; - } - [dir="ltr"] .md-sidebar__inner { - padding: 0; - } - - .md-nav__item { - padding: 0; - } -} - /* Font changes */ .md-typeset { font-weight: 300; @@ -325,7 +50,7 @@ p.intro { font-weight: 500; } -/* Hide invisible jump selectors */ +/* Hide "Overview" jump selector */ h2#overview { visibility: hidden; height: 0; @@ -333,44 +58,7 @@ h2#overview { padding: 0; } -/* Code blocks */ -.md-typeset pre > code { - border-radius: 16px; -} - -.md-typeset .highlighttable .linenos { - max-height: var(--code-max-height); - overflow: hidden; -} - -.md-typeset .tabbed-block .highlighttable code { - border-radius: 0; -} - -.md-typeset .tabbed-block { - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - overflow: hidden; -} - -.js .md-typeset .tabbed-labels { - background: var(--tabbed-labels-color); - border-top-left-radius: 8px; - border-top-right-radius: 8px; -} - -.md-typeset .tabbed-labels > label { - font-weight: 400; - font-size: 0.7rem; - padding-top: 0.55em; - padding-bottom: 0.35em; -} - -.md-typeset pre > code { - max-height: var(--code-max-height); -} - -/* Reduce height of outdated banner */ +/* Reduce size of the outdated banner */ .md-banner__inner { margin: 0.45rem auto; } diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css new file mode 100644 index 00000000..b8becdd7 --- /dev/null +++ b/docs/src/assets/css/sidebar.css @@ -0,0 +1,114 @@ +/* Move the sidebars to the edges of the page */ +.md-main__inner.md-grid { + margin-left: 0; + margin-right: 0; + max-width: unset; + display: flex; + justify-content: center; +} + +.md-sidebar--primary { + margin-right: auto; +} + +.md-sidebar.md-sidebar--secondary { + margin-left: auto; +} + +.md-content { + max-width: 56rem; +} + +/* Maintain content positioning even if sidebars are disabled */ +@media screen and (min-width: 76.1875em) { + .md-sidebar { + display: block; + } + + .md-sidebar[hidden] { + visibility: hidden; + } +} + +/* Sidebar styling */ +@media screen and (min-width: 76.1875em) { + .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { + text-transform: uppercase; + } + + .md-nav__title[for="__toc"] { + text-transform: uppercase; + margin: 0.5rem; + } + + .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { + color: rgb(133 142 159); + margin: 0.5rem; + } + + .md-nav__item .md-nav__link { + position: relative; + } + + .md-nav__link:is(:focus, :hover):not(.md-nav__link--active) { + color: unset; + } + + .md-nav__item + .md-nav__link:is(:focus, :hover):not(.md-nav__link--active):before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.2; + z-index: -1; + background-color: grey; + } + + .md-nav__item .md-nav__link--active:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.15; + z-index: -1; + background-color: var(--md-typeset-a-color); + } + + .md-nav__link { + padding: 0.5rem 0.5rem 0.5rem 1rem; + margin: 0; + border-radius: 0 10px 10px 0; + font-weight: 600; + overflow: hidden; + } + + .md-sidebar__scrollwrap { + margin: 0; + } + + [dir="ltr"] + .md-nav--lifted + .md-nav[data-md-level="1"] + > .md-nav__list + > .md-nav__item { + padding: 0; + } + + .md-nav__item--nested .md-nav__item .md-nav__item { + padding: 0; + } + + .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { + font-weight: 300; + } + + .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { + font-weight: 400; + padding-left: 1.25rem; + } +} diff --git a/docs/src/assets/css/table-of-contents.css b/docs/src/assets/css/table-of-contents.css new file mode 100644 index 00000000..be9037a2 --- /dev/null +++ b/docs/src/assets/css/table-of-contents.css @@ -0,0 +1,37 @@ +/* Table of Contents styling */ +@media screen and (min-width: 60em) { + [data-md-component="sidebar"] .md-nav__title[for="__toc"] { + text-transform: uppercase; + margin: 0.5rem; + margin-left: 0; + } + + [data-md-component="toc"] .md-nav__item .md-nav__link--active { + position: relative; + } + + [data-md-component="toc"] .md-nav__item .md-nav__link--active:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.15; + z-index: -1; + background-color: var(--md-typeset-a-color); + } + + [data-md-component="toc"] .md-nav__link { + padding: 0.5rem 0.5rem; + margin: 0; + border-radius: 10px 0 0 10px; + } + [dir="ltr"] .md-sidebar__inner { + padding: 0; + } + + .md-nav__item { + padding: 0; + } +} diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md index e40c5224..7291ff47 100644 --- a/docs/src/learn/your-first-component.md +++ b/docs/src/learn/your-first-component.md @@ -8,7 +8,7 @@ Components are one of the core concepts of ReactPy. They are the foundation upon !!! note - If you have reached this point, you should have already [installed ReactPy-Django](../get-started/add-reactpy-to-a-project.md) through the previous steps. + If you have reached this point, you should have already [installed ReactPy-Django](../learn/add-reactpy-to-a-project.md) through the previous steps. --- diff --git a/mkdocs.yml b/mkdocs.yml index ec1cc22b..1356b5fe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -92,6 +92,11 @@ extra_javascript: extra_css: - assets/css/main.css - assets/css/home.css + - assets/css/button.css + - assets/css/admonition.css + - assets/css/sidebar.css + - assets/css/table-of-contents.css + - assets/css/code.css watch: - docs From 9678833665f15e5da8f19b2668627d4c27bad00c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 27 Aug 2023 23:12:37 -0700 Subject: [PATCH 05/32] reactJS style navbar --- docs/overrides/home.html | 13 +-- docs/src/assets/css/admonition.css | 8 +- docs/src/assets/css/button.css | 4 +- docs/src/assets/css/footer.css | 3 + docs/src/assets/css/home.css | 61 ++++++++-- docs/src/assets/css/main.css | 14 ++- docs/src/assets/css/navbar.css | 130 ++++++++++++++++++++++ docs/src/assets/css/sidebar.css | 6 +- docs/src/assets/css/table-of-contents.css | 2 +- mkdocs.yml | 4 + 10 files changed, 214 insertions(+), 31 deletions(-) create mode 100644 docs/src/assets/css/footer.css create mode 100644 docs/src/assets/css/navbar.css diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 34bf6989..a61899d0 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -8,7 +8,6 @@ {% block tabs %} -{{ super() }}
-
+

{{ config.site_name }}

{{ config.site_description }}

-
+
Get Started @@ -41,7 +40,7 @@

{{ config.site_name }}

-
+

Create user interfaces from components

ReactPy lets you build user interfaces out of individual pieces called components. Create your own ReactPy @@ -55,7 +54,7 @@

Create user interfaces from components

-
+

Write components with code and markup

ReactPy components are Python functions. Want to show some content conditionally? Use an @@ -65,7 +64,7 @@

Write components with code and markup

-
+

Add interactivity wherever you need it

ReactPy components receive data and return what should appear on the screen. You can pass them new data in @@ -78,7 +77,7 @@

Add interactivity wherever you need it

-
+

Go full-stack with the Django framework

ReactPy is a library. It lets you put components together, but it doesn't prescribe how to do routing and diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css index 206d637e..d99fbe3a 100644 --- a/docs/src/assets/css/admonition.css +++ b/docs/src/assets/css/admonition.css @@ -56,7 +56,7 @@ [data-md-color-scheme="slate"] .md-typeset .note .admonition-title:before { font-size: 1.1rem; - background-color: rgb(68 172 153); + background: rgb(68 172 153); } .md-typeset .note > .admonition-title:before, @@ -81,7 +81,7 @@ [data-md-color-scheme="slate"] .md-typeset .warning .admonition-title:before { font-size: 1.1rem; - background-color: rgb(219 125 39); + background: rgb(219 125 39); } /* Colors for "info" admonition */ @@ -100,7 +100,7 @@ [data-md-color-scheme="slate"] .md-typeset .info .admonition-title:before { font-size: 1.1rem; - background-color: rgb(136 145 236); + background: rgb(136 145 236); } /* Colors for "example" admonition */ @@ -115,7 +115,7 @@ } [data-md-color-scheme="slate"] .md-typeset .example .admonition-title:before { - background-color: rgb(246 247 249); + background: rgb(246 247 249); } [data-md-color-scheme="slate"] .md-typeset .admonition.example code { diff --git a/docs/src/assets/css/button.css b/docs/src/assets/css/button.css index 83c28a6e..2e0dbd92 100644 --- a/docs/src/assets/css/button.css +++ b/docs/src/assets/css/button.css @@ -2,7 +2,7 @@ border-color: #404756; border-radius: 9999px; color: #fff; - transition: color 125ms, background-color 125ms, border-color 125ms, + transition: color 125ms, background 125ms, border-color 125ms, transform 125ms; } @@ -20,7 +20,7 @@ .md-typeset .md-button.md-button--primary:focus, .md-typeset .md-button.md-button--primary:hover { border-color: transparent; - background-color: var(--reactpy-color-darker); + background: var(--reactpy-color-darker); } .md-typeset .md-button:focus { diff --git a/docs/src/assets/css/footer.css b/docs/src/assets/css/footer.css new file mode 100644 index 00000000..2b33fdb4 --- /dev/null +++ b/docs/src/assets/css/footer.css @@ -0,0 +1,3 @@ +.md-footer { + border: 1px solid rgb(255 255 255 / 5%); +} diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index 5ae08cc1..41f60b26 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -2,42 +2,79 @@ img.home-logo { height: 100px; } -.homepage-row { +.home-row { display: flex; justify-content: center; align-items: center; flex-direction: column; - padding: 6rem 0; - text-align: center; + padding: 6rem 0.8rem; } -.homepage-row.stripe { - background-color: var(--md-primary-fg-color); - color: var(--md-primary-bg-color); +.home-row.stripe { + background: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); + border: 0 solid rgba(246, 247, 249, 0.1); + border-top-width: 1px; + border-bottom-width: 1px; } -.homepage-row h1 { +.home-row h1 { max-width: 28rem; - font-size: 52px; line-height: 1.15; font-weight: 500; margin-bottom: 0.55rem; margin-top: -1rem; } -.homepage-row.first h1 { +.home-row.first { + text-align: center; +} + +.home-row.first h1 { margin-top: 0.55rem; margin-bottom: -0.75rem; } -.homepage-row p { +.home-row p { max-width: 35rem; - font-size: 21px; line-height: 1.5; font-weight: 400; } -.homepage-row.first p { +.home-row.first p { font-size: 32px; font-weight: 500; } + +/* Desktop Styling */ +@media screen and (min-width: 76.1875em) { + .home-row { + text-align: center; + } + .home-row p { + font-size: 21px; + } + .home-row h1 { + font-size: 52px; + } +} + +/* Mobile Styling */ +@media screen and (max-width: 76.1875em) { + .home-row { + padding: 4rem 0.8rem; + } + .home-row.first { + padding-top: 2rem; + } + .home-btns { + width: 100%; + display: grid; + grid-gap: 0.5rem; + gap: 0.5rem; + } +} diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css index 72a078bd..155f6675 100644 --- a/docs/src/assets/css/main.css +++ b/docs/src/assets/css/main.css @@ -6,12 +6,12 @@ [data-md-color-scheme="slate"] { --md-code-hl-color: #ffffcf1c; --md-hue: 225; - --md-default-bg-color: hsla(var(--md-hue), 15%, 16%, 1); + --md-default-bg-color: rgb(35 39 47); --md-default-bg-color--light: hsla(var(--md-hue), 15%, 16%, 0.54); --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 16%, 0.26); --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 16%, 0.07); --md-code-bg-color: #16181d; - --md-primary-fg-color: #2b3540; + --md-primary-fg-color: var(--md-default-bg-color); --md-default-fg-color--light: #fff; --md-typeset-a-color: #00b0f0; --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); @@ -19,6 +19,8 @@ --reactpy-color: #58b962; --reactpy-color-dark: #42914a; --reactpy-color-darker: #34743b; + --md-footer-bg-color: var(--md-default-bg-color); + --md-footer-bg-color--dark: var(--md-default-bg-color); } [data-md-color-scheme="default"] { @@ -62,3 +64,11 @@ h2#overview { .md-banner__inner { margin: 0.45rem auto; } + +/* Desktop Styles */ +@media screen and (min-width: 76.1875em) { + /* Remove max width on desktop */ + .md-grid { + max-width: none; + } +} diff --git a/docs/src/assets/css/navbar.css b/docs/src/assets/css/navbar.css new file mode 100644 index 00000000..ab52fc40 --- /dev/null +++ b/docs/src/assets/css/navbar.css @@ -0,0 +1,130 @@ +.md-header { + border: 0 solid transparent; + border-bottom-width: 1px; +} + +.md-header--shadow { + box-shadow: none; + border-color: rgb(255 255 255 / 5%); + transition: border-color 0.35s cubic-bezier(0.1, 0.7, 0.1, 1); +} + +.md-header__title { + display: none; +} + +/* Mobile Styling */ +@media screen and (max-width: 76.1875em) { + label.md-header__button.md-icon[for="__drawer"] { + order: 1; + } + .md-header__button.md-logo { + display: initial; + order: 2; + margin-right: auto; + } + .md-header__button[for="__search"] { + order: 3; + } + .md-header__option[data-md-component="palette"] { + order: 4; + } + .md-header__source { + display: initial; + order: 5; + } + .md-header__source .md-source__repository { + display: none; + } +} + +/* Desktop Styling */ +@media screen and (min-width: 76.1875em) { + /* Nav container */ + nav.md-header__inner { + display: contents; + } + header.md-header { + display: flex; + align-items: center; + } + + /* Logo */ + .md-header__button.md-logo { + order: 1; + padding-right: 0.2rem; + padding-top: 0; + padding-bottom: 0; + } + .md-header__button.md-logo img { + height: 2rem; + } + + /* Search */ + .md-search { + order: 2; + width: 100%; + margin-right: 0.6rem; + } + .md-search__inner { + width: 100%; + float: unset !important; + } + .md-search__form { + border-radius: 9999px; + } + [data-md-toggle="search"]:checked ~ .md-header .md-header__option { + max-width: unset; + opacity: unset; + transition: unset; + } + + /* Tabs */ + .md-tabs { + order: 3; + min-width: -webkit-fit-content; + min-width: -moz-fit-content; + min-width: fit-content; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + z-index: -1; + overflow: visible; + } + li.md-tabs__item.md-tabs__item--active { + background: #00b0f026; + border-radius: 9999px; + color: var(--md-typeset-a-color); + } + a.md-tabs__link { + margin: 0; + } + .md-tabs__item { + height: 1.8rem; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + } + + /* Dark/Light Selector */ + .md-header__option[data-md-component="palette"] { + order: 4; + } + + /* GitHub info */ + .md-header__source { + order: 5; + margin-left: 0 !important; + } +} + +/* Ultrawide Desktop Styles */ +@media screen and (min-width: 1919px) { + .md-search { + order: 2; + width: 100%; + max-width: 34.4rem; + margin: 0 auto; + } +} diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css index b8becdd7..fabeed59 100644 --- a/docs/src/assets/css/sidebar.css +++ b/docs/src/assets/css/sidebar.css @@ -30,7 +30,7 @@ } } -/* Sidebar styling */ +/* Desktop Styling */ @media screen and (min-width: 76.1875em) { .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { text-transform: uppercase; @@ -64,7 +64,7 @@ height: 100%; opacity: 0.2; z-index: -1; - background-color: grey; + background: grey; } .md-nav__item .md-nav__link--active:before { @@ -76,7 +76,7 @@ height: 100%; opacity: 0.15; z-index: -1; - background-color: var(--md-typeset-a-color); + background: var(--md-typeset-a-color); } .md-nav__link { diff --git a/docs/src/assets/css/table-of-contents.css b/docs/src/assets/css/table-of-contents.css index be9037a2..2dc61373 100644 --- a/docs/src/assets/css/table-of-contents.css +++ b/docs/src/assets/css/table-of-contents.css @@ -19,7 +19,7 @@ height: 100%; opacity: 0.15; z-index: -1; - background-color: var(--md-typeset-a-color); + background: var(--md-typeset-a-color); } [data-md-component="toc"] .md-nav__link { diff --git a/mkdocs.yml b/mkdocs.yml index 1356b5fe..4d83ebe6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,8 +39,10 @@ theme: features: - navigation.instant - navigation.tabs + - navigation.tabs.sticky - navigation.top - content.code.copy + - search.highlight icon: repo: fontawesome/brands/github logo: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg @@ -95,8 +97,10 @@ extra_css: - assets/css/button.css - assets/css/admonition.css - assets/css/sidebar.css + - assets/css/navbar.css - assets/css/table-of-contents.css - assets/css/code.css + - assets/css/footer.css watch: - docs From d6a23f82238a8db77a057120af1728d59d282d75 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 27 Aug 2023 23:36:00 -0700 Subject: [PATCH 06/32] Fix mobile and tablet sites --- docs/src/assets/css/code.css | 2 ++ docs/src/assets/css/footer.css | 2 +- docs/src/assets/css/home.css | 4 ++-- docs/src/assets/css/main.css | 7 ++++++- docs/src/assets/css/navbar.css | 4 ++-- docs/src/assets/css/sidebar.css | 27 +++++++++++---------------- 6 files changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css index 4da68750..413f0c07 100644 --- a/docs/src/assets/css/code.css +++ b/docs/src/assets/css/code.css @@ -22,6 +22,8 @@ background: var(--tabbed-labels-color); border-top-left-radius: 8px; border-top-right-radius: 8px; + margin: 0; + padding-left: 0.8rem; } .md-typeset .tabbed-labels > label { diff --git a/docs/src/assets/css/footer.css b/docs/src/assets/css/footer.css index 2b33fdb4..9f21c4ba 100644 --- a/docs/src/assets/css/footer.css +++ b/docs/src/assets/css/footer.css @@ -1,3 +1,3 @@ .md-footer { - border: 1px solid rgb(255 255 255 / 5%); + border-top: 1px solid rgb(255 255 255 / 5%); } diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index 41f60b26..6cec69da 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -51,7 +51,7 @@ img.home-logo { } /* Desktop Styling */ -@media screen and (min-width: 76.1875em) { +@media screen and (min-width: 60em) { .home-row { text-align: center; } @@ -64,7 +64,7 @@ img.home-logo { } /* Mobile Styling */ -@media screen and (max-width: 76.1875em) { +@media screen and (max-width: 60em) { .home-row { padding: 4rem 0.8rem; } diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css index 155f6675..5295cb7d 100644 --- a/docs/src/assets/css/main.css +++ b/docs/src/assets/css/main.css @@ -66,9 +66,14 @@ h2#overview { } /* Desktop Styles */ -@media screen and (min-width: 76.1875em) { +@media screen and (min-width: 60em) { /* Remove max width on desktop */ .md-grid { max-width: none; } } + +/* Max size of page content */ +.md-content { + max-width: 56rem; +} diff --git a/docs/src/assets/css/navbar.css b/docs/src/assets/css/navbar.css index ab52fc40..f110b41b 100644 --- a/docs/src/assets/css/navbar.css +++ b/docs/src/assets/css/navbar.css @@ -14,7 +14,7 @@ } /* Mobile Styling */ -@media screen and (max-width: 76.1875em) { +@media screen and (max-width: 60em) { label.md-header__button.md-icon[for="__drawer"] { order: 1; } @@ -39,7 +39,7 @@ } /* Desktop Styling */ -@media screen and (min-width: 76.1875em) { +@media screen and (min-width: 60em) { /* Nav container */ nav.md-header__inner { display: contents; diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css index fabeed59..ab25b439 100644 --- a/docs/src/assets/css/sidebar.css +++ b/docs/src/assets/css/sidebar.css @@ -1,4 +1,4 @@ -/* Move the sidebars to the edges of the page */ +/* Move the sidebar and TOC to the edge of the page */ .md-main__inner.md-grid { margin-left: 0; margin-right: 0; @@ -15,23 +15,9 @@ margin-left: auto; } -.md-content { - max-width: 56rem; -} - -/* Maintain content positioning even if sidebars are disabled */ -@media screen and (min-width: 76.1875em) { - .md-sidebar { - display: block; - } - - .md-sidebar[hidden] { - visibility: hidden; - } -} - /* Desktop Styling */ @media screen and (min-width: 76.1875em) { + /* Made the sidebar buttons look React-like */ .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { text-transform: uppercase; } @@ -111,4 +97,13 @@ font-weight: 400; padding-left: 1.25rem; } + + /* Maintain content positioning even if sidebar or TOC is disabled */ + .md-sidebar { + display: block; + } + + .md-sidebar[hidden] { + visibility: hidden; + } } From 5c49769e3ef6ff1aa3c0ee64da8a36580d46f341 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 28 Aug 2023 01:12:17 -0700 Subject: [PATCH 07/32] switch to green color scheme --- docs/src/assets/css/main.css | 12 +++++++----- docs/src/assets/css/navbar.css | 2 +- docs/src/assets/css/sidebar.css | 3 +-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css index 5295cb7d..bce976b4 100644 --- a/docs/src/assets/css/main.css +++ b/docs/src/assets/css/main.css @@ -1,6 +1,10 @@ /* Variable overrides */ :root { --code-max-height: 17.25rem; + --reactpy-color: #58b962; + --reactpy-color-dark: #42914a; + --reactpy-color-darker: #34743b; + --reactpy-color-opacity-10: rgb(88 185 98 / 10%); } [data-md-color-scheme="slate"] { @@ -13,14 +17,12 @@ --md-code-bg-color: #16181d; --md-primary-fg-color: var(--md-default-bg-color); --md-default-fg-color--light: #fff; - --md-typeset-a-color: #00b0f0; + --md-typeset-a-color: var(--reactpy-color); --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); - --tabbed-labels-color: rgb(52 58 70); - --reactpy-color: #58b962; - --reactpy-color-dark: #42914a; - --reactpy-color-darker: #34743b; + --md-accent-fg-color: var(--reactpy-color-dark); --md-footer-bg-color: var(--md-default-bg-color); --md-footer-bg-color--dark: var(--md-default-bg-color); + --tabbed-labels-color: rgb(52 58 70); } [data-md-color-scheme="default"] { diff --git a/docs/src/assets/css/navbar.css b/docs/src/assets/css/navbar.css index f110b41b..f45c6705 100644 --- a/docs/src/assets/css/navbar.css +++ b/docs/src/assets/css/navbar.css @@ -92,7 +92,7 @@ overflow: visible; } li.md-tabs__item.md-tabs__item--active { - background: #00b0f026; + background: var(--reactpy-color-opacity-10); border-radius: 9999px; color: var(--md-typeset-a-color); } diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css index ab25b439..8afb505a 100644 --- a/docs/src/assets/css/sidebar.css +++ b/docs/src/assets/css/sidebar.css @@ -60,9 +60,8 @@ left: 0; width: 100%; height: 100%; - opacity: 0.15; z-index: -1; - background: var(--md-typeset-a-color); + background: var(--reactpy-color-opacity-10); } .md-nav__link { From 7c6ccb1439f7de88783cdc22d66d8ae137a98630 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 28 Aug 2023 01:12:53 -0700 Subject: [PATCH 08/32] rename code tab color --- docs/src/assets/css/code.css | 2 +- docs/src/assets/css/main.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css index 413f0c07..f81b95b2 100644 --- a/docs/src/assets/css/code.css +++ b/docs/src/assets/css/code.css @@ -19,7 +19,7 @@ } .js .md-typeset .tabbed-labels { - background: var(--tabbed-labels-color); + background: var(--code-tab-color); border-top-left-radius: 8px; border-top-right-radius: 8px; margin: 0; diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css index bce976b4..0ce4c9a9 100644 --- a/docs/src/assets/css/main.css +++ b/docs/src/assets/css/main.css @@ -22,11 +22,11 @@ --md-accent-fg-color: var(--reactpy-color-dark); --md-footer-bg-color: var(--md-default-bg-color); --md-footer-bg-color--dark: var(--md-default-bg-color); - --tabbed-labels-color: rgb(52 58 70); + --code-tab-color: rgb(52 58 70); } [data-md-color-scheme="default"] { - --tabbed-labels-color: #7d829e26; + --code-tab-color: #7d829e26; } /* Font changes */ From d7567806e84e3b1e20f8ce474249a65c073a5847 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 28 Aug 2023 01:24:20 -0700 Subject: [PATCH 09/32] fix broken link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 31a383e2..391806d6 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Follow the links below to find out more about this project. - [Documentation](https://reactive-python.github.io/reactpy-django) - [GitHub Discussions](https://github.com/reactive-python/reactpy-django/discussions) - [Discord](https://discord.gg/uNb5P4hA9X) -- [Contributor Guide](https://reactive-python.github.io/reactpy-django/about/code/) +- [Contributor Guide](https://reactive-python.github.io/reactpy-django/develop/about/code/) - [Code of Conduct](https://github.com/reactive-python/reactpy-django/blob/main/CODE_OF_CONDUCT.md) From c64b3e3dc8510ff7b497c5c974089c31d2153c51 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 28 Aug 2023 01:38:08 -0700 Subject: [PATCH 10/32] django proj --- ...actpy-to-a-project.md => add-reactpy-to-a-django-project.md} | 0 mkdocs.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/src/learn/{add-reactpy-to-a-project.md => add-reactpy-to-a-django-project.md} (100%) diff --git a/docs/src/learn/add-reactpy-to-a-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md similarity index 100% rename from docs/src/learn/add-reactpy-to-a-project.md rename to docs/src/learn/add-reactpy-to-a-django-project.md diff --git a/mkdocs.yml b/mkdocs.yml index 4d83ebe6..615a3588 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,7 +2,7 @@ nav: - Home: index.md - Get Started: - - Add ReactPy to a Project: learn/add-reactpy-to-a-project.md + - Add ReactPy to a Django Project: learn/add-reactpy-to-a-django-project.md - Your First Component: learn/your-first-component.md - Reference: - Components: reference/components.md From 7a1eaafe436842d9b13889241171ff5bc22c83d9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 28 Aug 2023 01:53:57 -0700 Subject: [PATCH 11/32] no affiliation text --- docs/overrides/home.html | 3 --- docs/src/assets/css/footer.css | 12 ++++++++++++ mkdocs.yml | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/overrides/home.html b/docs/overrides/home.html index a61899d0..a7eee81f 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -3,9 +3,6 @@ {% block content %}{% endblock %} - -{% block footer %}{% endblock %} - {% block tabs %} From 444125d202b230dc42ec14b271a57a9dcdc731ef Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 2 Sep 2023 02:59:55 -0700 Subject: [PATCH 26/32] home code examples --- .../home-code-examples/add-interactivity.py | 30 +++++++ .../home-code-examples/code-block.html | 8 ++ .../create-user-interfaces.py | 22 +++++ .../write-components-with-python.py | 15 ++++ docs/overrides/home.html | 57 +++++++++++-- docs/src/assets/css/home.css | 76 +++++++++--------- docs/src/assets/css/sidebar.css | 21 +---- docs/src/assets/img/add-interactivity.png | Bin 0 -> 20118 bytes .../src/assets/img/create-user-interfaces.png | Bin 0 -> 12381 bytes .../img/write-components-with-python.png | Bin 0 -> 15412 bytes mkdocs.yml | 2 +- 11 files changed, 169 insertions(+), 62 deletions(-) create mode 100644 docs/overrides/home-code-examples/add-interactivity.py create mode 100644 docs/overrides/home-code-examples/code-block.html create mode 100644 docs/overrides/home-code-examples/create-user-interfaces.py create mode 100644 docs/overrides/home-code-examples/write-components-with-python.py create mode 100644 docs/src/assets/img/add-interactivity.png create mode 100644 docs/src/assets/img/create-user-interfaces.png create mode 100644 docs/src/assets/img/write-components-with-python.png diff --git a/docs/overrides/home-code-examples/add-interactivity.py b/docs/overrides/home-code-examples/add-interactivity.py new file mode 100644 index 00000000..90976446 --- /dev/null +++ b/docs/overrides/home-code-examples/add-interactivity.py @@ -0,0 +1,30 @@ +from reactpy import component, html, use_state + + +def filter_videos(videos, search_text): + return None + + +def search_input(dictionary, value): + return None + + +def video_list(videos, empty_heading): + return None + + +@component +def searchable_video_list(videos): + search_text, set_search_text = use_state("") + found_videos = filter_videos(videos, search_text) + + return html._( + search_input( + {"on_change": lambda new_text: set_search_text(new_text)}, + value=search_text, + ), + video_list( + videos=found_videos, + empty_heading=f"No matches for “{search_text}”", + ), + ) diff --git a/docs/overrides/home-code-examples/code-block.html b/docs/overrides/home-code-examples/code-block.html new file mode 100644 index 00000000..5ee471be --- /dev/null +++ b/docs/overrides/home-code-examples/code-block.html @@ -0,0 +1,8 @@ +

+ +
+
+ +
+
diff --git a/docs/overrides/home-code-examples/create-user-interfaces.py b/docs/overrides/home-code-examples/create-user-interfaces.py new file mode 100644 index 00000000..37776ab1 --- /dev/null +++ b/docs/overrides/home-code-examples/create-user-interfaces.py @@ -0,0 +1,22 @@ +from reactpy import component, html + + +def thumbnail(video): + return None + + +def like_button(video): + return None + + +@component +def video(video): + return html.div( + thumbnail(video), + html.a( + {"href": video.url}, + html.h3(video.title), + html.p(video.description), + ), + like_button(video), + ) diff --git a/docs/overrides/home-code-examples/write-components-with-python.py b/docs/overrides/home-code-examples/write-components-with-python.py new file mode 100644 index 00000000..6af43baa --- /dev/null +++ b/docs/overrides/home-code-examples/write-components-with-python.py @@ -0,0 +1,15 @@ +from reactpy import component, html + + +@component +def video_list(videos, empty_heading): + count = len(videos) + heading = empty_heading + if count > 0: + noun = "Videos" if count > 1 else "Video" + heading = f"{count} {noun}" + + return html.section( + html.h2(heading), + [video(video) for video in videos], + ) diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 223b18eb..2fde8507 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -1,3 +1,4 @@ + {% extends "main.html" %} @@ -6,6 +7,28 @@ {% block tabs %} -
-
+
+
+ alt="ReactPy Logo" class="home-logo">

{{ config.site_name }}

{{ config.site_description }}

@@ -36,13 +59,18 @@

{{ config.site_name }}

-
+

Create user interfaces from components

ReactPy lets you build user interfaces out of individual pieces called components. Create your own ReactPy components like thumbnail, like_button, and video. Then combine them into entire screens, pages, and apps.

+
+ {% with image="create-user-interfaces.png", num=1 %} + {% include "home-code-examples/code-block.html" %} + {% endwith %} +

Whether you work on your own or with thousands of other developers, using React feels the same. It is designed to let you seamlessly combine components written by independent people, teams, and @@ -50,36 +78,49 @@

Create user interfaces from components

-
-

Write components with code and markup

+
+

Write components with pure Python code

ReactPy components are Python functions. Want to show some content conditionally? Use an if statement. Displaying a list? Try using list comprehension. Learning ReactPy is learning programming.

+
+ {% with image="write-components-with-python.png", num=2 %} + {% include "home-code-examples/code-block.html" %} + {% endwith %} +
-
+

Add interactivity wherever you need it

ReactPy components receive data and return what should appear on the screen. You can pass them new data in response to an interaction, like when the user types into an input. ReactPy will then update the screen to match the new data.

+
+ {% with image="add-interactivity.png", num=3 %} + {% include "home-code-examples/code-block.html" %} + {% endwith %} +

You don't have to build your whole page in ReactPy. Add React to your existing HTML page, and render interactive ReactPy components anywhere on it.

-
+

Go full-stack with the Django framework

ReactPy is a library. It lets you put components together, but it doesn't prescribe how to do routing and data fetching. To build an entire app with ReactPy, we recommend a backend framework like Django.

+ + Get Started +
{% endblock %} diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index 06290428..c9c944d8 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -1,28 +1,8 @@ -[data-md-color-scheme="slate"] { - --stripe-bg-color: conic-gradient( - from 90deg at -10% 100%, - #2b303b 0deg, - #2b303b 90deg, - #16181d 1turn - ); - --stripe-border-color: rgba(246, 247, 249, 0.1); -} - -[data-md-color-scheme="default"] { - --stripe-bg-color: conic-gradient( - from 90deg at -10% 100%, - #bcc1cd 0deg, - #bcc1cd 90deg, - #fff 1turn - ); - --stripe-border-color: rgba(35, 39, 47, 0.1); -} - img.home-logo { height: 120px; } -.home-row { +.home .row { display: flex; justify-content: center; align-items: center; @@ -30,14 +10,22 @@ img.home-logo { padding: 6rem 0.8rem; } -.home-row.stripe { - background: var(--stripe-bg-color); +.home .row:not(.first, .stripe) { + background: var(--row-bg-color); +} + +.home .row.stripe { + background: var(--row-stripe-bg-color); border: 0 solid var(--stripe-border-color); border-top-width: 1px; border-bottom-width: 1px; } -.home-row h1 { +.home .row.first { + text-align: center; +} + +.home .row h1 { max-width: 28rem; line-height: 1.15; font-weight: 500; @@ -45,45 +33,41 @@ img.home-logo { margin-top: -1rem; } -.home-row.first { - text-align: center; -} - -.home-row.first h1 { +.home .row.first h1 { margin-top: 0.55rem; margin-bottom: -0.75rem; } -.home-row p { +.home .row p { max-width: 35rem; line-height: 1.5; font-weight: 400; } -.home-row.first p { +.home .row.first p { font-size: 32px; font-weight: 500; } /* Desktop Styling */ @media screen and (min-width: 60em) { - .home-row { + .home .row { text-align: center; } - .home-row p { + .home .row p { font-size: 21px; } - .home-row h1 { + .home .row h1 { font-size: 52px; } } /* Mobile Styling */ @media screen and (max-width: 60em) { - .home-row { + .home .row { padding: 4rem 0.8rem; } - .home-row.first { + .home .row.first { padding-top: 2rem; } .home-btns { @@ -93,3 +77,23 @@ img.home-logo { gap: 0.5rem; } } + +/* Code blocks */ +.home .row .tabbed-content { + background: #1f1f1f; + padding: 20px 18px; + width: 610px; +} + +.home .row .tabbed-content img { + user-select: none; + -moz-user-select: none; + -webkit-user-drag: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +.home .row .tabbed-content { + -webkit-filter: var(--code-block-filter); + filter: var(--code-block-filter); +} diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css index 544cc125..06d97b51 100644 --- a/docs/src/assets/css/sidebar.css +++ b/docs/src/assets/css/sidebar.css @@ -7,16 +7,12 @@ margin-left: 0; margin-right: 0; max-width: unset; - display: flex; - justify-content: center; + display: grid; + grid-template-columns: auto 1fr auto; } -.md-sidebar--primary { - margin-right: auto; -} - -.md-sidebar.md-sidebar--secondary { - margin-left: auto; +.md-content { + justify-self: center; } /* Desktop Styling */ @@ -101,13 +97,4 @@ font-weight: 400; padding-left: 1.25rem; } - - /* Maintain content positioning even if sidebar or TOC is disabled */ - .md-sidebar { - display: block; - } - - .md-sidebar[hidden] { - visibility: hidden; - } } diff --git a/docs/src/assets/img/add-interactivity.png b/docs/src/assets/img/add-interactivity.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e24d2925db020621ed3a157b3b34bd516353c1 GIT binary patch literal 20118 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 literal 0 HcmV?d00001 diff --git a/docs/src/assets/img/create-user-interfaces.png b/docs/src/assets/img/create-user-interfaces.png new file mode 100644 index 0000000000000000000000000000000000000000..13abd064d9ba90d6640f3769bba0eade6e366868 GIT binary patch literal 12381 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 literal 0 HcmV?d00001 diff --git a/docs/src/assets/img/write-components-with-python.png b/docs/src/assets/img/write-components-with-python.png new file mode 100644 index 0000000000000000000000000000000000000000..ba34cdf9e66c5b631a829ef1951014851b04cc50 GIT binary patch literal 15412 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 qn!hSB Date: Sat, 2 Sep 2023 04:01:12 -0700 Subject: [PATCH 27/32] fix mobile markdown page positioning --- docs/src/assets/css/sidebar.css | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css index 06d97b51..aeadf3b5 100644 --- a/docs/src/assets/css/sidebar.css +++ b/docs/src/assets/css/sidebar.css @@ -2,21 +2,21 @@ --sizebar-font-size: 0.62rem; } -/* Move the sidebar and TOC to the edge of the page */ -.md-main__inner.md-grid { - margin-left: 0; - margin-right: 0; - max-width: unset; - display: grid; - grid-template-columns: auto 1fr auto; -} - -.md-content { - justify-self: center; -} - /* Desktop Styling */ @media screen and (min-width: 76.1875em) { + /* Move the sidebar and TOC to the edge of the page */ + .md-main__inner.md-grid { + margin-left: 0; + margin-right: 0; + max-width: unset; + display: grid; + grid-template-columns: auto 1fr auto; + } + + .md-content { + justify-self: center; + width: 100%; + } /* Made the sidebar buttons look React-like */ .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { text-transform: uppercase; From a76b72a84246f33815b2bb72e987fe93b55c4306 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 4 Sep 2023 03:02:48 -0700 Subject: [PATCH 28/32] tons of syntax highlighting and misc tweaks --- CHANGELOG.md | 10 +- README.md | 2 - docs/includes/orm.md | 6 +- docs/src/about/code.md | 4 +- docs/src/about/docs.md | 2 +- docs/src/assets/css/code.css | 40 +++++++ docs/src/dictionary.txt | 5 +- .../learn/add-reactpy-to-a-django-project.md | 24 ++--- docs/src/learn/your-first-component.md | 12 +-- docs/src/reference/components.md | 88 +++++++-------- docs/src/reference/decorators.md | 28 ++--- docs/src/reference/hooks.md | 100 +++++++++--------- docs/src/reference/settings.md | 30 +++--- docs/src/reference/template-tag.md | 24 ++--- docs/src/reference/utils.md | 26 ++--- src/reactpy_django/checks.py | 2 +- tests/test_app/settings.py | 2 +- 17 files changed, 221 insertions(+), 184 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f29365..577f67f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ Using the following categories, list your changes in this order: ### Changed - Bumped the minimum ReactPy version to `1.0.2`. -- Prettier websocket URLs for components that do not have sessions. +- Prettier WebSocket URLs for components that do not have sessions. - Template tag will now only validate `args`/`kwargs` if `settings.py:DEBUG` is enabled. - Bumped the minimum `@reactpy/client` version to `0.3.1` - Bumped the minimum Django version to `4.2`. @@ -79,7 +79,7 @@ Using the following categories, list your changes in this order: ### Changed -- ReactPy will now provide a warning if your HTTP URLs are not on the same prefix as your websockets. +- ReactPy will now provide a warning if your HTTP URLs are not on the same prefix as your WebSockets. - Cleaner logging output for auto-detected ReactPy root components. ### Deprecated @@ -91,14 +91,14 @@ Using the following categories, list your changes in this order: - Warning W007 (`REACTPY_WEBSOCKET_URL doesn't end with a slash`) has been removed. ReactPy now automatically handles slashes. - Warning W008 (`REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character`) has been removed. ReactPy now automatically handles this scenario. -- Error E009 (`channels is not in settings.py:INSTALLED_APPS`) has been removed. Newer versions of `channels` do not require installation via `INSTALLED_APPS` to receive an ASGI webserver. +- Error E009 (`channels is not in settings.py:INSTALLED_APPS`) has been removed. Newer versions of `channels` do not require installation via `INSTALLED_APPS` to receive an ASGI web server. ## [3.3.2] - 2023-08-13 ### Added -- ReactPy Websocket will now decode messages via `orjson` resulting in an ~6% overall performance improvement. -- Built-in `asyncio` event loops are now patched via `nest_asyncio`, resulting in an ~10% overall performance improvement. This has no performance impact if you are running your webserver with `uvloop`. +- ReactPy WebSocket will now decode messages via `orjson` resulting in an ~6% overall performance improvement. +- Built-in `asyncio` event loops are now patched via `nest_asyncio`, resulting in an ~10% overall performance improvement. This has no performance impact if you are running your web server with `uvloop`. ### Fixed diff --git a/README.md b/README.md index 8e05e247..8aebcd8b 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,6 @@ def hello_world(recipient: str): In your **Django app**'s HTML template, you can now embed your ReactPy component using the `component` template tag. Within this tag, you will need to type in the dotted path to the component. - - Additionally, you can pass in `args` and `kwargs` into your component function. After reading the code below, pay attention to how the function definition for `hello_world` (_from the previous example_) accepts a `recipient` argument. diff --git a/docs/includes/orm.md b/docs/includes/orm.md index 22151a74..fafb6226 100644 --- a/docs/includes/orm.md +++ b/docs/includes/orm.md @@ -1,13 +1,13 @@ -Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `SynchronousOnlyOperation` exception. +Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `#!python SynchronousOnlyOperation` exception. -These `SynchronousOnlyOperation` exceptions may be resolved in a future version of Django containing an asynchronous ORM. However, it is best practice to always perform ORM calls in the background via hooks. +These `#!python SynchronousOnlyOperation` exceptions may be resolved in a future version of Django containing an asynchronous ORM. However, it is best practice to always perform ORM calls in the background via hooks. -By default, automatic recursive fetching of `ManyToMany` or `ForeignKey` fields is enabled within the default `QueryOptions.postprocessor`. This is needed to prevent `SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components. +By default, automatic recursive fetching of `#!python ManyToMany` or `#!python ForeignKey` fields is enabled within the default `#!python QueryOptions.postprocessor`. This is needed to prevent `#!python SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components. diff --git a/docs/src/about/code.md b/docs/src/about/code.md index e0dfe7d7..b163d01c 100644 --- a/docs/src/about/code.md +++ b/docs/src/about/code.md @@ -44,7 +44,7 @@ pip install -e . -r requirements.txt If you receive errors related to this during installation, follow the instructions in your console errors. -Finally, to verify that everything is working properly, you can manually run the test webserver. +Finally, to verify that everything is working properly, you can manually run the test web server. ```bash linenums="0" cd tests @@ -84,7 +84,7 @@ cd tests python manage.py test ``` -## Running Django test webserver +## Running Django test web server If you want to manually run the Django test application, you can use the following command: diff --git a/docs/src/about/docs.md b/docs/src/about/docs.md index 913bc0a6..6c2f413b 100644 --- a/docs/src/about/docs.md +++ b/docs/src/about/docs.md @@ -31,7 +31,7 @@ Then, by running the command below you can: pip install -e . -r requirements.txt --upgrade ``` -Finally, to verify that everything is working properly, you can manually run the docs preview webserver. +Finally, to verify that everything is working properly, you can manually run the docs preview web server. ```bash linenums="0" mkdocs serve diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css index 8452ab4c..4949ef72 100644 --- a/docs/src/assets/css/code.css +++ b/docs/src/assets/css/code.css @@ -10,12 +10,21 @@ --md-code-bg-color: #16181d; --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); --code-tab-color: rgb(52 58 70); + --md-code-hl-name-color: #aadafc; + --md-code-hl-string-color: hsl(21 49% 63% / 1); + --md-code-hl-keyword-color: hsl(289.67deg 35% 60%); + --md-code-hl-constant-color: hsl(213.91deg 68% 61%); + --md-code-hl-number-color: #bfd9ab; + --func-and-decorator-color: #dcdcae; + --module-import-color: #60c4ac; } [data-md-color-scheme="default"] { --md-code-hl-color: #ffffcf1c; --md-code-bg-color: rgba(208, 211, 220, 0.4); --md-code-fg-color: rgb(64, 71, 86); --code-tab-color: #fff; + --func-and-decorator-color: var(--md-code-hl-function-color); + --module-import-color: #e153e5; } [data-md-color-scheme="default"] .md-typeset .highlight > pre > code, [data-md-color-scheme="default"] .md-typeset .highlight > table.highlighttable { @@ -68,3 +77,34 @@ .md-typeset .tabbed-set .highlighttable { border-radius: 0; } + +/* Code hightlighting colors */ + +/* Module imports */ +.highlight .nc, +.highlight .ne, +.highlight .nn, +.highlight .nv { + color: var(--module-import-color); +} + +/* Function def name and decorator */ +.highlight .nd, +.highlight .nf { + color: var(--func-and-decorator-color); +} + +/* None type */ +.highlight .kc { + color: var(--md-code-hl-constant-color); +} + +/* Keywords such as def and return */ +.highlight .k { + color: var(--md-code-hl-constant-color); +} + +/* HTML tags */ +.highlight .nt { + color: var(--md-code-hl-constant-color); +} diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 2dbbc4e6..d3d2eb25 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -2,8 +2,8 @@ django sanic plotly nox -websocket -websockets +WebSocket +WebSockets changelog async pre @@ -16,7 +16,6 @@ refetched refetching html jupyter -webserver iframe keyworded stylesheet diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md index 83dd37a5..7d7c949f 100644 --- a/docs/src/learn/add-reactpy-to-a-django-project.md +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -24,7 +24,7 @@ pip install reactpy-django ## Step 2: Configure `settings.py` -Add `reactpy_django` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) file. +Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) file. === "settings.py" @@ -39,13 +39,13 @@ Add `reactpy_django` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev If you have not enabled ASGI on your **Django project** yet, here is a summary of the [`django`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) and [`channels`](https://channels.readthedocs.io/en/stable/installation.html) installation docs: 1. Install `channels[daphne]` - 2. Add `daphne` to `INSTALLED_APPS` + 2. Add `#!python "daphne"` to `#!python INSTALLED_APPS`. ```python linenums="0" {% include "../../python/configure-channels-installed-app.py" %} ``` - 3. Set your `ASGI_APPLICATION` variable. + 3. Set your `#!python ASGI_APPLICATION` variable. ```python linenums="0" {% include "../../python/configure-channels-asgi-app.py" %} @@ -53,11 +53,11 @@ Add `reactpy_django` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev ??? note "Configure ReactPy settings (Optional)" - {% include "../reference/settings.md" start="" end="" %} + {% include "../reference/settings.md" start="" end="" %} ## Step 3: Configure `urls.py` -Add ReactPy HTTP paths to your `urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) file. +Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) file. === "urls.py" @@ -67,7 +67,7 @@ Add ReactPy HTTP paths to your `urlpatterns` in your [`urls.py`](https://docs.dj ## Step 4: Configure `asgi.py` -Register ReactPy's WebSocket using `REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) file. +Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) file. === "asgi.py" @@ -75,15 +75,15 @@ Register ReactPy's WebSocket using `REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`] {% include "../../python/configure-asgi.py" %} ``` -??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Optional)" +??? note "Add `#!python AuthMiddlewareStack` and `#!python SessionMiddlewareStack` (Optional)" - There are many situations where you need to access the Django `User` or `Session` objects within ReactPy components. For example, if you want to: + There are many situations where you need to access the Django `#!python User` or `#!python Session` objects within ReactPy components. For example, if you want to: - 1. Access the `User` that is currently logged in - 2. Login or logout the current `User` - 3. Access Django's `Session` object + 1. Access the `#!python User` that is currently logged in + 2. Login or logout the current `#!python User` + 3. Access Django's `#!python Session` object - In these situations will need to ensure you are using `AuthMiddlewareStack` and/or `SessionMiddlewareStack`. + In these situations will need to ensure you are using `#!python AuthMiddlewareStack` and/or `#!python SessionMiddlewareStack`. ```python linenums="0" {% include "../../python/configure-asgi-middleware.py" start="# start" %} diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md index ac155b49..e7ddcd65 100644 --- a/docs/src/learn/your-first-component.md +++ b/docs/src/learn/your-first-component.md @@ -31,7 +31,7 @@ You will need a file to start creating ReactPy components. We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. -Within this file, you can define your component functions and then add ReactPy's `#!python @component` decorator. +Within this file, you can define your component functions using ReactPy's `#!python @component` decorator. === "components.py" @@ -43,7 +43,7 @@ Within this file, you can define your component functions and then add ReactPy's We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. - Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](#embedding-in-a-template)). So, at minimum your component path needs to be valid to Python's `importlib`. + Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`. ??? question "What does the decorator actually do?" @@ -58,9 +58,9 @@ Within this file, you can define your component functions and then add ReactPy's ## Embedding in a template -{% include-markdown "../../../README.md" start="" end="" %} +In your **Django app**'s HTML template, you can now embed your ReactPy component using the `#!jinja {% component %}` template tag. Within this tag, you will need to type in the dotted path to the component. -Additionally, you can pass in `args` and `kwargs` into your component function. After reading the code below, pay attention to how the function definition for `hello_world` ([_from the previous step_](#defining-a-component)) accepts a `recipient` argument. +Additionally, you can pass in `#!python args` and `#!python kwargs` into your component function. After reading the code below, pay attention to how the function definition for `#!python hello_world` ([_from the previous step_](#defining-a-component)) accepts a `#!python recipient` argument. === "my-template.html" @@ -72,7 +72,7 @@ Additionally, you can pass in `args` and `kwargs` into your component function. ??? question "Where is my templates folder?" - If you do not have a `templates` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). + If you do not have a `./templates/` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). ## Setting up a Django view @@ -102,7 +102,7 @@ We will add this new view into your [`urls.py`](https://docs.djangoproject.com/e ## Viewing your component -To test your new Django view, run the following command to start up a development webserver. +To test your new Django view, run the following command to start up a development web server. ```bash linenums="0" python manage.py runserver diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index f4dae25a..d3f235da 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -24,24 +24,24 @@ Convert any Django view into a ReactPy component by using this decorator. Compat | Name | Type | Description | Default | | --- | --- | --- | --- | - | `view` | `Callable | View` | The view function or class to convert. | N/A | - | `compatibility` | `bool` | If True, the component will be rendered in an iframe. When using compatibility mode `tranforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments will be ignored. | `False` | - | `transforms` | `Sequence[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `tuple` | - | `strict_parsing` | `bool` | If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `True` | + | `#!python view` | `#!python Callable | View` | The view function or class to convert. | N/A | + | `#!python compatibility` | `#!python bool` | If `#!python True`, the component will be rendered in an iframe. When using compatibility mode `#!python tranforms`, `#!python strict_parsing`, `#!python request`, `#!python args`, and `#!python kwargs` arguments will be ignored. | `#!python False` | + | `#!python transforms` | `#!python Sequence[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `#!python tuple` | + | `#!python strict_parsing` | `#!python bool` | If `#!python True`, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `#!python True` | **Returns** | Type | Description | | --- | --- | - | `_ViewComponentConstructor` | A function that takes `request, *args, key, **kwargs` and returns a ReactPy component. All parameters are directly provided to your view, besides `key` which is used by ReactPy. | + | `#!python _ViewComponentConstructor` | A function that takes `#!python request, *args, key, **kwargs` and returns a ReactPy component. All parameters are directly provided to your view, besides `#!python key` which is used by ReactPy. | -??? Warning "Potential information exposure when using `compatibility = True`" +??? Warning "Potential information exposure when using `#!python compatibility = True`" - When using `compatibility` mode, ReactPy automatically exposes a URL to your view. + When using `#!python compatibility` mode, ReactPy automatically exposes a URL to your view. It is your responsibility to ensure privileged information is not leaked via this method. - You must implement a method to ensure only authorized users can access your view. This can be done via directly writing conditionals into your view, or by adding decorators such as [`user_passes_test`](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views. For example... + You must implement a method to ensure only authorized users can access your view. This can be done via directly writing conditionals into your view, or by adding decorators such as [`#!python user_passes_test`](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views. For example... === "Function Based View" @@ -57,17 +57,17 @@ Convert any Django view into a ReactPy component by using this decorator. Compat ??? info "Existing limitations" - There are currently several limitations of using `view_to_component` that may be resolved in a future version of `reactpy_django`. + There are currently several limitations of using `#!python view_to_component` that may be resolved in a future version. - Requires manual intervention to change request methods beyond `GET`. - ReactPy events cannot conveniently be attached to converted view HTML. - Has no option to automatically intercept local anchor link (such as `#!html `) click events. - _Please note these limitations do not exist when using `compatibility` mode._ + _Please note these limitations do not exist when using `#!python compatibility` mode._ ??? question "How do I use this for Class Based Views?" - You can simply pass your Class Based View directly into `view_to_component`. + You can simply pass your Class Based View directly into `#!python view_to_component`. === "components.py" @@ -77,7 +77,7 @@ Convert any Django view into a ReactPy component by using this decorator. Compat ??? question "How do I transform views from external libraries?" - In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator. + In order to convert external views, you can utilize `#!python view_to_component` as a function, rather than a decorator. === "components.py" @@ -85,11 +85,11 @@ Convert any Django view into a ReactPy component by using this decorator. Compat {% include "../../python/vtc-func.py" %} ``` -??? question "How do I provide `request`, `args`, and `kwargs` to a view?" +??? question "How do I provide `#!python request`, `#!python args`, and `#!python kwargs` to a view?" - **`Request`** + **`#!python Request`** - You can use the `request` parameter to provide the view a custom request object. + You can use the `#!python request` parameter to provide the view a custom request object. === "components.py" @@ -99,9 +99,9 @@ Convert any Django view into a ReactPy component by using this decorator. Compat --- - **`args` and `kwargs`** + **`#!python args` and `#!python kwargs`** - You can use the `args` and `kwargs` parameters to provide positional and keyworded arguments to a view. + You can use the `#!python args` and `#!python kwargs` parameters to provide positional and keyworded arguments to a view. === "components.py" @@ -109,15 +109,15 @@ Convert any Django view into a ReactPy component by using this decorator. Compat {% include "../../python/vtc-args-kwargs.py" %} ``` -??? question "How do I use `strict_parsing`, `compatibility`, and `transforms`?" +??? question "How do I use `#!python strict_parsing`, `#!python compatibility`, and `#!python transforms`?" - **`strict_parsing`** + **`#!python strict_parsing`** By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5. However, there are some circumstances where you may not have control over the original HTML, so you may be unable to fix it. Or you may be relying on non-standard HTML tags such as `#!html Hello World `. - In these scenarios, you may want to rely on best-fit parsing by setting the `strict_parsing` parameter to `False`. + In these scenarios, you may want to rely on best-fit parsing by setting the `#!python strict_parsing` parameter to `#!python False`. === "components.py" @@ -129,11 +129,11 @@ Convert any Django view into a ReactPy component by using this decorator. Compat --- - **`compatibility`** + **`#!python compatibility`** For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe. - Any view can be rendered within compatibility mode. However, the `transforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments do not apply to compatibility mode. + Any view can be rendered within compatibility mode. However, the `#!python transforms`, `#!python strict_parsing`, `#!python request`, `#!python args`, and `#!python kwargs` arguments do not apply to compatibility mode. @@ -143,17 +143,17 @@ Convert any Django view into a ReactPy component by using this decorator. Compat {% include "../../python/vtc-compatibility.py" %} ``` - _Note: By default the `compatibility` iframe is unstyled, and thus won't look pretty until you add some CSS._ + _Note: By default the `#!python compatibility` iframe is unstyled, and thus won't look pretty until you add some CSS._ --- - **`transforms`** + **`#!python transforms`** - After your view has been turned into [VDOM](https://reactpy.dev/docs/reference/specifications.html#vdom) (python dictionaries), `view_to_component` will call your `transforms` functions on every VDOM node. + After your view has been turned into [VDOM](https://reactpy.dev/docs/reference/specifications.html#vdom) (python dictionaries), `#!python view_to_component` will call your `#!python transforms` functions on every VDOM node. This allows you to modify your view prior to rendering. - For example, if you are trying to modify the text of a node with a certain `id`, you can create a transform like such: + For example, if you are trying to modify the text of a node with a certain `#!python id`, you can create a transform like such: === "components.py" @@ -177,22 +177,22 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. | Name | Type | Description | Default | | --- | --- | --- | --- | - | `static_path` | `str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | - | `key` | `Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings | `None` | + | `#!python static_path` | `#!python str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | + | `#!python key` | `#!python Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `Component` | A ReactPy component. | + | `#!python Component` | A ReactPy component. | -??? question "Should I put `django_css` at the top of my HTML?" +??? question "Should I put `#!python django_css` at the top of my HTML?" Yes, if the stylesheet contains styling for your component. -??? question "Can I load static CSS using `html.link` instead?" +??? question "Can I load static CSS using `#!python html.link` instead?" - While you can load stylesheets with `html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will be loaded after your component is displayed. This would likely cause unintended visual behavior, so use this at your own discretion. + While you can load stylesheets with `#!python html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will be loaded after your component is displayed. This would likely cause unintended visual behavior, so use this at your own discretion. Here's an example on what you should avoid doing for Django static files: @@ -202,9 +202,9 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. ??? question "How do I load external CSS?" - `django_css` can only be used with local static files. + `#!python django_css` can only be used with local static files. - For external CSS, substitute `django_css` with `html.link`. + For external CSS, substitute `#!python django_css` with `#!python html.link`. ```python {% include "../../python/django-css-external-link.py" %} @@ -214,7 +214,7 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. Traditionally, stylesheets are loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. - To help improve webpage load times, you can use the `django_css` component to defer loading your stylesheet until it is needed. + To help improve webpage load times, you can use the `#!python django_css` component to defer loading your stylesheet until it is needed. ## Django JS @@ -232,22 +232,22 @@ Allows you to defer loading JavaScript until a component begins rendering. This | Name | Type | Description | Default | | --- | --- | --- | --- | - | `static_path` | `str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | - | `key` | `Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings | `None` | + | `#!python static_path` | `#!python str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | + | `#!python key` | `#!python Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `Component` | A ReactPy component. | + | `#!python Component` | A ReactPy component. | -??? question "Should I put `django_js` at the bottom of my HTML?" +??? question "Should I put `#!python django_js` at the bottom of my HTML?" Yes, if your scripts are reliant on the contents of the component. -??? question "Can I load static JavaScript using `html.script` instead?" +??? question "Can I load static JavaScript using `#!python html.script` instead?" - While you can load JavaScript with `html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed. + While you can load JavaScript with `#!python html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed. Here's an example on what you should avoid doing for Django static files: @@ -257,9 +257,9 @@ Allows you to defer loading JavaScript until a component begins rendering. This ??? question "How do I load external JS?" - `django_js` can only be used with local static files. + `#!python django_js` can only be used with local static files. - For external JavaScript, substitute `django_js` with `html.script`. + For external JavaScript, substitute `#!python django_js` with `#!python html.script`. ```python {% include "../../python/django-js-remote-script.py" %} @@ -269,4 +269,4 @@ Allows you to defer loading JavaScript until a component begins rendering. This Traditionally, JavaScript is loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. - To help improve webpage load times, you can use the `django_js` component to defer loading your JavaScript until it is needed. + To help improve webpage load times, you can use the `#!python django_js` component to defer loading your JavaScript until it is needed. diff --git a/docs/src/reference/decorators.md b/docs/src/reference/decorators.md index ee79a4e8..59440366 100644 --- a/docs/src/reference/decorators.md +++ b/docs/src/reference/decorators.md @@ -10,11 +10,11 @@ Decorator functions can be used within your `components.py` to help simplify dev ## Auth Required -You can limit access to a component to users with a specific `auth_attribute` by using this decorator (with or without parentheses). +You can limit access to a component to users with a specific `#!python auth_attribute` by using this decorator (with or without parentheses). -By default, this decorator checks if the user is logged in and not deactivated (`is_active`). +By default, this decorator checks if the user is logged in and not deactivated (`#!python is_active`). -This decorator is commonly used to selectively render a component only if a user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). +This decorator is commonly used to selectively render a component only if a user [`#!python is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`#!python is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). === "components.py" @@ -28,20 +28,20 @@ This decorator is commonly used to selectively render a component only if a user | Name | Type | Description | Default | | --- | --- | --- | --- | - | `auth_attribute` | `str` | The value to check within the user object. This is checked in the form of `UserModel.`. | `#!python "is_active"` | - | `fallback` | `ComponentType`, `VdomDict`, `None` | The `component` or `reactpy.html` snippet to render if the user is not authenticated. | `None` | + | `#!python auth_attribute` | `#!python str` | The value to check within the user object. This is checked via `#!python getattr(scope["user"], auth_attribute)`. | `#!python "is_active"` | + | `#!python fallback` | `#!python ComponentType | VdomDict | None` | The `#!python component` or `#!python reactpy.html` snippet to render if the user is not authenticated. | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `Component` | A ReactPy component. | - | `VdomDict` | An `reactpy.html` snippet. | - | `None` | No component render. | + | #!python Component` | A ReactPy component. | + | #!python VdomDict` | A `#!python reactpy.html` snippet. | + | #!python None` | No component render. | ??? question "How do I render a different component if authentication fails?" - You can use a component with the `fallback` argument, as seen below. + You can use a component with the `#!python fallback` argument, as seen below. === "components.py" @@ -49,9 +49,9 @@ This decorator is commonly used to selectively render a component only if a user {% include "../../python/auth-required-component-fallback.py" %} ``` -??? question "How do I render a simple `reactpy.html` snippet if authentication fails?" +??? question "How do I render a simple `#!python reactpy.html` snippet if authentication fails?" - You can use a `reactpy.html` snippet with the `fallback` argument, as seen below. + You can use a `#!python reactpy.html` snippet with the `#!python fallback` argument, as seen below. === "components.py" @@ -59,9 +59,9 @@ This decorator is commonly used to selectively render a component only if a user {% include "../../python/auth-required-vdom-fallback.py" %} ``` -??? question "How can I check if a user `is_staff`?" +??? question "How can I check if a user `#!python is_staff`?" - You can set the `auth_attribute` to `is_staff`, as seen blow. + You can do this by setting `#!python auth_attribute="is_staff"`, as seen blow. === "components.py" @@ -73,7 +73,7 @@ This decorator is commonly used to selectively render a component only if a user You will need to be using a [custom user model](https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model) within your Django instance. - For example, if your user model has the field `is_really_cool` ... + For example, if your user model has the field `#!python is_really_cool` ... === "models.py" diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index 5150e4d4..62986930 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -16,9 +16,9 @@ Prefabricated hooks can be used within your `components.py` to help simplify dev ## Use Query -The `use_query` hook is used fetch Django ORM queries. +This hook is used [read](https://www.sumologic.com/glossary/crud/) data from the Django ORM. -The function you provide into this hook must return either a `Model` or `QuerySet`. +The query function you provide must return either a `#!python Model` or `#!python QuerySet`. === "components.py" @@ -38,20 +38,20 @@ The function you provide into this hook must return either a `Model` or `QuerySe | Name | Type | Description | Default | | --- | --- | --- | --- | - | `options` | `QueryOptions | None` | An optional `QueryOptions` object that can modify how the query is executed. | None | - | `query` | `Callable[_Params, _Result | None]` | A callable that returns a Django `Model` or `QuerySet`. | N/A | - | `*args` | `_Params.args` | Positional arguments to pass into `query`. | N/A | - | `**kwargs` | `_Params.kwargs` | Keyword arguments to pass into `query`. | N/A | + | `#!python options` | `#!python QueryOptions | None` | An optional `#!python QueryOptions` object that can modify how the query is executed. | `#!python None` | + | `#!python query` | `#!python Callable[_Params, _Result | None]` | A callable that returns a Django `#!python Model` or `#!python QuerySet`. | N/A | + | `#!python *args` | `#!python _Params.args` | Positional arguments to pass into `#!python query`. | N/A | + | `#!python **kwargs` | `#!python _Params.kwargs` | Keyword arguments to pass into `#!python query`. | N/A | **Returns** | Type | Description | | --- | --- | - | `Query[_Result | None]` | An object containing `loading`/`error` states, your `data` (if the query has successfully executed), and a `refetch` callable that can be used to re-run the query. | + | `#!python Query[_Result | None]` | An object containing `#!python loading`/`#!python error` states, your `#!python data` (if the query has successfully executed), and a `#!python refetch` callable that can be used to re-run the query. | ??? question "How can I provide arguments to my query function?" - `*args` and `**kwargs` can be provided to your query function via `use_query` parameters. + `#!python *args` and `#!python **kwargs` can be provided to your query function via `#!python use_query` parameters. === "components.py" @@ -59,17 +59,17 @@ The function you provide into this hook must return either a `Model` or `QuerySe {% include "../../python/use-query-args.py" %} ``` -??? question "Why does `get_items` in the example return `TodoItem.objects.all()`?" +??? question "Why does `#!python get_items` in the example return `#!python TodoItem.objects.all()`?" - This was a technical design decision to based on [Apollo's `useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `SynchronousOnlyOperation` exceptions. + This was a technical design decision to based on [Apollo's `#!javascript useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `#!python SynchronousOnlyOperation` exceptions. - The `use_query` hook ensures the provided `Model` or `QuerySet` executes all [deferred](https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_deferred_fields)/[lazy queries](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) safely prior to reaching your components. + The `#!python use_query` hook ensures the provided `#!python Model` or `#!python QuerySet` executes all [deferred](https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_deferred_fields)/[lazy queries](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) safely prior to reaching your components. -??? question "How can I use `QueryOptions` to customize fetching behavior?" +??? question "How can I use `#!python QueryOptions` to customize fetching behavior?" - **`thread_sensitive`** + **`#!python thread_sensitive`** - Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. + Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`#!python sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. This setting only applies to sync query functions, and will be ignored for async functions. @@ -81,16 +81,16 @@ The function you provide into this hook must return either a `Model` or `QuerySe --- - **`postprocessor`** + **`#!python postprocessor`** {% include-markdown "../../includes/orm.md" start="" end="" %} However, if you... 1. Want to use this hook to defer IO intensive tasks to be computed in the background - 2. Want to to utilize `use_query` with a different ORM + 2. Want to to utilize `#!python use_query` with a different ORM - ... then you can either set a custom `postprocessor`, or disable all postprocessing behavior by modifying the `QueryOptions.postprocessor` parameter. In the example below, we will set the `postprocessor` to `None` to disable postprocessing behavior. + ... then you can either set a custom `#!python postprocessor`, or disable all postprocessing behavior by modifying the `#!python QueryOptions.postprocessor` parameter. In the example below, we will set the `#!python postprocessor` to `#!python None` to disable postprocessing behavior. === "components.py" @@ -98,11 +98,11 @@ The function you provide into this hook must return either a `Model` or `QuerySe {% include "../../python/use-query-postprocessor-disable.py" %} ``` - If you wish to create a custom `postprocessor`, you will need to create a callable. + If you wish to create a custom `#!python postprocessor`, you will need to create a callable. - The first argument of `postprocessor` must be the query `data`. All proceeding arguments - are optional `postprocessor_kwargs` (see below). This `postprocessor` must return - the modified `data`. + The first argument of `#!python postprocessor` must be the query `#!python data`. All proceeding arguments + are optional `#!python postprocessor_kwargs` (see below). This `#!python postprocessor` must return + the modified `#!python data`. === "components.py" @@ -112,13 +112,13 @@ The function you provide into this hook must return either a `Model` or `QuerySe --- - **`postprocessor_kwargs`** + **`#!python postprocessor_kwargs`** {% include-markdown "../../includes/orm.md" start="" end="" %} - However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second `use_query` hook. + However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second `#!python use_query` hook. - You can disable the prefetching behavior of the default `postprocessor` (located at `reactpy_django.utils.django_query_postprocessor`) via the `QueryOptions.postprocessor_kwargs` parameter. + You can disable the prefetching behavior of the default `#!python postprocessor` (located at `#!python reactpy_django.utils.django_query_postprocessor`) via the `#!python QueryOptions.postprocessor_kwargs` parameter. === "components.py" @@ -130,7 +130,7 @@ The function you provide into this hook must return either a `Model` or `QuerySe ??? question "Can I define async query functions?" - Async functions are supported by `use_query`. You can use them in the same way as a sync query function. + Async functions are supported by `#!python use_query`. You can use them in the same way as a sync query function. However, be mindful of Django async ORM restrictions. @@ -146,9 +146,9 @@ The function you provide into this hook must return either a `Model` or `QuerySe ## Use Mutation -The `use_mutation` hook is used to create, update, or delete Django ORM objects. +This hook is used to [create, update, or delete](https://www.sumologic.com/glossary/crud/) Django ORM objects. -The function you provide into this hook will have no return value. +The mutation function you provide should have no return value. === "components.py" @@ -168,18 +168,18 @@ The function you provide into this hook will have no return value. | Name | Type | Description | Default | | --- | --- | --- | --- | - | `mutate` | `Callable[_Params, bool | None]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `False`, then your `refetch` function will not be used. | N/A | - | `refetch` | `Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A `query` function (used by the `use_query` hook) or a sequence of `query` functions that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. | `None` | + | `#!python mutate` | `#!python Callable[_Params, bool | None]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `#!python False`, then your `#!python refetch` function will not be used. | N/A | + | `#!python refetch` | `#!python Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A `#!python query` function (used by the `#!python use_query` hook) or a sequence of `#!python query` functions that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `Mutation[_Params]` | An object containing `loading`/`error` states, a `reset` callable that will set `loading`/`error` states to defaults, and a `execute` callable that will run the query. | + | `#!python Mutation[_Params]` | An object containing `#!python loading`/`#!python error` states, a `#!python reset` callable that will set `#!python loading`/`#!python error` states to defaults, and a `#!python execute` callable that will run the query. | ??? question "How can I provide arguments to my mutation function?" - `*args` and `**kwargs` can be provided to your mutation function via `mutation.execute` parameters. + `#!python *args` and `#!python **kwargs` can be provided to your mutation function via #!python mutation.execute` parameters. === "components.py" @@ -187,13 +187,13 @@ The function you provide into this hook will have no return value. {% include "../../python/use-mutation-args-kwargs.py" %} ``` -??? question "Can `use_mutation` trigger a refetch of `use_query`?" +??? question "Can `#!python use_mutation` trigger a refetch of `#!python use_query`?" - Yes, `use_mutation` can queue a refetch of a `use_query` via the `refetch=...` argument. + Yes, `#!python use_mutation` can queue a refetch of a `#!python use_query` via the `#!python refetch=...` argument. - The example below is a merge of the `use_query` and `use_mutation` examples above with the addition of a `refetch` argument on `use_mutation`. + The example below is a merge of the `#!python use_query` and `#!python use_mutation` examples above with the addition of a `#!python use_mutation(refetch=...)` argument. - Please note that any `use_query` hooks that use `get_items` will be refetched upon a successful mutation. + Please note that any `#!python use_query` hooks that use `#!python get_items` will be refetched upon a successful mutation. === "components.py" @@ -207,11 +207,11 @@ The function you provide into this hook will have no return value. {% include "../../python/example/models.py" %} ``` -??? question "Can I make a failed `use_mutation` try again?" +??? question "Can I make a failed `#!python use_mutation` try again?" - Yes, a `use_mutation` can be re-performed by calling `reset()` on your `use_mutation` instance. + Yes, a `#!python use_mutation` can be re-performed by calling `#!python reset()` on your `#!python use_mutation` instance. - For example, take a look at `reset_event` below. + For example, take a look at `#!python reset_event` below. === "components.py" @@ -231,7 +231,7 @@ The function you provide into this hook will have no return value. ## Use Connection -You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_connection`. +This hook is used to fetch the Django Channels [WebSocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer). === "components.py" @@ -243,17 +243,17 @@ You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en **Parameters** - `None` + `#!python None` **Returns** | Type | Description | | --- | --- | - | `Connection` | The component's websocket. | + | `#!python Connection` | The component's WebSocket. | ## Use Scope -This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). +This is a shortcut that returns the WebSocket's [`#!python scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). === "components.py" @@ -265,17 +265,17 @@ This is a shortcut that returns the Websocket's [`scope`](https://channels.readt **Parameters** - `None` + `#!python None` **Returns** | Type | Description | | --- | --- | - | `MutableMapping[str, Any]` | The websocket's `scope`. | + | `#!python MutableMapping[str, Any]` | The WebSocket's `#!python scope`. | ## Use Location -This is a shortcut that returns the Websocket's `path`. +This is a shortcut that returns the WebSocket's `#!python path`. You can expect this hook to provide strings such as `/reactpy/my_path`. @@ -289,13 +289,13 @@ You can expect this hook to provide strings such as `/reactpy/my_path`. **Parameters** - `None` + `#!python None` **Returns** | Type | Description | | --- | --- | - | `Location` | An object containing the current URL's `pathname` and `search` query. | + | `#!python Location` | An object containing the current URL's `#!python pathname` and `#!python search` query. | ??? info "This hook's behavior will be changed in a future update" @@ -305,7 +305,7 @@ You can expect this hook to provide strings such as `/reactpy/my_path`. ## Use Origin -This is a shortcut that returns the Websocket's `origin`. +This is a shortcut that returns the WebSocket's `#!python origin`. You can expect this hook to provide strings such as `http://example.com`. @@ -319,10 +319,10 @@ You can expect this hook to provide strings such as `http://example.com`. **Parameters** - `None` + `#!python None` **Returns** | Type | Description | | --- | --- | - | `str | None` | A string containing the browser's current origin, obtained from websocket headers (if available). | + | `#!python str | None` | A string containing the browser's current origin, obtained from WebSocket headers (if available). | diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index fc55e41f..9f108a0d 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -16,23 +16,23 @@ Your **Django project's** `settings.py` can modify the behavior of ReactPy. ## Primary Configuration - + These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy. | Setting | Default Value | Example Value(s) | Description | | --- | --- | --- | --- | -| `REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache.
We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). | -| `REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database used to store ReactPy session data. ReactPy requires a multiprocessing-safe and thread-safe database.
If configuring `REACTPY_DATABASE`, it is mandatory to use our database router like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` | -| `REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy session data, such as `args` and `kwargs` passed into your component template tag.
Use `#!python 0` to not store any session data. | -| `REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy websocket and HTTP URLs. | -| `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python None`, `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the parameters must contain the arg `#!python data`. Set `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. | -| `REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `AuthMiddlewareStack` and...
2. You are using Django's `AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `backend` attribute. | -| `REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the webserver to process web traffic while during ReactPy rendering.
Vastly improves throughput with web servers such as `hypercorn` and `uvicorn`. | -| `REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing.
You can use the `host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. | -| `REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. | -| `REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. | -| `REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. | -| `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | Multiplier for the time between client reconnection attempts. On each reconnection attempt, the `REACTPY_RECONNECT_INTERVAL` will be multiplied by this to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. | - - +| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). | +| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database used to store ReactPy session data. ReactPy requires a multiprocessing-safe and thread-safe database. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` | +| `#!python REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy session data, such as `#!python args` and `#!python kwargs` passed into your component template tag. Use `#!python 0` to not store any session data. | +| `#!python REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy WebSocket and HTTP URLs. | +| `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python None`, `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the parameters must contain the arg `#!python data`. Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. | +| `#!python REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `#!python AuthMiddlewareStack` and...
2. You are using Django's `#!python AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `#!python backend` attribute. | +| `#!python REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the web server to process traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). | +| `#!python REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. | +| `#!python REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. | +| `#!python REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. | +| `#!python REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. | +| `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | Multiplier for the time between client reconnection attempts. On each reconnection attempt, the `#!python REACTPY_RECONNECT_INTERVAL` will be multiplied by this to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. | + + diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index e04c5bb9..6f20be36 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -10,7 +10,7 @@ Django template tags can be used within your HTML templates to provide ReactPy f ## Component -The `component` template tag can be used to insert any number of ReactPy components onto your page. +This template tag can be used to insert any number of ReactPy components onto your page. === "my-template.html" @@ -22,18 +22,18 @@ The `component` template tag can be used to insert any number of ReactPy compone | Name | Type | Description | Default | | --- | --- | --- | --- | - | `dotted_path` | `str` | The dotted path to the component to render. | N/A | - | `*args` | `Any` | The positional arguments to provide to the component. | N/A | - | `class` | `str | None` | The HTML class to apply to the top-level component div. | `None` | - | `key` | `str | None` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `key` within a template tag is effectively useless. | `None` | - | `host` | `str | None` | The host to use for the ReactPy connections. If set to `None`, the host will be automatically configured.
Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `None` | - | `**kwargs` | `Any` | The keyword arguments to provide to the component. | N/A | + | `#!python dotted_path` | `#!python str` | The dotted path to the component to render. | N/A | + | `#!python *args` | `#!python Any` | The positional arguments to provide to the component. | N/A | + | `#!python class` | `#!python str | None` | The HTML class to apply to the top-level component div. | `#!python None` | + | `#!python key` | `#!python str | None` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `#!python key` within a template tag is effectively useless. | `#!python None` | + | `#!python host` | `#!python str | None` | The host to use for the ReactPy connections. If set to `#!python None`, the host will be automatically configured.
Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `#!python None` | + | `#!python **kwargs` | `#!python Any` | The keyword arguments to provide to the component. | N/A | **Returns** | Type | Description | | --- | --- | - | `Component` | A ReactPy component. | + | `#!python Component` | A ReactPy component. | @@ -65,7 +65,7 @@ The `component` template tag can be used to insert any number of ReactPy compone ??? question "Can I render components on a different server (distributed computing)?" - Yes! By using the `host` keyword argument, you can render components from a completely separate ASGI server. + Yes! By using the `#!python host` keyword argument, you can render components from a completely separate ASGI server. === "my-template.html" @@ -80,8 +80,8 @@ The `component` template tag can be used to insert any number of ReactPy compone Here's a couple of things to keep in mind: 1. If your host address are completely separate ( `origin1.com != origin2.com` ) you will need to [configure CORS headers](https://pypi.org/project/django-cors-headers/) on your main application during deployment. - 2. You will not need to register ReactPy HTTP or websocket paths on any applications that do not perform any component rendering. - 3. Your component will only be able to access `*args`/`**kwargs` you provide to the template tag if your applications share a common database. + 2. You will not need to register ReactPy HTTP or WebSocket paths on any applications that do not perform any component rendering. + 3. Your component will only be able to access `#!python *args`/`#!python **kwargs` you provide to the template tag if your applications share a common database. @@ -112,7 +112,7 @@ The `component` template tag can be used to insert any number of ReactPy compone ??? question "Can I use positional arguments instead of keyword arguments?" - You can use any combination of `*args`/`**kwargs` in your template tag. + You can use any combination of `#!python *args`/`#!python **kwargs` in your template tag. === "my-template.html" diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md index 0b1632ca..d71facc2 100644 --- a/docs/src/reference/utils.md +++ b/docs/src/reference/utils.md @@ -10,9 +10,9 @@ Utility functions provide various miscellaneous functionality. These are typical ## Django Query Postprocessor -This is the default postprocessor for the `use_query` hook. +This is the default postprocessor for the `#!python use_query` hook. -This postprocessor is designed to avoid Django's `SynchronousOnlyException` by recursively fetching all fields within a `Model` or `QuerySet` to prevent [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy). +This postprocessor is designed to avoid Django's `#!python SynchronousOnlyException` by recursively fetching all fields within a `#!python Model` or `#!python QuerySet` to prevent [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy). === "components.py" @@ -32,21 +32,19 @@ This postprocessor is designed to avoid Django's `SynchronousOnlyException` by r | Name | Type | Description | Default | | --- | --- | --- | --- | - | `data` | `QuerySet | Model` | The `Model` or `QuerySet` to recursively fetch fields from. | N/A | - | `many_to_many` | `bool` | Whether or not to recursively fetch `ManyToManyField` relationships. | `True` | - | `many_to_one` | `bool` | Whether or not to recursively fetch `ForeignKey` relationships. | `True` | + | `#!python data` | `#!python QuerySet | Model` | The `#!python Model` or `#!python QuerySet` to recursively fetch fields from. | N/A | + | `#!python many_to_many` | `#!python bool` | Whether or not to recursively fetch `#!python ManyToManyField` relationships. | `#!python True` | + | `#!python many_to_one` | `#!python bool` | Whether or not to recursively fetch `#!python ForeignKey` relationships. | `#!python True` | **Returns** | Type | Description | | --- | --- | - | `QuerySet | Model` | The `Model` or `QuerySet` with all fields fetched. | + | `#!python QuerySet | Model` | The `#!python Model` or `#!python QuerySet` with all fields fetched. | ## Register Component -The `register_component` function is used manually register a root component with ReactPy. - -You should always call `register_component` within a Django [`AppConfig.ready()` method](https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready) to retain compatibility with ASGI webserver workers. +This function is used manually register a root component with ReactPy. === "apps.py" @@ -54,12 +52,14 @@ You should always call `register_component` within a Django [`AppConfig.ready()` {% include "../../python/register-component.py" %} ``` -??? question "Do I need to register my components?" +??? warning "Only use this within `#!python AppConfig.ready()`" + + You should always call `#!python register_component` within a Django [`#!python AppConfig.ready()` method](https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. + +??? question "Do I need to use this?" You typically will not need to use this function. For security reasons, ReactPy does not allow non-registered components to be root components. However, all components contained within Django templates are automatically considered root components. - You only need to use this function if your host application does not contain any HTML templates that [reference](../reference/template-tag.md#component) your components. - - A common scenario where this is needed is when you are modifying the [template tag `host = ...` argument](../reference/template-tag.md#component) in order to configure a dedicated Django application as a rendering server for ReactPy. On this dedicated rendering server, you would need to manually register your components. + This is typically only needed when you have a dedicated Django application as a rendering server that doesn't have templates, such as when modifying the [template tag `#!python host` argument](../reference/template-tag.md#component). On this dedicated rendering server, you would need to manually register your components. diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py index adc437d0..0025008b 100644 --- a/src/reactpy_django/checks.py +++ b/src/reactpy_django/checks.py @@ -60,7 +60,7 @@ def reactpy_warnings(app_configs, **kwargs): Warning( "Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled " "and you running with Daphne.", - hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different webserver.", + hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different web server.", id="reactpy_django.W003", ) ) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index a99da927..df552e42 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -24,7 +24,7 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" -# Run in production mode when using a real webserver +# Run in production mode when using a real web server DEBUG = all( not sys.argv[0].endswith(substring) for substring in {"hypercorn", "uvicorn", "daphne"} From f83f04218e0013d7f1af093a7f2a571e9c48c8a4 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 7 Sep 2023 01:29:53 -0700 Subject: [PATCH 29/32] canned code demos --- .../add-interactivity-demo.html | 165 +++++++++++++ .../home-code-examples/code-block.html | 7 +- .../create-user-interfaces-demo.html | 24 ++ .../write-components-with-python-demo.html | 65 ++++++ docs/overrides/home.html | 18 +- docs/src/assets/css/code.css | 1 + docs/src/assets/css/home.css | 220 +++++++++++++++++- 7 files changed, 484 insertions(+), 16 deletions(-) create mode 100644 docs/overrides/home-code-examples/add-interactivity-demo.html create mode 100644 docs/overrides/home-code-examples/create-user-interfaces-demo.html create mode 100644 docs/overrides/home-code-examples/write-components-with-python-demo.html diff --git a/docs/overrides/home-code-examples/add-interactivity-demo.html b/docs/overrides/home-code-examples/add-interactivity-demo.html new file mode 100644 index 00000000..d9e99579 --- /dev/null +++ b/docs/overrides/home-code-examples/add-interactivity-demo.html @@ -0,0 +1,165 @@ +
+
+ +
+
+ + + + example.com/videos.html +
+
+ +
+
+

Searchable Videos

+

Type a search query below.

+ +
+ +

5 Videos

+ +
+
+ + + +
+
+

ReactPy: The Documentary

+

From web library to taco delivery service

+
+ +
+ +
+
+ + + +
+
+

Code using Worst Practices

+

Harriet Potter (2013)

+
+ +
+ +
+
+ + + +
+
+

Introducing ReactPy Foriegn

+

Tim Cooker (2015)

+
+ +
+ +
+
+ + + +
+
+

Introducing ReactPy Cooks

+

Soap Boat and Dinosaur Dan (2018)

+
+ +
+ +
+
+ + + +
+
+

Introducing Quantum Components

+

Isaac Asimov and Lauren-kun (2020)

+
+ +
+
+ + +
+
diff --git a/docs/overrides/home-code-examples/code-block.html b/docs/overrides/home-code-examples/code-block.html index 5ee471be..c1f14e5d 100644 --- a/docs/overrides/home-code-examples/code-block.html +++ b/docs/overrides/home-code-examples/code-block.html @@ -1,7 +1,6 @@ -
- -
+
+ +
diff --git a/docs/overrides/home-code-examples/create-user-interfaces-demo.html b/docs/overrides/home-code-examples/create-user-interfaces-demo.html new file mode 100644 index 00000000..9a684d3c --- /dev/null +++ b/docs/overrides/home-code-examples/create-user-interfaces-demo.html @@ -0,0 +1,24 @@ +
+
+
+
+ + + +
+
+

My video

+

Video description

+
+ +
+
+
diff --git a/docs/overrides/home-code-examples/write-components-with-python-demo.html b/docs/overrides/home-code-examples/write-components-with-python-demo.html new file mode 100644 index 00000000..203287c6 --- /dev/null +++ b/docs/overrides/home-code-examples/write-components-with-python-demo.html @@ -0,0 +1,65 @@ +
+
+

3 Videos

+
+
+ + + +
+
+

First video

+

Video description

+
+ +
+
+
+ + + +
+
+

Second video

+

Video description

+
+ +
+
+
+ + + +
+
+

Third video

+

Video description

+
+ +
+
+
diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 2fde8507..3664c44f 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -16,6 +16,7 @@ --row-bg-color: conic-gradient(from -90deg at 110% 100%, #2b303b 0deg, #16181d 90deg, #16181d 1turn); --stripe-border-color: rgba(246, 247, 249, 0.1); --code-block-filter: none; + --home-tabbed-set-bg-color: #1f1f1f; } [data-md-color-scheme="default"] { @@ -27,6 +28,7 @@ --stripe-border-color: rgba(35, 39, 47, 0.1); --code-block-filter: invert(1) contrast(1.3) hue-rotate(180deg) saturate(2); --code-tab-color: rgb(246 247 249); + --home-tabbed-set-bg-color: #fff; } /* Application header should be static for the landing page */ @@ -66,10 +68,11 @@

Create user interfaces from components

components like thumbnail, like_button, and video. Then combine them into entire screens, pages, and apps.

-
- {% with image="create-user-interfaces.png", num=1 %} +
+ {% with image="create-user-interfaces.png", class="pop-left" %} {% include "home-code-examples/code-block.html" %} {% endwith %} + {% include "home-code-examples/create-user-interfaces-demo.html" %}

Whether you work on your own or with thousands of other developers, using React feels the same. It is @@ -86,10 +89,12 @@

Write components with pure Python code

list comprehension. Learning ReactPy is learning programming.

-
- {% with image="write-components-with-python.png", num=2 %} +
+ {% with image="write-components-with-python.png", class="pop-left" %} {% include "home-code-examples/code-block.html" %} {% endwith %} + {% include "home-code-examples/write-components-with-python-demo.html" %} +
@@ -100,10 +105,11 @@

Add interactivity wherever you need it

response to an interaction, like when the user types into an input. ReactPy will then update the screen to match the new data.

-
- {% with image="add-interactivity.png", num=3 %} +
+ {% with image="add-interactivity.png" %} {% include "home-code-examples/code-block.html" %} {% endwith %} + {% include "home-code-examples/add-interactivity-demo.html" %}

You don't have to build your whole page in ReactPy. Add React to your existing HTML page, and render diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css index 4949ef72..d1556dc0 100644 --- a/docs/src/assets/css/code.css +++ b/docs/src/assets/css/code.css @@ -59,6 +59,7 @@ box-shadow: var(--md-code-backdrop); border-radius: 8px; overflow: hidden; + border: 1px solid var(--md-default-fg-color--lightest); } .md-typeset .tabbed-set .tabbed-block { overflow: hidden; diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index c9c944d8..38c59fa0 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -38,13 +38,13 @@ img.home-logo { margin-bottom: -0.75rem; } -.home .row p { +.home .row > p { max-width: 35rem; line-height: 1.5; font-weight: 400; } -.home .row.first p { +.home .row.first > p { font-size: 32px; font-weight: 500; } @@ -54,10 +54,10 @@ img.home-logo { .home .row { text-align: center; } - .home .row p { + .home .row > p { font-size: 21px; } - .home .row h1 { + .home .row > h1 { font-size: 52px; } } @@ -79,10 +79,28 @@ img.home-logo { } /* Code blocks */ +.home .row .tabbed-set { + background: var(--home-tabbed-set-bg-color); + margin: 0; +} + +.home .row .pop-left { + margin-left: -20px; + margin-right: 0; + margin-top: -20px; + margin-bottom: -20px; +} + +.home .row .pop-right { + margin-left: 0px; + margin-right: 0px; + margin-top: -20px; + margin-bottom: -20px; +} + .home .row .tabbed-content { - background: #1f1f1f; padding: 20px 18px; - width: 610px; + overflow-x: auto; } .home .row .tabbed-content img { @@ -91,9 +109,199 @@ img.home-logo { -webkit-user-drag: none; -webkit-user-select: none; -ms-user-select: none; + max-width: 580px; } .home .row .tabbed-content { -webkit-filter: var(--code-block-filter); filter: var(--code-block-filter); } + +/* Code examples */ +.home .example-container { + background: radial-gradient( + circle at 0% 100%, + rgb(41 84 147 / 11%) 0%, + rgb(22 89 189 / 4%) 70%, + rgb(48 99 175 / 0%) 80% + ), + radial-gradient( + circle at 100% 100%, + rgb(24 87 45 / 55%) 0%, + rgb(29 61 12 / 4%) 70%, + rgb(94 116 93 / 0%) 80% + ), + radial-gradient( + circle at 100% 0%, + rgba(54, 66, 84, 0.55) 0%, + rgb(102 111 125 / 4%) 70%, + rgba(54, 66, 84, 0) 80% + ), + radial-gradient( + circle at 0% 0%, + rgba(91, 114, 135, 0.55) 0%, + rgb(45 111 171 / 4%) 70%, + rgb(5 82 153 / 0%) 80% + ), + rgb(0, 0, 0) center center/cover no-repeat fixed; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: center; + border-radius: 16px; + margin: 30px 0; + max-width: 100%; + -moz-column-gap: 20px; + column-gap: 20px; + padding-left: 20px; + padding-right: 20px; +} + +.home .demo .white-bg { + background: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + max-width: 590px; + min-width: -webkit-min-content; + min-width: -moz-min-content; + min-width: min-content; + row-gap: 1rem; + padding: 1rem; +} + +.home .demo .vid-row { + display: flex; + flex-direction: row; + -moz-column-gap: 12px; + column-gap: 12px; +} + +.home .demo { + color: #000; +} + +.home .demo .vid-thumbnail { + background: radial-gradient( + circle at 0% 100%, + rgb(41 84 147 / 55%) 0%, + rgb(22 89 189 / 4%) 70%, + rgb(48 99 175 / 0%) 80% + ), + radial-gradient( + circle at 100% 100%, + rgb(24 63 87 / 55%) 0%, + rgb(29 61 12 / 4%) 70%, + rgb(94 116 93 / 0%) 80% + ), + radial-gradient( + circle at 100% 0%, + rgba(54, 66, 84, 0.55) 0%, + rgb(102 111 125 / 4%) 70%, + rgba(54, 66, 84, 0) 80% + ), + radial-gradient( + circle at 0% 0%, + rgba(91, 114, 135, 0.55) 0%, + rgb(45 111 171 / 4%) 70%, + rgb(5 82 153 / 0%) 80% + ), + rgb(0, 0, 0) center center/cover no-repeat fixed; + width: 9rem; + aspect-ratio: 16 / 9; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; +} + +.home .demo .vid-text { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + width: 100%; +} + +.home .demo h2 { + font-size: 18px; + line-height: 1.375; + margin: 0; + text-align: left; + font-weight: 700; +} + +.home .demo h3 { + font-size: 16px; + line-height: 1.25; + margin: 0; +} + +.home .demo p { + font-size: 14px; + line-height: 1.375; + margin: 0; +} + +.home .demo .browser-nav-url { + background: rgba(153, 161, 179, 0.2); + border-radius: 9999px; + font-size: 14px; + color: grey; + display: flex; + align-items: center; + justify-content: center; + -moz-column-gap: 5px; + column-gap: 5px; +} + +.home .demo .browser-navbar { + margin: -1rem; + margin-bottom: 0; + padding: 0.75rem 1rem; + border-bottom: 1px solid darkgrey; +} + +.home .demo .browser-viewport { + background: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + row-gap: 1rem; + height: 400px; + overflow-y: scroll; + margin: -1rem; + padding: 1rem; +} + +.home .demo .browser-viewport .search-header > h1 { + color: #000; + text-align: left; + font-size: 24px; + margin: 0; +} + +.home .demo .browser-viewport .search-header > p { + text-align: left; + font-size: 16px; + margin: 10px 0; +} + +.home .demo .search-bar input { + width: 100%; + background: rgba(153, 161, 179, 0.2); + border-radius: 9999px; + padding-left: 40px; + padding-right: 40px; + height: 40px; + color: #000; +} + +.home .demo .search-bar svg { + height: 40px; + position: absolute; + transform: translateX(75%); +} + +.home .demo .search-bar { + position: relative; +} From a9cd03bf80254c0811d8263335306ec19974706e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 7 Sep 2023 01:58:55 -0700 Subject: [PATCH 30/32] mobile code demos --- docs/src/assets/css/home.css | 118 ++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index 38c59fa0..c72e7093 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -49,55 +49,12 @@ img.home-logo { font-weight: 500; } -/* Desktop Styling */ -@media screen and (min-width: 60em) { - .home .row { - text-align: center; - } - .home .row > p { - font-size: 21px; - } - .home .row > h1 { - font-size: 52px; - } -} - -/* Mobile Styling */ -@media screen and (max-width: 60em) { - .home .row { - padding: 4rem 0.8rem; - } - .home .row.first { - padding-top: 2rem; - } - .home-btns { - width: 100%; - display: grid; - grid-gap: 0.5rem; - gap: 0.5rem; - } -} - /* Code blocks */ .home .row .tabbed-set { background: var(--home-tabbed-set-bg-color); margin: 0; } -.home .row .pop-left { - margin-left: -20px; - margin-right: 0; - margin-top: -20px; - margin-bottom: -20px; -} - -.home .row .pop-right { - margin-left: 0px; - margin-right: 0px; - margin-top: -20px; - margin-bottom: -20px; -} - .home .row .tabbed-content { padding: 20px 18px; overflow-x: auto; @@ -150,8 +107,7 @@ img.home-logo { border-radius: 16px; margin: 30px 0; max-width: 100%; - -moz-column-gap: 20px; - column-gap: 20px; + grid-column-gap: 20px; padding-left: 20px; padding-right: 20px; } @@ -305,3 +261,75 @@ img.home-logo { .home .demo .search-bar { position: relative; } + +/* Desktop Styling */ +@media screen and (min-width: 60em) { + .home .row { + text-align: center; + } + .home .row > p { + font-size: 21px; + } + .home .row > h1 { + font-size: 52px; + } + .home .row .pop-left { + margin-left: -20px; + margin-right: 0; + margin-top: -20px; + margin-bottom: -20px; + } + .home .row .pop-right { + margin-left: 0px; + margin-right: 0px; + margin-top: -20px; + margin-bottom: -20px; + } +} + +/* Mobile Styling */ +@media screen and (max-width: 60em) { + .home .row { + padding: 4rem 0.8rem; + } + .home .row > h1, + .home .row > p { + padding-left: 1rem; + padding-right: 1rem; + } + .home .row.first { + padding-top: 2rem; + } + .home-btns { + width: 100%; + display: grid; + grid-gap: 0.5rem; + gap: 0.5rem; + } + .home .example-container { + display: flex; + flex-direction: column; + row-gap: 20px; + width: 100%; + justify-content: center; + border-radius: 0; + padding: 1rem 0; + } + .home .row { + padding-left: 0; + padding-right: 0; + } + .home .tabbed-set { + width: 100%; + border-radius: 0; + } + .home .demo { + width: 100%; + display: flex; + justify-content: center; + } + .home .demo > .white-bg { + width: 80%; + max-width: 80%; + } +} From 9e89f0528b7dc720dcff03f5790493eccc4aa95f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 7 Sep 2023 02:28:46 -0700 Subject: [PATCH 31/32] version selector styling --- docs/src/assets/css/navbar.css | 40 +++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/docs/src/assets/css/navbar.css b/docs/src/assets/css/navbar.css index f42b13bc..32347ccb 100644 --- a/docs/src/assets/css/navbar.css +++ b/docs/src/assets/css/navbar.css @@ -17,10 +17,20 @@ transition: border-color 0.35s cubic-bezier(0.1, 0.7, 0.1, 1); } -.md-header__title { +/* Version selector */ +.md-header__topic .md-ellipsis, +.md-header__title [data-md-component="header-topic"] { display: none; } +[dir="ltr"] .md-version__current { + margin: 0; +} + +.md-version__list { + margin: 0.2rem -0.8rem; +} + /* Mobile Styling */ @media screen and (max-width: 60em) { label.md-header__button.md-icon[for="__drawer"] { @@ -31,15 +41,18 @@ order: 2; margin-right: auto; } - .md-header__button[for="__search"] { + .md-header__title { order: 3; } - .md-header__option[data-md-component="palette"] { + .md-header__button[for="__search"] { order: 4; } + .md-header__option[data-md-component="palette"] { + order: 5; + } .md-header__source { display: initial; - order: 5; + order: 6; } .md-header__source .md-source__repository { display: none; @@ -68,9 +81,20 @@ height: 2rem; } + /* Version selector */ + [dir="ltr"] .md-header__title { + order: 2; + margin: 0; + margin-right: 0.8rem; + margin-left: 0.2rem; + } + .md-header__topic { + position: relative; + } + /* Search */ .md-search { - order: 2; + order: 3; width: 100%; margin-right: 0.6rem; } @@ -89,7 +113,7 @@ /* Tabs */ .md-tabs { - order: 3; + order: 4; min-width: -webkit-fit-content; min-width: -moz-fit-content; min-width: fit-content; @@ -118,12 +142,12 @@ /* Dark/Light Selector */ .md-header__option[data-md-component="palette"] { - order: 4; + order: 5; } /* GitHub info */ .md-header__source { - order: 5; + order: 6; margin-left: 0 !important; } } From e456b5e7982660e13a910dcfb60689cfb209d513 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 7 Sep 2023 02:28:57 -0700 Subject: [PATCH 32/32] fix mobile navbar github styling --- docs/src/assets/css/main.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css index bcfb4f27..500ae4be 100644 --- a/docs/src/assets/css/main.css +++ b/docs/src/assets/css/main.css @@ -6,6 +6,11 @@ --reactpy-color-opacity-10: rgb(88 185 98 / 10%); } +[data-md-color-accent="red"] { + --md-primary-fg-color--light: var(--reactpy-color); + --md-primary-fg-color--dark: var(--reactpy-color-dark); +} + [data-md-color-scheme="slate"] { --md-default-bg-color: rgb(35 39 47); --md-default-bg-color--light: hsla(var(--md-hue), 15%, 16%, 0.54);