diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..094c32693
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,35 @@
+# http://editorconfig.org
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+end_of_line = lf
+
+[*.py]
+indent_size = 4
+max_line_length = 120
+
+[*.md]
+indent_size = 4
+
+[*.yml]
+indent_size = 4
+
+[*.html]
+max_line_length = off
+
+[*.js]
+max_line_length = off
+
+[*.css]
+indent_size = 4
+max_line_length = off
+
+# Tests can violate line width restrictions in the interest of clarity.
+[**/test_*.py]
+max_line_length = off
diff --git a/.github/workflows/.hatch-run.yml b/.github/workflows/.hatch-run.yml
deleted file mode 100644
index b312869e4..000000000
--- a/.github/workflows/.hatch-run.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-name: hatch-run
-
-on:
- workflow_call:
- inputs:
- job-name:
- required: true
- type: string
- hatch-run:
- required: true
- type: string
- runs-on-array:
- required: false
- type: string
- default: '["ubuntu-latest"]'
- python-version-array:
- required: false
- type: string
- default: '["3.x"]'
- node-registry-url:
- required: false
- type: string
- default: ""
- secrets:
- node-auth-token:
- required: false
- pypi-username:
- required: false
- pypi-password:
- required: false
-
-jobs:
- hatch:
- name: ${{ format(inputs.job-name, matrix.python-version, matrix.runs-on) }}
- strategy:
- matrix:
- python-version: ${{ fromJson(inputs.python-version-array) }}
- runs-on: ${{ fromJson(inputs.runs-on-array) }}
- runs-on: ${{ matrix.runs-on }}
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2
- with:
- node-version: "14.x"
- registry-url: ${{ inputs.node-registry-url }}
- - name: Pin NPM Version
- run: npm install -g npm@8.19.3
- - name: Use Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install Python Dependencies
- run: pip install hatch poetry
- - name: Run Scripts
- env:
- NODE_AUTH_TOKEN: ${{ secrets.node-auth-token }}
- PYPI_USERNAME: ${{ secrets.pypi-username }}
- PYPI_PASSWORD: ${{ secrets.pypi-password }}
- run: hatch run ${{ inputs.hatch-run }}
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
deleted file mode 100644
index af768579c..000000000
--- a/.github/workflows/check.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-name: check
-
-on:
- push:
- branches:
- - main
- pull_request:
- branches:
- - main
- schedule:
- - cron: "0 0 * * 0"
-
-jobs:
- test-py-cov:
- uses: ./.github/workflows/.hatch-run.yml
- with:
- job-name: "python-{0}"
- hatch-run: "test-py"
- lint-py:
- uses: ./.github/workflows/.hatch-run.yml
- with:
- job-name: "python-{0}"
- hatch-run: "lint-py"
- test-py-matrix:
- uses: ./.github/workflows/.hatch-run.yml
- with:
- job-name: "python-{0} {1}"
- hatch-run: "test-py --no-cov"
- runs-on-array: '["ubuntu-latest", "macos-latest", "windows-latest"]'
- python-version-array: '["3.9", "3.10", "3.11"]'
- test-docs:
- uses: ./.github/workflows/.hatch-run.yml
- with:
- job-name: "python-{0}"
- hatch-run: "test-docs"
- test-js:
- uses: ./.github/workflows/.hatch-run.yml
- with:
- job-name: "{1}"
- hatch-run: "test-js"
- lint-js:
- uses: ./.github/workflows/.hatch-run.yml
- with:
- job-name: "{1}"
- hatch-run: "lint-js"
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index b4f77ee00..000000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,71 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: codeql
-
-on:
- push:
- branches: [main]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [main]
- schedule:
- - cron: "43 3 * * 3"
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: ["javascript", "python"]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
- # Learn more:
- # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v2
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v1
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v1
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 https://git.io/JvXDl
-
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
- # and modify them (or add more) to build your code if your project
- # uses a compiled language
-
- #- run: |
- # make bootstrap
- # make release
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
deleted file mode 100644
index 7337f505b..000000000
--- a/.github/workflows/deploy-docs.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-# This workflows will upload a Python Package using Twine when a release is created
-# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
-
-name: deploy-docs
-
-on:
- push:
- branches:
- - "main"
- tags:
- - "*"
-
-jobs:
- deploy-documentation:
- runs-on: ubuntu-latest
- steps:
- - name: Check out src from Git
- uses: actions/checkout@v2
- - name: Get history and tags for SCM versioning to work
- run: |
- git fetch --prune --unshallow
- git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- - name: Login to Heroku Container Registry
- run: echo ${{ secrets.HEROKU_API_KEY }} | docker login -u ${{ secrets.HEROKU_EMAIL }} --password-stdin registry.heroku.com
- - name: Build Docker Image
- run: docker build . --file docs/Dockerfile --tag registry.heroku.com/${{ secrets.HEROKU_APP_NAME }}/web
- - name: Push Docker Image
- run: docker push registry.heroku.com/${{ secrets.HEROKU_APP_NAME }}/web
- - name: Deploy
- run: HEROKU_API_KEY=${{ secrets.HEROKU_API_KEY }} heroku container:release web --app ${{ secrets.HEROKU_APP_NAME }}
diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
new file mode 100644
index 000000000..199bc1766
--- /dev/null
+++ b/.github/workflows/publish-docs.yml
@@ -0,0 +1,17 @@
+name: Publish Docs
+on:
+ push:
+ branches:
+ - new-docs
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-python@v4
+ with:
+ python-version: 3.x
+ - run: pip install -r docs/requirements.txt
+ - run: cd docs && mkdocs gh-deploy --force
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index e9271cbd5..000000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-# This workflows will upload a Javscript Package using NPM to npmjs.org when a release is created
-# For more information see: https://docs.github.com/en/actions/guides/publishing-nodejs-packages
-
-name: publish
-
-on:
- release:
- types: [published]
-
-jobs:
- publish:
- uses: ./.github/workflows/.hatch-run.yml
- with:
- job-name: "publish"
- hatch-run: "publish"
- node-registry-url: "https://registry.npmjs.org"
- secrets:
- node-auth-token: ${{ secrets.NODE_AUTH_TOKEN }}
- pypi-username: ${{ secrets.PYPI_USERNAME }}
- pypi-password: ${{ secrets.PYPI_PASSWORD }}
diff --git a/.gitignore b/.gitignore
index 20c041e11..788d5a329 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
+# --- Build Artifacts ---
+src/reactpy/static/index.js*
+src/reactpy/static/morphdom/
+src/reactpy/static/pyscript/
+
# --- Jupyter ---
*.ipynb_checkpoints
*Untitled*.ipynb
@@ -11,8 +16,9 @@
.jupyter
# --- Python ---
-.venv
-venv
+.hatch
+.venv*
+venv*
MANIFEST
build
dist
@@ -28,6 +34,7 @@ pip-wheel-metadata
.python-version
# -- Python Tests ---
+.coverage.*
*.coverage
*.pytest_cache
*.mypy_cache
@@ -38,4 +45,3 @@ pip-wheel-metadata
# --- JS ---
node_modules
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..d7bd784cf
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,27 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+
+
+
+
+
+
+## [Unreleased]
+
+- Nothing (yet)
+
+[unreleased]: https://github.com/reactive-python/reactpy/compare/1.0.0...HEAD
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 5caf76c93..000000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2019-2022 Ryan S. Morshead
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 000000000..f5423c3d3
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,9 @@
+## The MIT License (MIT)
+
+#### Copyright (c) Reactive Python and affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
index 83241e19a..d24c250a4 100644
--- a/README.md
+++ b/README.md
@@ -1,72 +1,21 @@
-# ReactPy
+Temporary branch being used to rewrite ReactPy's documentation.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Supported Backends | -|
---|---|
Built-in | -External | -
- - Flask, FastAPI, Sanic, Tornado - - | -- Django, - Jupyter, - Plotly-Dash - | -
{{ config.site_description }}
+ +
+ 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. +
+
+ 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.
+
+ 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. +
++ 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 Starlette. +
+ + Get Started + +Video description
+Video description
+Video description
+Video description
+Just below is an embedded ReactPy view...
- - - - diff --git a/docs/source/guides/getting-started/_static/embed-reactpy-view/main.py b/docs/source/guides/getting-started/_static/embed-reactpy-view/main.py deleted file mode 100644 index 6e3687f27..000000000 --- a/docs/source/guides/getting-started/_static/embed-reactpy-view/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from sanic import Sanic -from sanic.response import file - -from reactpy import component, html -from reactpy.backend.sanic import Options, configure - -app = Sanic("MyApp") - - -@app.route("/") -async def index(request): - return await file("index.html") - - -@component -def ReactPyView(): - return html.code("This text came from an ReactPy App") - - -configure(app, ReactPyView, Options(url_prefix="/_reactpy")) - -app.run(host="127.0.0.1", port=5000) diff --git a/docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png b/docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png deleted file mode 100644 index 7439c83cf..000000000 Binary files a/docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png and /dev/null differ diff --git a/docs/source/guides/getting-started/_static/logo-django.svg b/docs/source/guides/getting-started/_static/logo-django.svg deleted file mode 100644 index 1538f0817..000000000 --- a/docs/source/guides/getting-started/_static/logo-django.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - -]> - diff --git a/docs/source/guides/getting-started/_static/logo-jupyter.svg b/docs/source/guides/getting-started/_static/logo-jupyter.svg deleted file mode 100644 index fb2921a41..000000000 --- a/docs/source/guides/getting-started/_static/logo-jupyter.svg +++ /dev/null @@ -1,88 +0,0 @@ - diff --git a/docs/source/guides/getting-started/_static/logo-plotly.svg b/docs/source/guides/getting-started/_static/logo-plotly.svg deleted file mode 100644 index 3dd95459a..000000000 --- a/docs/source/guides/getting-started/_static/logo-plotly.svg +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif b/docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif deleted file mode 100644 index b420ecd8c..000000000 Binary files a/docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif and /dev/null differ diff --git a/docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif b/docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif deleted file mode 100644 index 61bb8295f..000000000 Binary files a/docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif and /dev/null differ diff --git a/docs/source/guides/getting-started/index.rst b/docs/source/guides/getting-started/index.rst deleted file mode 100644 index dd210be60..000000000 --- a/docs/source/guides/getting-started/index.rst +++ /dev/null @@ -1,123 +0,0 @@ -Getting Started -=============== - -.. toctree:: - :hidden: - - installing-reactpy - running-reactpy - -.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn - :color: info - :animate: fade-in - :open: - - .. grid:: 1 2 2 2 - - .. grid-item-card:: :octicon:`tools` Installing ReactPy - :link: installing-reactpy - :link-type: doc - - Learn how ReactPy can be installed in a variety of different ways - with - different web servers and even in different frameworks. - - .. grid-item-card:: :octicon:`play` Running ReactPy - :link: running-reactpy - :link-type: doc - - See how ReactPy can be run with a variety of different production servers or be - added to existing applications. - -The fastest way to get started with ReactPy is to try it out in a `Juptyer Notebook -+ +{% include-markdown "../../../CHANGELOG.md" start="" end="" %} + +
+ +{% include-markdown "../../../CHANGELOG.md" start="" %} diff --git a/docs/docs_app/__init__.py b/docs/src/about/code.md similarity index 100% rename from docs/docs_app/__init__.py rename to docs/src/about/code.md diff --git a/src/py/reactpy/reactpy/_console/__init__.py b/docs/src/about/community.md similarity index 100% rename from src/py/reactpy/reactpy/_console/__init__.py rename to docs/src/about/community.md diff --git a/src/py/reactpy/reactpy/backend/__init__.py b/docs/src/about/docs.md similarity index 100% rename from src/py/reactpy/reactpy/backend/__init__.py rename to docs/src/about/docs.md diff --git a/src/py/reactpy/reactpy/core/__init__.py b/docs/src/about/licenses.md similarity index 100% rename from src/py/reactpy/reactpy/core/__init__.py rename to docs/src/about/licenses.md diff --git a/src/py/reactpy/reactpy/future.py b/docs/src/about/running-tests.md similarity index 100% rename from src/py/reactpy/reactpy/future.py rename to docs/src/about/running-tests.md diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css new file mode 100644 index 000000000..8b3f06ef6 --- /dev/null +++ b/docs/src/assets/css/admonition.css @@ -0,0 +1,160 @@ +[data-md-color-scheme="slate"] { + --admonition-border-color: transparent; + --admonition-expanded-border-color: rgba(255, 255, 255, 0.1); + --note-bg-color: rgba(43, 110, 98, 0.2); + --terminal-bg-color: #0c0c0c; + --terminal-title-bg-color: #000; + --deep-dive-bg-color: rgba(43, 52, 145, 0.2); + --you-will-learn-bg-color: #353a45; + --pitfall-bg-color: rgba(182, 87, 0, 0.2); +} +[data-md-color-scheme="default"] { + --admonition-border-color: rgba(0, 0, 0, 0.08); + --admonition-expanded-border-color: var(--admonition-border-color); + --note-bg-color: rgb(244, 251, 249); + --terminal-bg-color: rgb(64, 71, 86); + --terminal-title-bg-color: rgb(35, 39, 47); + --deep-dive-bg-color: rgb(243, 244, 253); + --you-will-learn-bg-color: rgb(246, 247, 249); + --pitfall-bg-color: rgb(254, 245, 231); +} + +.md-typeset details, +.md-typeset .admonition { + border-color: var(--admonition-border-color) !important; + box-shadow: none; +} + +.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: none !important; +} + +.md-typeset details[open] { + border-color: var(--admonition-expanded-border-color) !important; +} + +/* +Admonition: "summary" +React Name: "You will learn" +*/ +.md-typeset .admonition.summary { + background: var(--you-will-learn-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .summary .admonition-title { + font-size: 1rem; + background: transparent; + padding-left: 0.6rem; + padding-bottom: 0; +} + +.md-typeset .summary .admonition-title:before { + display: none; +} + +.md-typeset .admonition.summary { + border-color: #ffffff17 !important; +} + +/* +Admonition: "abstract" +React Name: "Note" +*/ +.md-typeset .admonition.abstract { + background: var(--note-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .abstract .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(68, 172, 153); +} + +.md-typeset .abstract .admonition-title:before { + font-size: 1.1rem; + background: rgb(68, 172, 153); +} + +/* +Admonition: "warning" +React Name: "Pitfall" +*/ +.md-typeset .admonition.warning { + background: var(--pitfall-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .warning .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(219, 125, 39); +} + +.md-typeset .warning .admonition-title:before { + font-size: 1.1rem; + background: rgb(219, 125, 39); +} + +/* +Admonition: "info" +React Name: "Deep Dive" +*/ +.md-typeset .admonition.info { + background: var(--deep-dive-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .info .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(136, 145, 236); +} + +.md-typeset .info .admonition-title:before { + font-size: 1.1rem; + background: rgb(136, 145, 236); +} + +/* +Admonition: "example" +React Name: "Terminal" +*/ +.md-typeset .admonition.example { + background: var(--terminal-bg-color); + border-radius: 0.4rem; + overflow: hidden; + border: none; +} + +.md-typeset .example .admonition-title { + background: var(--terminal-title-bg-color); + color: rgb(246, 247, 249); +} + +.md-typeset .example .admonition-title:before { + background: rgb(246, 247, 249); +} + +.md-typeset .admonition.example code { + background: transparent; + color: #fff; + box-shadow: none; +} diff --git a/docs/src/assets/css/banner.css b/docs/src/assets/css/banner.css new file mode 100644 index 000000000..3739a73c1 --- /dev/null +++ b/docs/src/assets/css/banner.css @@ -0,0 +1,15 @@ +body[data-md-color-scheme="slate"] { + --md-banner-bg-color: rgb(55, 81, 78); + --md-banner-font-color: #fff; +} + +body[data-md-color-scheme="default"] { + --md-banner-bg-color: #ff9; + --md-banner-font-color: #000; +} + +.md-banner--warning { + background-color: var(--md-banner-bg-color); + color: var(--md-banner-font-color); + text-align: center; +} diff --git a/docs/src/assets/css/button.css b/docs/src/assets/css/button.css new file mode 100644 index 000000000..8f71391aa --- /dev/null +++ b/docs/src/assets/css/button.css @@ -0,0 +1,41 @@ +[data-md-color-scheme="slate"] { + --md-button-font-color: #fff; + --md-button-border-color: #404756; +} + +[data-md-color-scheme="default"] { + --md-button-font-color: #000; + --md-button-border-color: #8d8d8d; +} + +.md-typeset .md-button { + border-width: 1px; + border-color: var(--md-button-border-color); + border-radius: 9999px; + color: var(--md-button-font-color); + transition: color 125ms, background 125ms, border-color 125ms, + transform 125ms; +} + +.md-typeset .md-button:focus, +.md-typeset .md-button:hover { + border-color: var(--md-button-border-color); + color: var(--md-button-font-color); + background: rgba(78, 87, 105, 0.05); +} + +.md-typeset .md-button.md-button--primary { + color: #fff; + 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: 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 000000000..c54654980 --- /dev/null +++ b/docs/src/assets/css/code.css @@ -0,0 +1,111 @@ +:root { + --code-max-height: 17.25rem; + --md-code-backdrop: rgba(0, 0, 0, 0) 0px 0px 0px 0px, + rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.03) 0px 0.8px 2px 0px, + rgba(0, 0, 0, 0.047) 0px 2.7px 6.7px 0px, + rgba(0, 0, 0, 0.08) 0px 12px 30px 0px; +} +[data-md-color-scheme="slate"] { + --md-code-hl-color: #ffffcf1c; + --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 { + --md-code-bg-color: #fff; +} + +/* All code blocks */ +.md-typeset pre > code { + max-height: var(--code-max-height); +} + +/* Code blocks with no line number */ +.md-typeset .highlight > pre > code { + border-radius: 16px; + max-height: var(--code-max-height); + box-shadow: var(--md-code-backdrop); +} + +/* Code blocks with line numbers */ +.md-typeset .highlighttable .linenos { + max-height: var(--code-max-height); + overflow: hidden; +} +.md-typeset .highlighttable { + box-shadow: var(--md-code-backdrop); + border-radius: 8px; + overflow: hidden; +} + +/* Tabbed code blocks */ +.md-typeset .tabbed-set { + 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; +} +.js .md-typeset .tabbed-set .tabbed-labels { + background: var(--code-tab-color); + margin: 0; + padding-left: 0.8rem; +} +.md-typeset .tabbed-set .tabbed-labels > label { + font-weight: 400; + font-size: 0.7rem; + padding-top: 0.55em; + padding-bottom: 0.35em; +} +.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/assets/css/footer.css b/docs/src/assets/css/footer.css new file mode 100644 index 000000000..b3408286e --- /dev/null +++ b/docs/src/assets/css/footer.css @@ -0,0 +1,33 @@ +[data-md-color-scheme="slate"] { + --md-footer-bg-color: var(--md-default-bg-color); + --md-footer-bg-color--dark: var(--md-default-bg-color); + --md-footer-border-color: var(--md-header-border-color); +} + +[data-md-color-scheme="default"] { + --md-footer-fg-color: var(--md-typeset-color); + --md-footer-fg-color--light: var(--md-typeset-color); + --md-footer-bg-color: var(--md-default-bg-color); + --md-footer-bg-color--dark: var(--md-default-bg-color); + --md-footer-border-color: var(--md-header-border-color); +} + +.md-footer { + border-top: 1px solid var(--md-footer-border-color); +} + +.md-copyright { + width: 100%; +} + +.md-copyright__highlight { + width: 100%; +} + +.legal-footer-right { + float: right; +} + +.md-copyright__highlight div { + display: inline; +} diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css new file mode 100644 index 000000000..c72e7093a --- /dev/null +++ b/docs/src/assets/css/home.css @@ -0,0 +1,335 @@ +img.home-logo { + height: 120px; +} + +.home .row { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 6rem 0.8rem; +} + +.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.first { + text-align: center; +} + +.home .row h1 { + max-width: 28rem; + line-height: 1.15; + font-weight: 500; + margin-bottom: 0.55rem; + margin-top: -1rem; +} + +.home .row.first h1 { + margin-top: 0.55rem; + margin-bottom: -0.75rem; +} + +.home .row > p { + max-width: 35rem; + line-height: 1.5; + font-weight: 400; +} + +.home .row.first > p { + font-size: 32px; + font-weight: 500; +} + +/* Code blocks */ +.home .row .tabbed-set { + background: var(--home-tabbed-set-bg-color); + margin: 0; +} + +.home .row .tabbed-content { + padding: 20px 18px; + overflow-x: auto; +} + +.home .row .tabbed-content img { + user-select: none; + -moz-user-select: none; + -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%; + grid-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; +} + +/* 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%; + } +} diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css new file mode 100644 index 000000000..6eefdf2f4 --- /dev/null +++ b/docs/src/assets/css/main.css @@ -0,0 +1,85 @@ +/* Variable overrides */ +:root { + --reactpy-color: #58b962; + --reactpy-color-dark: #42914a; + --reactpy-color-darker: #34743b; + --reactpy-color-opacity-10: rgba(88, 185, 98, 0.1); +} + +[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); + --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-primary-fg-color: var(--md-default-bg-color); + --md-default-fg-color: hsla(var(--md-hue), 75%, 95%, 1); + --md-default-fg-color--light: #fff; + --md-typeset-a-color: var(--reactpy-color); + --md-accent-fg-color: var(--reactpy-color-dark); +} + +[data-md-color-scheme="default"] { + --md-primary-fg-color: var(--md-default-bg-color); + --md-default-fg-color--light: #000; + --md-default-fg-color--lighter: #0000007e; + --md-default-fg-color--lightest: #00000029; + --md-typeset-color: rgb(35, 39, 47); + --md-typeset-a-color: var(--reactpy-color); + --md-accent-fg-color: var(--reactpy-color-dark); +} + +/* Font changes */ +.md-typeset { + font-weight: 300; +} + +.md-typeset h1 { + font-weight: 600; + margin: 0; + font-size: 2.5em; +} + +.md-typeset h2 { + font-weight: 500; +} + +.md-typeset h3 { + font-weight: 400; +} + +/* Intro section styling */ +p.intro { + font-size: 0.9rem; + font-weight: 500; +} + +/* Hide "Overview" jump selector */ +h2#overview { + visibility: hidden; + height: 0; + margin: 0; + padding: 0; +} + +/* Reduce size of the outdated banner */ +.md-banner__inner { + margin: 0.45rem auto; +} + +/* Desktop Styles */ +@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 new file mode 100644 index 000000000..33e8b14fd --- /dev/null +++ b/docs/src/assets/css/navbar.css @@ -0,0 +1,185 @@ +[data-md-color-scheme="slate"] { + --md-header-border-color: rgb(255 255 255 / 5%); + --md-version-bg-color: #ffffff0d; +} + +[data-md-color-scheme="default"] { + --md-header-border-color: rgb(0 0 0 / 7%); + --md-version-bg-color: #ae58ee2e; +} + +.md-header { + border: 0 solid transparent; + border-bottom-width: 1px; +} + +.md-header--shadow { + box-shadow: none; + border-color: var(--md-header-border-color); + transition: border-color 0.35s cubic-bezier(0.1, 0.7, 0.1, 1); +} + +/* 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; + left: 0; + right: 0; + top: 2.5rem; +} + +.md-version { + background: var(--md-version-bg-color); + border-radius: 999px; + padding: 0 0.8rem; + margin: 0.3rem 0; + height: 1.8rem; + display: flex; + font-size: 0.7rem; +} + +/* Mobile Styling */ +@media screen and (max-width: 60em) { + label.md-header__button.md-icon[for="__drawer"] { + order: 1; + } + .md-header__button.md-logo { + display: initial; + order: 2; + margin-right: auto; + } + .md-header__title { + order: 3; + } + .md-header__button[for="__search"] { + order: 4; + } + .md-header__option[data-md-component="palette"] { + order: 5; + } + .md-header__source { + display: initial; + order: 6; + } + .md-header__source .md-source__repository { + display: none; + } +} + +/* Desktop Styling */ +@media screen and (min-width: 60em) { + /* 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.4rem; + padding-top: 0; + padding-bottom: 0; + } + .md-header__button.md-logo img { + height: 2rem; + } + + /* Version selector */ + [dir="ltr"] .md-header__title { + order: 2; + margin: 0; + margin-right: 0.8rem; + margin-left: 0.2rem; + flex-grow: 0; + } + .md-header__topic { + position: relative; + } + .md-header__title--active .md-header__topic { + transform: none; + opacity: 1; + pointer-events: auto; + z-index: 4; + } + + /* Search */ + .md-search { + order: 3; + 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: 4; + 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; + border: none !important; + } + li.md-tabs__item.md-tabs__item--active { + background: var(--reactpy-color-opacity-10); + border-radius: 9999px; + color: var(--md-typeset-a-color); + } + .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: 5; + } + + /* GitHub info */ + .md-header__source { + order: 6; + 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 new file mode 100644 index 000000000..b6507d963 --- /dev/null +++ b/docs/src/assets/css/sidebar.css @@ -0,0 +1,104 @@ +:root { + --sizebar-font-size: 0.62rem; +} + +.md-nav__link { + word-break: break-word; +} + +/* 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; + } + + .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: grey; + } + + .md-nav__item .md-nav__link--active:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background: var(--reactpy-color-opacity-10); + } + + .md-nav__link { + padding: 0.5rem 0.5rem 0.5rem 1rem; + margin: 0; + border-radius: 0 10px 10px 0; + font-weight: 500; + overflow: hidden; + font-size: var(--sizebar-font-size); + } + + .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 000000000..6c94f06ef --- /dev/null +++ b/docs/src/assets/css/table-of-contents.css @@ -0,0 +1,48 @@ +/* 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; + font-size: var(--sizebar-font-size); + } + + [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: 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; + font-weight: 400; + } + + [data-md-component="toc"] + .md-nav__item + .md-nav__list + .md-nav__item + .md-nav__link { + padding-left: 1.25rem; + } + + [dir="ltr"] .md-sidebar__inner { + padding: 0; + } + + .md-nav__item { + padding: 0; + } +} diff --git a/docs/src/assets/images/add-interactivity.png b/docs/src/assets/images/add-interactivity.png new file mode 100644 index 000000000..c32431905 Binary files /dev/null and b/docs/src/assets/images/add-interactivity.png differ diff --git a/docs/src/assets/images/create-user-interfaces.png b/docs/src/assets/images/create-user-interfaces.png new file mode 100644 index 000000000..06f6ea0cb Binary files /dev/null and b/docs/src/assets/images/create-user-interfaces.png differ diff --git a/docs/src/assets/images/s_thinking-in-react_ui.png b/docs/src/assets/images/s_thinking-in-react_ui.png new file mode 100644 index 000000000..a3249d526 Binary files /dev/null and b/docs/src/assets/images/s_thinking-in-react_ui.png differ diff --git a/docs/src/assets/images/s_thinking-in-react_ui_outline.png b/docs/src/assets/images/s_thinking-in-react_ui_outline.png new file mode 100644 index 000000000..e705738c9 Binary files /dev/null and b/docs/src/assets/images/s_thinking-in-react_ui_outline.png differ diff --git a/docs/src/assets/images/write-components-with-python.png b/docs/src/assets/images/write-components-with-python.png new file mode 100644 index 000000000..380d2c3ad Binary files /dev/null and b/docs/src/assets/images/write-components-with-python.png differ diff --git a/docs/src/assets/js/main.js b/docs/src/assets/js/main.js new file mode 100644 index 000000000..50e2dda30 --- /dev/null +++ b/docs/src/assets/js/main.js @@ -0,0 +1,19 @@ +// Sync scrolling between the code node and the line number node +// Event needs to be a separate function, otherwise the event will be triggered multiple times +let code_with_lineno_scroll_event = function () { + let tr = this.parentNode.parentNode.parentNode.parentNode; + let lineno = tr.querySelector(".linenos"); + lineno.scrollTop = this.scrollTop; +}; + +const observer = new MutationObserver((mutations) => { + let lineno = document.querySelectorAll(".linenos~.code"); + lineno.forEach(function (element) { + let code = element.parentNode.querySelector("code"); + code.addEventListener("scroll", code_with_lineno_scroll_event); + }); +}); + +observer.observe(document.body, { + childList: true, +}); diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt new file mode 100644 index 000000000..af891a7e5 --- /dev/null +++ b/docs/src/dictionary.txt @@ -0,0 +1,28 @@ +django +nox +websocket +websockets +changelog +async +pre +prefetch +prefetching +preloader +whitespace +refetch +refetched +refetching +html +jupyter +webserver +iframe +keyworded +stylesheet +stylesheets +unstyled +py +idom +reactpy +asgi +postfixed +postprocessing diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 000000000..384ec5b6d --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,6 @@ +--- +template: home.html +hide: + - navigation + - toc +--- diff --git a/docs/src/learn/add-react-to-an-existing-project.md b/docs/src/learn/add-react-to-an-existing-project.md new file mode 100644 index 000000000..d201c1b9e --- /dev/null +++ b/docs/src/learn/add-react-to-an-existing-project.md @@ -0,0 +1,135 @@ +## Overview + ++ +If you want to add some interactivity to your existing project, you don't have to rewrite it in React. Add React to your existing stack, and render interactive React components anywhere. + +
+ +## Using React for an entire subroute of your existing website + +Let's say you have an existing web app at `example.com` built with another server technology (like Rails), and you want to implement all routes starting with `example.com/some-app/` fully with React. + +### Using an ASGI subroute + +Here's how we recommend to set it up: + +1. **Build the React part of your app** using one of the [ReactPy executors](./creating-a-react-app.md). +2. **Specify `/some-app` as the _base path_** in your executors kwargs (`#!python path_prefix="/some-app"`). +3. **Configure your server or a proxy** so that all requests under `/some-app/` are handled by your React app. + +This ensures the React part of your app can [benefit from the best practices](./creating-a-react-app.md) baked into those frameworks. + +### Using static site generation ([SSG](https://developer.mozilla.org/en-US/docs/Glossary/SSG)) + + + +Support for SSG is coming in a [future version](https://github.com/reactive-python/reactpy/issues/1272). + +## Using React for a part of your existing page + +Let's say you have an existing page built with another Python web technology (ASGI or WSGI), and you want to render interactive React components somewhere on that page. + +The exact approach depends on your existing page setup, so let's walk through some details. + +### Using ASGI Middleware + +ReactPy supports running as middleware for any existing ASGI application. ReactPy components are embedded into your existing HTML templates using Jinja2. You can use any ASGI framework, however for demonstration purposes we have selected [Starlette](https://www.starlette.io/) for the example below. + +First, install ReactPy, Starlette, and your preferred ASGI webserver. + +!!! example "Terminal" + + ```linenums="0" + pip install reactpy[asgi,jinja] starlette uvicorn[standard] + ``` + +Next, configure your ASGI framework to use ReactPy's Jinja2 template tag. The method for doing this will vary depending on the ASGI framework you are using. Below is an example that follow's [Starlette's documentation](https://www.starlette.io/templates/): + +```python linenums="0" hl_lines="6 11 17" +{% include "../../examples/add_react_to_an_existing_project/asgi_configure_jinja.py" %} +``` + +Now you will need to wrap your existing ASGI application with ReactPy's middleware, define the dotted path to your root components, and render your components in your existing HTML templates. + +!!! abstract "Note" + + The `ReactPyJinja` extension enables a handful of [template tags](/reference/templatetags/) that allow you to render ReactPy components in your templates. The `component` tag is used to render a ReactPy SSR component, while the `pyscript_setup` and `pyscript_component` tags can be used together to render CSR components. + +=== "main.py" + + ```python hl_lines="6 22" + {% include "../../examples/add_react_to_an_existing_project/asgi_middleware.py" %} + ``` + +=== "my_components.py" + + ```python + {% include "../../examples/add_react_to_an_existing_project/asgi_component.py" %} + ``` + +=== "my_template.html" + + ```html hl_lines="5 9" + {% include "../../examples/add_react_to_an_existing_project/asgi_template.html" %} + ``` + +Finally, use your webserver of choice to start ReactPy: + +!!! example "Terminal" + + ```linenums="0" + uvicorn main:reactpy_app + ``` + +### Using WSGI Middleware + +Support for WSGI executors is coming in a [future version](https://github.com/reactive-python/reactpy/issues/1260). + +## External Executors + +!!! abstract "Note" + + **External executors** exist outside ReactPy's core library and have significantly different installation and configuration instructions. + + Make sure to follow the documentation for setting up your chosen _external_ executor. + +### Django + +[Django](https://www.djangoproject.com/) is a full-featured web framework that provides a batteries-included approach to web development. + +Due to it's batteries-included approach, ReactPy has unique features only available to this executor. + +To learn how to configure Django for ReactPy, see the [ReactPy-Django documentation](https://reactive-python.github.io/reactpy-django/). + + + + diff --git a/docs/src/learn/choosing-the-state-structure.md b/docs/src/learn/choosing-the-state-structure.md new file mode 100644 index 000000000..c582becee --- /dev/null +++ b/docs/src/learn/choosing-the-state-structure.md @@ -0,0 +1,2862 @@ +## Overview + ++ +Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. Here are some tips you should consider when structuring state. + +
+ +!!! summary "You will learn" + + - When to use a single vs multiple state variables + - What to avoid when organizing state + - How to fix common issues with the state structure + +## Principles for structuring state + +When you write a component that holds some state, you'll have to make choices about how many state variables to use and what the shape of their data should be. While it's possible to write correct programs even with a suboptimal state structure, there are a few principles that can guide you to make better choices: + +1. **Group related state.** If you always update two or more state variables at the same time, consider merging them into a single state variable. +2. **Avoid contradictions in state.** When the state is structured in a way that several pieces of state may contradict and "disagree" with each other, you leave room for mistakes. Try to avoid this. +3. **Avoid redundant state.** If you can calculate some information from the component's props or its existing state variables during rendering, you should not put that information into that component's state. +4. **Avoid duplication in state.** When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can. +5. **Avoid deeply nested state.** Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way. + +The goal behind these principles is to _make state easy to update without introducing mistakes_. Removing redundant and duplicate data from state helps ensure that all its pieces stay in sync. This is similar to how a database engineer might want to ["normalize" the database structure](https://docs.microsoft.com/en-us/office/troubleshoot/access/database-normalization-description) to reduce the chance of bugs. To paraphrase Albert Einstein, **"Make your state as simple as it can be--but no simpler."** + +Now let's see how these principles apply in action. + +## Group related state + +You might sometimes be unsure between using a single or multiple state variables. + +Should you do this? + +```js +const [x, setX] = useState(0); +const [y, setY] = useState(0); +``` + +Or this? + +```js +const [position, setPosition] = useState({ x: 0, y: 0 }); +``` + +Technically, you can use either of these approaches. But **if some two state variables always change together, it might be a good idea to unify them into a single state variable.** Then you won't forget to always keep them in sync, like in this example where moving the cursor updates both coordinates of the red dot: + +```js +import { useState } from "react"; + +export default function MovingDot() { + const [position, setPosition] = useState({ + x: 0, + y: 0, + }); + return ( ++ Your ticket will be issued to: {fullName} +
+ > + ); +} +``` + +```css +label { + display: block; + margin-bottom: 5px; +} +``` + +This form has three state variables: `firstName`, `lastName`, and `fullName`. However, `fullName` is redundant. **You can always calculate `fullName` from `firstName` and `lastName` during render, so remove it from state.** + +This is how you can do it: + +```js +import { useState } from "react"; + +export default function Form() { + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + + const fullName = firstName + " " + lastName; + + function handleFirstNameChange(e) { + setFirstName(e.target.value); + } + + function handleLastNameChange(e) { + setLastName(e.target.value); + } + + return ( + <> ++ Your ticket will be issued to: {fullName} +
+ > + ); +} +``` + +```css +label { + display: block; + margin-bottom: 5px; +} +``` + +Here, `fullName` is _not_ a state variable. Instead, it's calculated during render: + +```js +const fullName = firstName + " " + lastName; +``` + +As a result, the change handlers don't need to do anything special to update it. When you call `setFirstName` or `setLastName`, you trigger a re-render, and then the next `fullName` will be calculated from the fresh data. + +You picked {selectedItem.title}.
+ > + ); +} +``` + +```css +button { + margin-top: 10px; +} +``` + +Currently, it stores the selected item as an object in the `selectedItem` state variable. However, this is not great: **the contents of the `selectedItem` is the same object as one of the items inside the `items` list.** This means that the information about the item itself is duplicated in two places. + +Why is this a problem? Let's make each item editable: + +```js +import { useState } from "react"; + +const initialItems = [ + { title: "pretzels", id: 0 }, + { title: "crispy seaweed", id: 1 }, + { title: "granola bar", id: 2 }, +]; + +export default function Menu() { + const [items, setItems] = useState(initialItems); + const [selectedItem, setSelectedItem] = useState(items[0]); + + function handleItemChange(id, e) { + setItems( + items.map((item) => { + if (item.id === id) { + return { + ...item, + title: e.target.value, + }; + } else { + return item; + } + }) + ); + } + + return ( + <> +You picked {selectedItem.title}.
+ > + ); +} +``` + +```css +button { + margin-top: 10px; +} +``` + +Notice how if you first click "Choose" on an item and _then_ edit it, **the input updates but the label at the bottom does not reflect the edits.** This is because you have duplicated state, and you forgot to update `selectedItem`. + +Although you could update `selectedItem` too, an easier fix is to remove duplication. In this example, instead of a `selectedItem` object (which creates a duplication with objects inside `items`), you hold the `selectedId` in state, and _then_ get the `selectedItem` by searching the `items` array for an item with that ID: + +```js +import { useState } from "react"; + +const initialItems = [ + { title: "pretzels", id: 0 }, + { title: "crispy seaweed", id: 1 }, + { title: "granola bar", id: 2 }, +]; + +export default function Menu() { + const [items, setItems] = useState(initialItems); + const [selectedId, setSelectedId] = useState(0); + + const selectedItem = items.find((item) => item.id === selectedId); + + function handleItemChange(id, e) { + setItems( + items.map((item) => { + if (item.id === id) { + return { + ...item, + title: e.target.value, + }; + } else { + return item; + } + }) + ); + } + + return ( + <> +You picked {selectedItem.title}.
+ > + ); +} +``` + +```css +button { + margin-top: 10px; +} +``` + +(Alternatively, you may hold the selected index in state.) + +The state used to be duplicated like this: + +- `items = [{ id: 0, title: 'pretzels'}, ...]` +- `selectedItem = {id: 0, title: 'pretzels'}` + +But after the change it's like this: + +- `items = [{ id: 0, title: 'pretzels'}, ...]` +- `selectedId = 0` + +The duplication is gone, and you only keep the essential state! + +Now if you edit the _selected_ item, the message below will update immediately. This is because `setItems` triggers a re-render, and `items.find(...)` would find the item with the updated title. You didn't need to hold _the selected item_ in state, because only the _selected ID_ is essential. The rest could be calculated during render. + +## Avoid deeply nested state + +Imagine a travel plan consisting of planets, continents, and countries. You might be tempted to structure its state using nested objects and arrays, like in this example: + +```js +import { useState } from "react"; +import { initialTravelPlan } from "./places.js"; + +function PlaceTree({ place }) { + const childPlaces = place.childPlaces; + return ( ++ Pick a color:{" "} + +
++ Pick a color:{" "} + +
++ Pick a color:{" "} + +
++ You selected {selectedCount} letters +
++ You selected {selectedCount} letters +
++ You selected {selectedCount} letters +
++ +Your components will often need to display different things depending on different conditions. In React, you can conditionally render JSX using JavaScript syntax like `if` statements, `&&`, and `? :` operators. + +
+ +!!! summary "You will learn" + + - How to return different JSX depending on a condition + - How to conditionally include or exclude a piece of JSX + - Common conditional syntax shortcuts you’ll encounter in React codebases + +## Conditionally returning JSX + +Let’s say you have a `PackingList` component rendering several `Item`s, which can be marked as packed or not: + +```js +function Item({ name, isPacked }) { + returnNew messages
`. It's easy to assume that it renders nothing when `messageCount` is `0`, but it really renders the `0` itself! + +To fix it, make the left side a boolean: `messageCount > 0 &&New messages
`. + ++ +If you want to build a new app or website with React, we recommend starting with a standalone executor. + +
+ +If your app has constraints not well-served by existing web frameworks, you prefer to build your own framework, or you just want to learn the basics of a React app, you can use ReactPy in **standalone mode**. + +## Using ReactPy for full-stack + +ReactPy is a component library that helps you build a full-stack web application. For convenience, ReactPy is also bundled with several different standalone executors. + +These standalone executors are the easiest way to get started with ReactPy, as they require no additional setup or configuration. + +!!! abstract "Note" + + **Standalone ReactPy requires a server** + + In order to serve the initial HTML page, you will need to run a server. The ASGI examples below use [Uvicorn](https://www.uvicorn.org/), but you can use [any ASGI server](https://github.com/florimondmanca/awesome-asgi#servers). + + Executors on this page can either support client-side rendering ([CSR](https://developer.mozilla.org/en-US/docs/Glossary/CSR)) or server-side rendering ([SSR](https://developer.mozilla.org/en-US/docs/Glossary/SSR)) + +### Running via ASGI SSR + +ReactPy can run in **server-side standalone mode**, where both page loading and component rendering occurs on an ASGI server. + +This executor is the most commonly used, as it provides maximum extensibility. + +First, install ReactPy and your preferred ASGI webserver. + +!!! example "Terminal" + + ```linenums="0" + pip install reactpy[asgi] uvicorn[standard] + ``` + +Next, create a new file called `main.py` containing the ASGI application: + +=== "main.py" + + ```python + {% include "../../examples/creating_a_react_app/asgi_ssr.py" %} + ``` + +Finally, use your webserver of choice to start ReactPy: + +!!! example "Terminal" + + ```linenums="0" + uvicorn main:my_app + ``` + +### Running via ASGI CSR + +ReactPy can run in **client-side standalone mode**, where the initial page is served using the ASGI protocol. This is configuration allows direct execution of Javascript, but requires special considerations since all ReactPy component code is run on the browser [via WebAssembly](https://pyscript.net/). + +First, install ReactPy and your preferred ASGI webserver. + +!!! example "Terminal" + + ```linenums="0" + pip install reactpy[asgi] uvicorn[standard] + ``` + +Next, create a new file called `main.py` containing the ASGI application, and a `root.py` file containing the root component: + +=== "main.py" + + ```python + {% include "../../examples/creating_a_react_app/asgi_csr.py" %} + ``` + +=== "root.py" + + ```python + {% include "../../examples/creating_a_react_app/asgi_csr_root.py" %} + ``` + +Finally, use your webserver of choice to start ReactPy: + +!!! example "Terminal" + + ```linenums="0" + uvicorn main:my_app + ``` + +### Running via WSGI SSR + +Support for WSGI executors is coming in a [future version](https://github.com/reactive-python/reactpy/issues/1260). + +### Running via WSGI CSR + +Support for WSGI executors is coming in a [future version](https://github.com/reactive-python/reactpy/issues/1260). diff --git a/src/py/reactpy/tests/test_backend/__init__.py b/docs/src/learn/creating-backends.md similarity index 100% rename from src/py/reactpy/tests/test_backend/__init__.py rename to docs/src/learn/creating-backends.md diff --git a/src/py/reactpy/tests/test_core/__init__.py b/docs/src/learn/creating-html-tags.md similarity index 100% rename from src/py/reactpy/tests/test_core/__init__.py rename to docs/src/learn/creating-html-tags.md diff --git a/src/py/reactpy/tests/test_web/__init__.py b/docs/src/learn/creating-vdom-event-handlers.md similarity index 100% rename from src/py/reactpy/tests/test_web/__init__.py rename to docs/src/learn/creating-vdom-event-handlers.md diff --git a/docs/src/learn/editor-setup.md b/docs/src/learn/editor-setup.md new file mode 100644 index 000000000..052f16663 --- /dev/null +++ b/docs/src/learn/editor-setup.md @@ -0,0 +1,70 @@ +## Overview + ++ +A properly configured editor can make code clearer to read and faster to write. It can even help you catch bugs as you write them! If this is your first time setting up an editor or you're looking to tune up your current editor, we have a few recommendations. + +
+ +!!! summary "You will learn" + + - What the most popular editors are + - How to format your code automatically + +## Your editor + +[VS Code](https://code.visualstudio.com/) is one of the most popular editors in use today. It has a large marketplace of extensions and integrates well with popular services like GitHub. Most of the features listed below can be added to VS Code as extensions as well, making it highly configurable! + +Other popular text editors used in the React community include: + +- [WebStorm](https://www.jetbrains.com/webstorm/) is an integrated development environment designed specifically for JavaScript. +- [Sublime Text](https://www.sublimetext.com/) has support for [syntax highlighting](https://stackoverflow.com/a/70960574/458193) and autocomplete built in. +- [Vim](https://www.vim.org/) is a highly configurable text editor built to make creating and changing any kind of text very efficient. It is included as "vi" with most UNIX systems and with Apple OS X. + +## Recommended text editor features + +Some editors come with these features built in, but others might require adding an extension. Check to see what support your editor of choice provides to be sure! + +### Python Linting + +Linting is the process of running a program that will analyse code for potential errors. [Flake8](https://flake8.pycqa.org/en/latest/) is a popular, open source linter for Python. + +- [Install Flake8](https://flake8.pycqa.org/en/latest/#installation) (be sure you have [Python installed!](https://www.python.org/downloads/)) +- [Integrate Flake8 in VSCode with the official extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8) +- [Install Reactpy-Flake8](https://pypi.org/project/reactpy-flake8/) to lint your ReactPy code + +### JavaScript Linting + +You typically won't use much JavaScript alongside ReactPy, but there are still some cases where you might. For example, you might want to use JavaScript to fetch data from an API or to add some interactivity to your app. + +In these cases, it's helpful to have a linter that can catch common mistakes in your code as you write it. [ESLint](https://eslint.org/) is a popular, open source linter for JavaScript. + +- [Install ESLint with the recommended configuration for React](https://www.npmjs.com/package/eslint-config-react-app) (be sure you have [Node installed!](https://nodejs.org/en/download/current/)) +- [Integrate ESLint in VSCode with the official extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + +**Make sure that you've enabled all the [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) rules for your project.** They are essential and catch the most severe bugs early. The recommended [`eslint-config-react-app`](https://www.npmjs.com/package/eslint-config-react-app) preset already includes them. + +### Formatting + +The last thing you want to do when sharing your code with another contributor is get into an discussion about [tabs vs spaces](https://www.google.com/search?q=tabs+vs+spaces)! Fortunately, [Prettier](https://prettier.io/) will clean up your code by reformatting it to conform to preset, configurable rules. Run Prettier, and all your tabs will be converted to spaces—and your indentation, quotes, etc will also all be changed to conform to the configuration. In the ideal setup, Prettier will run when you save your file, quickly making these edits for you. + +You can install the [Prettier extension in VSCode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) by following these steps: + +1. Launch VS Code +2. Use Quick Open, press ++ctrl+p++ +3. Paste in `ext install esbenp.prettier-vscode` +4. Press Enter + +#### Formatting on save + +Ideally, you should format your code on every save. VS Code has settings for this! + +1. In VS Code, press ++ctrl+shift+p++ +2. Type "settings" +3. Hit Enter +4. In the search bar, type "format on save" +5. Be sure the "format on save" option is ticked! + +!!! abstract "Note" + + If your ESLint preset has formatting rules, they may conflict with Prettier. We recommend disabling all formatting rules in your ESLint preset using [`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier) so that ESLint is _only_ used for catching logical mistakes. If you want to enforce that files are formatted before a pull request is merged, use [`prettier --check`](https://prettier.io/docs/en/cli.html#--check) for your continuous integration. diff --git a/docs/src/learn/extra-tools-and-packages.md b/docs/src/learn/extra-tools-and-packages.md new file mode 100644 index 000000000..17a619944 --- /dev/null +++ b/docs/src/learn/extra-tools-and-packages.md @@ -0,0 +1,2 @@ +- ReactPy Router +- ReactPy Flake8 diff --git a/docs/src/learn/extracting-state-logic-into-a-reducer.md b/docs/src/learn/extracting-state-logic-into-a-reducer.md new file mode 100644 index 000000000..bf871ac88 --- /dev/null +++ b/docs/src/learn/extracting-state-logic-into-a-reducer.md @@ -0,0 +1,2640 @@ +## Overview + ++ +Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a _reducer._ + +
+ +!!! summary "You will learn" + + - What a reducer function is + - How to refactor `useState` to `useReducer` + - When to use a reducer + - How to write one well + +## Consolidate state logic with a reducer + +As your components grow in complexity, it can get harder to see at a glance all the different ways in which a component's state gets updated. For example, the `TaskApp` component below holds an array of `tasks` in state and uses three different event handlers to add, remove, and edit tasks: + +```js +import { useState } from "react"; +import AddTask from "./AddTask.js"; +import TaskList from "./TaskList.js"; + +export default function TaskApp() { + const [tasks, setTasks] = useState(initialTasks); + + function handleAddTask(text) { + setTasks([ + ...tasks, + { + id: nextId++, + text: text, + done: false, + }, + ]); + } + + function handleChangeTask(task) { + setTasks( + tasks.map((t) => { + if (t.id === task.id) { + return task; + } else { + return t; + } + }) + ); + } + + function handleDeleteTask(taskId) { + setTasks(tasks.filter((t) => t.id !== taskId)); + } + + return ( + <> ++ +The magic of components lies in their reusability: you can create components that are composed of other components. But as you nest more and more components, it often makes sense to start splitting them into different files. This lets you keep your files easy to scan and reuse components in more places. + +
+ +!!! summary "You will learn" + + - What a root component file is + - How to import and export a component + - When to use default and named imports and exports + - How to import and export multiple components from one file + - How to split components into multiple files + +## The root component file + +In [Your First Component](/learn/your-first-component), you made a `Profile` component and a `Gallery` component that renders it: + +```js +function Profile() { + return ( ++ +React has been designed from the start for gradual adoption. You can use as little or as much React as you need. Whether you want to get a taste of React, add some interactivity to an HTML page, or start a complex React-powered app, this section will help you get started. + +
+ +!!! summary "You will learn" + + * [How to start a new React project](../learn/start-a-new-react-project.md) + * [How to add React to an existing project](../learn/add-react-to-an-existing-project.md) + * [How to set up your editor](../learn/editor-setup.md) + * [How to install React Developer Tools](../learn/react-developer-tools.md) + + + +## Start a new React project + +If you want to build an app or a website fully with React, [start a new React project.](../learn/start-a-new-react-project.md) + +## Add React to an existing project + +If want to try using React in your existing app or a website, [add React to an existing project.](../learn/add-react-to-an-existing-project.md) + +## Next steps + +Head to the [Quick Start](../learn/quick-start.md) guide for a tour of the most important React concepts you will encounter every day. diff --git a/docs/src/learn/keeping-components-pure.md b/docs/src/learn/keeping-components-pure.md new file mode 100644 index 000000000..833527bdd --- /dev/null +++ b/docs/src/learn/keeping-components-pure.md @@ -0,0 +1,811 @@ +## Overview + ++ +Some JavaScript functions are _pure._ Pure functions only perform a calculation and nothing more. By strictly only writing your components as pure functions, you can avoid an entire class of baffling bugs and unpredictable behavior as your codebase grows. To get these benefits, though, there are a few rules you must follow. + +
+ +!!! summary "You will learn" + + - What purity is and how it helps you avoid bugs + - How to keep components pure by keeping changes out of the render phase + - How to use Strict Mode to find mistakes in your components + +## Purity: Components as formulas + +In computer science (and especially the world of functional programming), [a pure function](https://wikipedia.org/wiki/Pure_function) is a function with the following characteristics: + +- **It minds its own business.** It does not change any objects or variables that existed before it was called. +- **Same inputs, same output.** Given the same inputs, a pure function should always return the same result. + +You might already be familiar with one example of pure functions: formulas in math. + +Consider this math formula: . + +If then . Always. + +If then . Always. + +If ,+ +Effects have a different lifecycle from components. Components may mount, update, or unmount. An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it. This cycle can happen multiple times if your Effect depends on props and state that change over time. React provides a linter rule to check that you've specified your Effect's dependencies correctly. This keeps your Effect synchronized to the latest props and state. + +
+ +!!! summary "You will learn" + + - How an Effect's lifecycle is different from a component's lifecycle + - How to think about each individual Effect in isolation + - When your Effect needs to re-synchronize, and why + - How your Effect's dependencies are determined + - What it means for a value to be reactive + - What an empty dependency array means + - How React verifies your dependencies are correct with a linter + - What to do when you disagree with the linter + +## The lifecycle of an Effect + +Every React component goes through the same lifecycle: + +- A component _mounts_ when it's added to the screen. +- A component _updates_ when it receives new props or state, usually in response to an interaction. +- A component _unmounts_ when it's removed from the screen. + +**It's a good way to think about components, but _not_ about Effects.** Instead, try to think about each Effect independently from your component's lifecycle. An Effect describes how to [synchronize an external system](/learn/synchronizing-with-effects) to the current props and state. As your code changes, synchronization will need to happen more or less often. + +To illustrate this point, consider this Effect connecting your component to a chat server: + +```js +const serverUrl = "https://localhost:1234"; + +function ChatRoom({ roomId }) { + useEffect(() => { + const connection = createConnection(serverUrl, roomId); + connection.connect(); + return () => { + connection.disconnect(); + }; + }, [roomId]); + // ... +} +``` + +Your Effect's body specifies how to **start synchronizing:** + +```js +// ... +const connection = createConnection(serverUrl, roomId); +connection.connect(); +return () => { + connection.disconnect(); +}; +// ... +``` + +The cleanup function returned by your Effect specifies how to **stop synchronizing:** + +```js +// ... +const connection = createConnection(serverUrl, roomId); +connection.connect(); +return () => { + connection.disconnect(); +}; +// ... +``` + +Intuitively, you might think that React would **start synchronizing** when your component mounts and **stop synchronizing** when your component unmounts. However, this is not the end of the story! Sometimes, it may also be necessary to **start and stop synchronizing multiple times** while the component remains mounted. + +Let's look at _why_ this is necessary, _when_ it happens, and _how_ you can control this behavior. + ++ You are going to: {placeId || "???"} on {planetId || "???"}{" "} +
+ > + ); +} +``` + +```js +export function fetchData(url) { + if (url === "/planets") { + return fetchPlanets(); + } else if (url.startsWith("/planets/")) { + const match = url.match(/^\/planets\/([\w-]+)\/places(\/)?$/); + if (!match || !match[1] || !match[1].length) { + throw Error( + 'Expected URL like "/planets/earth/places". Received: "' + + url + + '".' + ); + } + return fetchPlaces(match[1]); + } else + throw Error( + 'Expected URL like "/planets" or "/planets/earth/places". Received: "' + + url + + '".' + ); +} + +async function fetchPlanets() { + return new Promise((resolve) => { + setTimeout(() => { + resolve([ + { + id: "earth", + name: "Earth", + }, + { + id: "venus", + name: "Venus", + }, + { + id: "mars", + name: "Mars", + }, + ]); + }, 1000); + }); +} + +async function fetchPlaces(planetId) { + if (typeof planetId !== "string") { + throw Error( + "fetchPlaces(planetId) expects a string argument. " + + "Instead received: " + + planetId + + "." + ); + } + return new Promise((resolve) => { + setTimeout(() => { + if (planetId === "earth") { + resolve([ + { + id: "laos", + name: "Laos", + }, + { + id: "spain", + name: "Spain", + }, + { + id: "vietnam", + name: "Vietnam", + }, + ]); + } else if (planetId === "venus") { + resolve([ + { + id: "aurelia", + name: "Aurelia", + }, + { + id: "diana-chasma", + name: "Diana Chasma", + }, + { + id: "kumsong-vallis", + name: "Kŭmsŏng Vallis", + }, + ]); + } else if (planetId === "mars") { + resolve([ + { + id: "aluminum-city", + name: "Aluminum City", + }, + { + id: "new-new-york", + name: "New New York", + }, + { + id: "vishniac", + name: "Vishniac", + }, + ]); + } else throw Error("Unknown planet ID: " + planetId); + }, 1000); + }); +} +``` + +```css +label { + display: block; + margin-bottom: 10px; +} +``` + ++ You are going to: {placeId || "???"} on {planetId || "???"}{" "} +
+ > + ); +} +``` + +```js +export function fetchData(url) { + if (url === "/planets") { + return fetchPlanets(); + } else if (url.startsWith("/planets/")) { + const match = url.match(/^\/planets\/([\w-]+)\/places(\/)?$/); + if (!match || !match[1] || !match[1].length) { + throw Error( + 'Expected URL like "/planets/earth/places". Received: "' + + url + + '".' + ); + } + return fetchPlaces(match[1]); + } else + throw Error( + 'Expected URL like "/planets" or "/planets/earth/places". Received: "' + + url + + '".' + ); +} + +async function fetchPlanets() { + return new Promise((resolve) => { + setTimeout(() => { + resolve([ + { + id: "earth", + name: "Earth", + }, + { + id: "venus", + name: "Venus", + }, + { + id: "mars", + name: "Mars", + }, + ]); + }, 1000); + }); +} + +async function fetchPlaces(planetId) { + if (typeof planetId !== "string") { + throw Error( + "fetchPlaces(planetId) expects a string argument. " + + "Instead received: " + + planetId + + "." + ); + } + return new Promise((resolve) => { + setTimeout(() => { + if (planetId === "earth") { + resolve([ + { + id: "laos", + name: "Laos", + }, + { + id: "spain", + name: "Spain", + }, + { + id: "vietnam", + name: "Vietnam", + }, + ]); + } else if (planetId === "venus") { + resolve([ + { + id: "aurelia", + name: "Aurelia", + }, + { + id: "diana-chasma", + name: "Diana Chasma", + }, + { + id: "kumsong-vallis", + name: "Kŭmsŏng Vallis", + }, + ]); + } else if (planetId === "mars") { + resolve([ + { + id: "aluminum-city", + name: "Aluminum City", + }, + { + id: "new-new-york", + name: "New New York", + }, + { + id: "vishniac", + name: "Vishniac", + }, + ]); + } else throw Error("Unknown planet ID: " + planetId); + }, 1000); + }); +} +``` + +```css +label { + display: block; + margin-bottom: 10px; +} +``` + +This code is a bit repetitive. However, that's not a good reason to combine it into a single Effect! If you did this, you'd have to combine both Effect's dependencies into one list, and then changing the planet would refetch the list of all planets. Effects are not a tool for code reuse. + +Instead, to reduce repetition, you can extract some logic into a custom Hook like `useSelectOptions` below: + +```js +import { useState } from "react"; +import { useSelectOptions } from "./useSelectOptions.js"; + +export default function Page() { + const [planetList, planetId, setPlanetId] = useSelectOptions("/planets"); + + const [placeList, placeId, setPlaceId] = useSelectOptions( + planetId ? `/planets/${planetId}/places` : null + ); + + return ( + <> + + ++ You are going to: {placeId || "..."} on {planetId || "..."}{" "} +
+ > + ); +} +``` + +```js +import { useState, useEffect } from "react"; +import { fetchData } from "./api.js"; + +export function useSelectOptions(url) { + const [list, setList] = useState(null); + const [selectedId, setSelectedId] = useState(""); + useEffect(() => { + if (url === null) { + return; + } + + let ignore = false; + fetchData(url).then((result) => { + if (!ignore) { + setList(result); + setSelectedId(result[0].id); + } + }); + return () => { + ignore = true; + }; + }, [url]); + return [list, selectedId, setSelectedId]; +} +``` + +```js +export function fetchData(url) { + if (url === "/planets") { + return fetchPlanets(); + } else if (url.startsWith("/planets/")) { + const match = url.match(/^\/planets\/([\w-]+)\/places(\/)?$/); + if (!match || !match[1] || !match[1].length) { + throw Error( + 'Expected URL like "/planets/earth/places". Received: "' + + url + + '".' + ); + } + return fetchPlaces(match[1]); + } else + throw Error( + 'Expected URL like "/planets" or "/planets/earth/places". Received: "' + + url + + '".' + ); +} + +async function fetchPlanets() { + return new Promise((resolve) => { + setTimeout(() => { + resolve([ + { + id: "earth", + name: "Earth", + }, + { + id: "venus", + name: "Venus", + }, + { + id: "mars", + name: "Mars", + }, + ]); + }, 1000); + }); +} + +async function fetchPlaces(planetId) { + if (typeof planetId !== "string") { + throw Error( + "fetchPlaces(planetId) expects a string argument. " + + "Instead received: " + + planetId + + "." + ); + } + return new Promise((resolve) => { + setTimeout(() => { + if (planetId === "earth") { + resolve([ + { + id: "laos", + name: "Laos", + }, + { + id: "spain", + name: "Spain", + }, + { + id: "vietnam", + name: "Vietnam", + }, + ]); + } else if (planetId === "venus") { + resolve([ + { + id: "aurelia", + name: "Aurelia", + }, + { + id: "diana-chasma", + name: "Diana Chasma", + }, + { + id: "kumsong-vallis", + name: "Kŭmsŏng Vallis", + }, + ]); + } else if (planetId === "mars") { + resolve([ + { + id: "aluminum-city", + name: "Aluminum City", + }, + { + id: "new-new-york", + name: "New New York", + }, + { + id: "vishniac", + name: "Vishniac", + }, + ]); + } else throw Error("Unknown planet ID: " + planetId); + }, 1000); + }); +} +``` + +```css +label { + display: block; + margin-bottom: 10px; +} +``` + +Check the `useSelectOptions.js` tab in the sandbox to see how it works. Ideally, most Effects in your application should eventually be replaced by custom Hooks, whether written by you or by the community. Custom Hooks hide the synchronization logic, so the calling component doesn't know about the Effect. As you keep working on your app, you'll develop a palette of Hooks to choose from, and eventually you won't need to write Effects in your components very often. + ++ +React automatically updates the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) to match your render output, so your components won't often need to manipulate it. However, sometimes you might need access to the DOM elements managed by React--for example, to focus a node, scroll to it, or measure its size and position. There is no built-in way to do those things in React, so you will need a _ref_ to the DOM node. + +
+ +!!! summary "You will learn" + + - How to access a DOM node managed by React with the `ref` attribute + - How the `ref` JSX attribute relates to the `useRef` Hook + - How to access another component's DOM node + - In which cases it's safe to modify the DOM managed by React + +## Getting a ref to the node + +To access a DOM node managed by React, first, import the `useRef` Hook: + +```js +import { useRef } from "react"; +``` + +Then, use it to declare a ref inside your component: + +```js +const myRef = useRef(null); +``` + +Finally, pass your ref as the `ref` attribute to the JSX tag for which you want to get the DOM node: + +```js +