diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1ff35c8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @reactive-python/maintainers diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..12f72a6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [archmonger] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c6bdd8f..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: Bug Report -labels: bug -assignees: rmorshea - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..36e5aeb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Start a Discussion + url: https://github.com/reactive-python/reactpy-router/discussions + about: Report issues, request features, ask questions, and share ideas diff --git a/.github/ISSUE_TEMPLATE/doc_enhancement.md b/.github/ISSUE_TEMPLATE/doc_enhancement.md deleted file mode 100644 index 9a960b0..0000000 --- a/.github/ISSUE_TEMPLATE/doc_enhancement.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Doc enhancement -about: Documentation needs to be fixed or added -title: Doc Enhancement -labels: docs -assignees: rmorshea - ---- - -**Describe what documentation needs to be fixed or added** -Is something missing, worded poorly, or flat out wrong? Tells us about it here. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 1c5de5f..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: rmorshea - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/issue-form.yml b/.github/ISSUE_TEMPLATE/issue-form.yml new file mode 100644 index 0000000..b4a4b89 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-form.yml @@ -0,0 +1,16 @@ +name: Plan a Task +description: Create a detailed plan of action (ONLY START AFTER DISCUSSION PLEASE πŸ™). +labels: ["flag: triage"] +body: +- type: textarea + attributes: + label: Current Situation + description: Discuss how things currently are, why they require action, and any relevant prior discussion/context. + validations: + required: false +- type: textarea + attributes: + label: Proposed Actions + description: Describe what ought to be done, and why that will address the reasons for action mentioned above. + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a555320 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## Description + + + +## Checklist + +Please update this checklist as you complete each item: + +- [ ] Tests have been developed for bug fixes or new functionality. +- [ ] The changelog has been updated, if necessary. +- [ ] Documentation has been updated, if necessary. +- [ ] GitHub Issues closed by this PR have been linked. + +By submitting this pull request I agree that all contributions comply with this project's open source license(s). diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..0f26793 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,78 @@ +# 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: + # Runs at 22:21 on Monday. + - cron: '21 22 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + 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. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # πŸ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml new file mode 100644 index 0000000..6b1d4de --- /dev/null +++ b/.github/workflows/publish-develop-docs.yml @@ -0,0 +1,22 @@ +name: Publish Develop Docs +on: + push: + branches: + - main +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 requirements/build-docs.txt + - name: Publish Develop Docs + run: | + git config user.name github-actions + git config user.email github-actions@github.com + cd docs + mike deploy --push develop diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml deleted file mode 100644 index 2d6aa80..0000000 --- a/.github/workflows/publish-docs.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: publish-docs - -on: - push: - branches: - - main - -jobs: - build: - name: Deploy docs - runs-on: ubuntu-latest - steps: - - name: Checkout main - uses: actions/checkout@v2 - - name: Deploy docs - # Use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to exclude mkdocs-material theme - uses: mhausenblas/mkdocs-deploy-gh-pages@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CONFIG_FILE: docs/mkdocs.yml - REQUIREMENTS: requirements/build-docs.txt diff --git a/.github/workflows/publish-py.yaml b/.github/workflows/publish-py.yaml new file mode 100644 index 0000000..34ae5fa --- /dev/null +++ b/.github/workflows/publish-py.yaml @@ -0,0 +1,29 @@ +# 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: Publish Python + +on: + release: + types: [published] + +jobs: + publish-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements/build-pkg.txt + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py bdist_wheel + twine upload dist/* diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-release-docs.yml new file mode 100644 index 0000000..6fc3233 --- /dev/null +++ b/.github/workflows/publish-release-docs.yml @@ -0,0 +1,23 @@ +name: Publish Release Docs + +on: + release: + types: [published] + +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 requirements/build-docs.txt + - name: Publish ${{ github.event.release.name }} Docs + run: | + git config user.name github-actions + git config user.email github-actions@github.com + cd docs + mike deploy --push --update-aliases ${{ github.event.release.name }} latest diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 00a3264..0000000 --- a/.github/workflows/release.yaml +++ /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: release - -on: - release: - types: - - created - -jobs: - publish-package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: "3.x" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py bdist_wheel - twine upload dist/* diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml new file mode 100644 index 0000000..d5f5052 --- /dev/null +++ b/.github/workflows/test-docs.yml @@ -0,0 +1,37 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * *" + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Check docs build + run: | + pip install -r requirements/build-docs.txt + linkcheckMarkdown docs/ -v -r + linkcheckMarkdown README.md -v -r + linkcheckMarkdown CHANGELOG.md -v -r + cd docs + mkdocs build --strict + - name: Check docs examples + run: | + pip install -r requirements/check-types.txt + pip install -r requirements/check-style.txt + mypy --show-error-codes docs/examples/python/ + black docs/examples/python/ --check + ruff check docs/examples/python/ diff --git a/.github/workflows/test-src.yaml b/.github/workflows/test-src.yaml new file mode 100644 index 0000000..b5ae7d0 --- /dev/null +++ b/.github/workflows/test-src.yaml @@ -0,0 +1,40 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * *" + +jobs: + source: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + - name: Use Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Python Dependencies + run: pip install -r requirements/test-run.txt + - name: Run Tests + run: nox -t test + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Latest Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + - name: Install Python Dependencies + run: pip install -r requirements/test-run.txt + - name: Run Tests + run: nox -t test -- --coverage diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 7e0a9c8..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: test - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Use Latest Python - uses: actions/setup-python@v2 - with: - python-version: "3.10" - - name: Install Python Dependencies - run: pip install -r requirements/nox-deps.txt - - name: Run Tests - run: nox -t test - - environments: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.9", "3.10", "3.11"] - steps: - - uses: actions/checkout@v2 - - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install Python Dependencies - run: pip install -r requirements/nox-deps.txt - - name: Run Tests - run: nox -t test -- --no-cov diff --git a/.gitignore b/.gitignore index c9676bc..9155bda 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ docs/site # --- JAVASCRIPT BUNDLES --- -reactpy_router/bundle.js +src/reactpy_router/bundle.js # --- PYTHON IGNORE FILES ---- @@ -65,21 +65,6 @@ cover/ *.mo *.pot -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ # PyBuilder .pybuilder/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c673a2f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# 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)! + +## [0.1.1] - 2023-12-13 + +### Fixed + +- Fixed relative navigation. + +## [0.1.0] - 2023-06-16 + +### Added + +- Automatically handle client-side history changes. + +## [0.0.1] - 2023-05-10 + +### Added + +- Add robust lint/testing. +- Upgrade `reactpy`. +- More robust routing with `starlette`. +- Initial draft of router compiler. + +### Changed + +- Rename `configure` to `create_router`. +- Rename from `idom-router` to `reactpy-router`. + +[Unreleased]: https://github.com/reactive-python/reactpy-router/compare/0.1.1...HEAD +[0.1.1]: https://github.com/reactive-python/reactpy-router/compare/0.1.0...0.1.1 +[0.1.0]: https://github.com/reactive-python/reactpy-router/compare/0.0.1...0.1.0 +[0.0.1]: https://github.com/reactive-python/reactpy-router/releases/tag/0.0.1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..809177a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,47 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ryan.morshead@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1067742..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 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 0000000..f5423c3 --- /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/MANIFEST.in b/MANIFEST.in index 9a3edbc..bdca1f4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,2 @@ -include README.md -include reactpy_router/bundle.js -include reactpy_router/py.typed -include LICENSE +include src/reactpy_router/bundle.js +include src/reactpy_router/py.typed diff --git a/README.md b/README.md index 63aeaab..3cb0c5d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ -# reactpy-router +# ReactPy Router -A URL router for ReactPy +

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

-Read the docs: https://reactive-python.github.io/reactpy-router +[ReactPy-Router](https://github.com/reactive-python/reactpy-router) is used to add used to add URL routing support to an existing **ReactPy project**. + +More information about this package can be found on [the documentation](https://reactive-python.github.io/reactpy-router). diff --git a/docs/examples/python/__init__.py b/docs/examples/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/examples/python/basic-routing-more-routes.py b/docs/examples/python/basic-routing-more-routes.py new file mode 100644 index 0000000..8ddbebb --- /dev/null +++ b/docs/examples/python/basic-routing-more-routes.py @@ -0,0 +1,15 @@ +from reactpy import component, html, run + +from reactpy_router import route, simple + + +@component +def root(): + return simple.router( + route("/", html.h1("Home Page 🏠")), + route("/messages", html.h1("Messages πŸ’¬")), + route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), + ) + + +run(root) diff --git a/docs/examples/python/basic-routing.py b/docs/examples/python/basic-routing.py new file mode 100644 index 0000000..57b7a37 --- /dev/null +++ b/docs/examples/python/basic-routing.py @@ -0,0 +1,14 @@ +from reactpy import component, html, run + +from reactpy_router import route, simple + + +@component +def root(): + return simple.router( + route("/", html.h1("Home Page 🏠")), + route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), + ) + + +run(root) diff --git a/docs/examples/python/nested-routes.py b/docs/examples/python/nested-routes.py new file mode 100644 index 0000000..f03a692 --- /dev/null +++ b/docs/examples/python/nested-routes.py @@ -0,0 +1,89 @@ +from typing import TypedDict + +from reactpy import component, html, run +from reactpy_router import link, route, simple + +message_data: list["MessageDataType"] = [ + {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, + {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, + {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, + {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, + {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, + {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, + {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, + {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, +] + + +@component +def root(): + return simple.router( + route("/", home()), + route( + "/messages", + all_messages(), + # we'll improve upon these manually created routes in the next section... + route("/with/Alice", messages_with("Alice")), + route("/with/Alice-Bob", messages_with("Alice", "Bob")), + ), + route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), + ) + + +@component +def home(): + return html.div( + html.h1("Home Page 🏠"), + link("Messages", to="/messages"), + ) + + +@component +def all_messages(): + last_messages = { + ", ".join(msg["with"]): msg + for msg in sorted(message_data, key=lambda m: m["id"]) + } + return html.div( + html.h1("All Messages πŸ’¬"), + html.ul( + [ + html.li( + {"key": msg["id"]}, + html.p( + link( + f"Conversation with: {', '.join(msg['with'])}", + to=f"/messages/with/{'-'.join(msg['with'])}", + ), + ), + f"{'' if msg['from'] is None else 'πŸ”΄'} {msg['message']}", + ) + for msg in last_messages.values() + ] + ), + ) + + +@component +def messages_with(*names): + messages = [msg for msg in message_data if set(msg["with"]) == names] + return html.div( + html.h1(f"Messages with {', '.join(names)} πŸ’¬"), + html.ul( + [ + html.li( + {"key": msg["id"]}, + f"{msg['from'] or 'You'}: {msg['message']}", + ) + for msg in messages + ] + ), + ) + + +run(root) + +MessageDataType = TypedDict( + "MessageDataType", + {"id": int, "with": list[str], "from": str | None, "message": str}, +) diff --git a/docs/examples/python/route-links.py b/docs/examples/python/route-links.py new file mode 100644 index 0000000..f2be305 --- /dev/null +++ b/docs/examples/python/route-links.py @@ -0,0 +1,23 @@ +from reactpy import component, html, run + +from reactpy_router import link, route, simple + + +@component +def root(): + return simple.router( + route("/", home()), + route("/messages", html.h1("Messages πŸ’¬")), + route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), + ) + + +@component +def home(): + return html.div( + html.h1("Home Page 🏠"), + link("Messages", to="/messages"), + ) + + +run(root) diff --git a/docs/examples/python/route-parameters.py b/docs/examples/python/route-parameters.py new file mode 100644 index 0000000..4fd30e2 --- /dev/null +++ b/docs/examples/python/route-parameters.py @@ -0,0 +1,89 @@ +from typing import TypedDict + +from reactpy import component, html, run +from reactpy_router import link, route, simple +from reactpy_router.core import use_params + +message_data: list["MessageDataType"] = [ + {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, + {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, + {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, + {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, + {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, + {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, + {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, + {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, +] + + +@component +def root(): + return simple.router( + route("/", home()), + route( + "/messages", + all_messages(), + route("/with/{names}", messages_with()), # note the path param + ), + route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), + ) + + +@component +def home(): + return html.div( + html.h1("Home Page 🏠"), + link("Messages", to="/messages"), + ) + + +@component +def all_messages(): + last_messages = { + ", ".join(msg["with"]): msg + for msg in sorted(message_data, key=lambda m: m["id"]) + } + return html.div( + html.h1("All Messages πŸ’¬"), + html.ul( + [ + html.li( + {"key": msg["id"]}, + html.p( + link( + f"Conversation with: {', '.join(msg['with'])}", + to=f"/messages/with/{'-'.join(msg['with'])}", + ), + ), + f"{'' if msg['from'] is None else 'πŸ”΄'} {msg['message']}", + ) + for msg in last_messages.values() + ] + ), + ) + + +@component +def messages_with(): + names = set(use_params()["names"].split("-")) # and here we use the path param + messages = [msg for msg in message_data if set(msg["with"]) == names] + return html.div( + html.h1(f"Messages with {', '.join(names)} πŸ’¬"), + html.ul( + [ + html.li( + {"key": msg["id"]}, + f"{msg['from'] or 'You'}: {msg['message']}", + ) + for msg in messages + ] + ), + ) + + +run(root) + +MessageDataType = TypedDict( + "MessageDataType", + {"id": int, "with": list[str], "from": str | None, "message": str}, +) diff --git a/docs/examples/python/use-params.py b/docs/examples/python/use-params.py new file mode 100644 index 0000000..7b1193a --- /dev/null +++ b/docs/examples/python/use-params.py @@ -0,0 +1,23 @@ +from reactpy import component, html + +from reactpy_router import link, route, simple, use_params + + +@component +def user(): + params = use_params() + return html.h1(f"User {params['id']} πŸ‘€") + + +@component +def root(): + return simple.router( + route( + "/", + html.div( + html.h1("Home Page 🏠"), + link("User 123", to="/user/123"), + ), + ), + route("/user/{id:int}", user()), + ) diff --git a/docs/examples/python/use-query.py b/docs/examples/python/use-query.py new file mode 100644 index 0000000..a8678cc --- /dev/null +++ b/docs/examples/python/use-query.py @@ -0,0 +1,23 @@ +from reactpy import component, html + +from reactpy_router import link, route, simple, use_query + + +@component +def search(): + query = use_query() + return html.h1(f"Search Results for {query['q'][0]} πŸ”") + + +@component +def root(): + return simple.router( + route( + "/", + html.div( + html.h1("Home Page 🏠"), + link("Search", to="/search?q=reactpy"), + ), + ), + route("/about", html.h1("About Page πŸ“–")), + ) diff --git a/docs/includes/pr.md b/docs/includes/pr.md new file mode 100644 index 0000000..9b4f0e4 --- /dev/null +++ b/docs/includes/pr.md @@ -0,0 +1,3 @@ +Now, you can create/modify the ReactPy-Router source code, and Pull Request (PR) your changes to our GitHub repository. + +To learn how to create GitHub PRs, [click here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 54a4f8c..d93b302 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,55 +1,149 @@ -site_name: ReactPy Router -docs_dir: src -repo_url: https://github.com/reactive-python/reactpy-router - +--- nav: - - Home: index.md - - Usage: usage.md - - Tutorials: - - Simple Application: tutorials/simple-app.md - - Custom Router: tutorials/custom-router.md - - Reference: reference.md - - Contributing: contributing.md - - Source Code: https://github.com/reactive-python/reactpy-router + - Get Started: + - Add ReactPy-Router to Your Project: index.md + - Your First Routed Application: learn/simple-application.md + - Advanced Topics: + - Routers, Routes, and Links: learn/routers-routes-and-links.md + - Hooks: learn/hooks.md + - Creating a Custom Router 🚧: learn/custom-router.md + - Reference: + - Core: reference/core.md + - Router: reference/router.md + - Types: reference/types.md + - About: + - Changelog: about/changelog.md + - Contributor Guide: + - Code: about/code.md + - Docs: about/docs.md + - Community: + - GitHub Discussions: https://github.com/reactive-python/reactpy-router/discussions + - Discord: https://discord.gg/uNb5P4hA9X + - Reddit: https://www.reddit.com/r/ReactPy/ + - License: about/license.md theme: - name: material - logo: assets/logo.svg - favicon: assets/logo.svg - palette: - # Palette toggle for light mode - - scheme: default - toggle: - icon: material/brightness-7 - name: Switch to dark mode - primary: black - accent: light-blue - - # Palette toggle for dark mode - - scheme: slate - toggle: - icon: material/brightness-4 - name: Switch to light mode - primary: black - accent: light-blue + name: material + custom_dir: overrides + palette: + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/white-balance-sunny + name: Switch to light mode + primary: red # We use red to indicate that something is unthemed + accent: red + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-night + name: Switch to dark mode + primary: white + accent: red + features: + - navigation.instant + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - content.code.copy + - search.highlight + icon: + repo: fontawesome/brands/github + admonition: + note: fontawesome/solid/note-sticky + logo: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg + favicon: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg +markdown_extensions: + - toc: + permalink: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + linenums: true + - pymdownx.superfences + - pymdownx.details + - pymdownx.inlinehilite + - admonition + - attr_list + - md_in_html + - pymdownx.keys plugins: -- search -- mkdocstrings: - default_handler: python - handlers: - python: - paths: ["../"] - import: - - https://reactpy.dev/docs/objects.inv - - https://installer.readthedocs.io/en/stable/objects.inv + - search + - include-markdown + - git-authors + - minify: + minify_html: true + minify_js: true + minify_css: true + cache_safe: true + - git-revision-date-localized: + fallback_to_build_date: true + - spellcheck: + known_words: dictionary.txt + allow_unicode: no + ignore_code: yes + skip_files: + - "index.md" + - "reference\\core.md" + - "reference/core.md" + - "reference\\types.md" + - "reference/types.md" + - mkdocstrings: + default_handler: python + handlers: + python: + paths: ["../"] + import: + - https://reactpy.dev/docs/objects.inv + - https://installer.readthedocs.io/en/stable/objects.inv -markdown_extensions: - - admonition - - pymdownx.details - - pymdownx.superfences +extra: + generator: false + version: + provider: mike + analytics: + provider: google + property: G-XRLQYZBG00 + +extra_javascript: + - assets/js/main.js + +extra_css: + - assets/css/main.css + - assets/css/button.css + - assets/css/admonition.css + - assets/css/banner.css + - assets/css/sidebar.css + - assets/css/navbar.css + - assets/css/table-of-contents.css + - assets/css/code.css + - assets/css/footer.css + - assets/css/home.css watch: - - "../reactpy_router" + - "../docs" + - ../README.md + - ../CHANGELOG.md + - ../LICENSE.md + - "../src" +site_name: ReactPy Router +site_author: Archmonger +site_description: It's React-Router, but in Python. +copyright: '© +
+ +Reactive Python and affiliates. +' +repo_url: https://github.com/reactive-python/reactpy-router +site_url: https://reactive-python.github.io/reactpy-router +repo_name: ReactPy Router (GitHub) +edit_uri: edit/main/docs/src/ +docs_dir: src 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 0000000..48ac19a --- /dev/null +++ b/docs/overrides/home-code-examples/add-interactivity-demo.html @@ -0,0 +1,172 @@ +
+
+ +
+
+ + + + 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/add-interactivity.py b/docs/overrides/home-code-examples/add-interactivity.py new file mode 100644 index 0000000..9097644 --- /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 0000000..c1f14e5 --- /dev/null +++ b/docs/overrides/home-code-examples/code-block.html @@ -0,0 +1,7 @@ +
+ +
+
+ +
+
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 0000000..9a684d3 --- /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/create-user-interfaces.py b/docs/overrides/home-code-examples/create-user-interfaces.py new file mode 100644 index 0000000..37776ab --- /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-demo.html b/docs/overrides/home-code-examples/write-components-with-python-demo.html new file mode 100644 index 0000000..203287c --- /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-code-examples/write-components-with-python.py b/docs/overrides/home-code-examples/write-components-with-python.py new file mode 100644 index 0000000..6af43ba --- /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/main.html b/docs/overrides/main.html new file mode 100644 index 0000000..c63ca9e --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +{{ super() }} + +{% if git_page_authors %} +
+ + Authors: {{ git_page_authors | default('enable mkdocs-git-authors-plugin') }} + +
+{% endif %} +{% endblock %} + +{% block outdated %} +You're not viewing the latest release. + + Click here to go to latest. + +{% endblock %} diff --git a/docs/src/about/changelog.md b/docs/src/about/changelog.md new file mode 100644 index 0000000..1ecf88e --- /dev/null +++ b/docs/src/about/changelog.md @@ -0,0 +1,14 @@ +--- +hide: + - toc +--- + +

+ +{% include-markdown "../../../CHANGELOG.md" start="" end="" %} + +

+ +--- + +{% include-markdown "../../../CHANGELOG.md" start="" %} diff --git a/docs/src/about/code.md b/docs/src/about/code.md new file mode 100644 index 0000000..5d73042 --- /dev/null +++ b/docs/src/about/code.md @@ -0,0 +1,53 @@ +## Overview + +

+ + You will need to set up a Python environment to develop ReactPy-Router. + +

+ +--- + +## Creating an environment + +If you plan to make code changes to this repository, 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-router.git +cd reactpy-router +``` + +Then, by running the command below you can install the dependencies needed to run the ReactPy-Router development environment. + +```bash linenums="0" +pip install -r requirements.txt --upgrade --verbose +``` + +## Running the full test suite + +!!! abstract "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 -t test +``` + +Or, if you want to run the tests in the foreground with a visible browser window, run: + + + +```bash linenums="0" +nox -t test -- --headed +``` + +## Creating a pull request + +{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/about/docs.md b/docs/src/about/docs.md new file mode 100644 index 0000000..4c1f566 --- /dev/null +++ b/docs/src/about/docs.md @@ -0,0 +1,45 @@ +## Overview + +

+ +You will need to set up a Python environment to create, test, and preview docs changes. + +

+ +--- + +## Modifying Docs + +If you plan to make changes to this documentation, 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-router.git +cd reactpy-router +``` + +Then, by running the command below you can: + +- Install an editable version of the documentation +- Self-host a test server for the documentation + +```bash linenums="0" +pip install -r requirements.txt --upgrade +``` + +Finally, to verify that everything is working properly, you can manually run the docs preview web server. + +```bash linenums="0" +cd docs +mkdocs serve +``` + +Navigate to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) to view a preview of the documentation. + +## Creating a pull request + +{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/about/license.md b/docs/src/about/license.md new file mode 100644 index 0000000..15d975d --- /dev/null +++ b/docs/src/about/license.md @@ -0,0 +1,8 @@ +--- +hide: + - toc +--- + +--- + +{% include "../../../LICENSE.md" %} diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css new file mode 100644 index 0000000..8b3f06e --- /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 0000000..3739a73 --- /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 0000000..8f71391 --- /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 0000000..c546549 --- /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 0000000..b340828 --- /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 0000000..c72e709 --- /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 0000000..6eefdf2 --- /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 0000000..33e8b14 --- /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 0000000..b6507d9 --- /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 0000000..6c94f06 --- /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/js/main.js b/docs/src/assets/js/main.js new file mode 100644 index 0000000..50e2dda --- /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/assets/logo.svg b/docs/src/assets/logo.svg deleted file mode 100644 index 312fb87..0000000 --- a/docs/src/assets/logo.svg +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/src/contributing.md b/docs/src/contributing.md deleted file mode 100644 index 520531f..0000000 --- a/docs/src/contributing.md +++ /dev/null @@ -1,70 +0,0 @@ -# Contributing - -!!! note - - The [Code of Conduct](https://github.com/reactive-python/reactpy/blob/main/CODE_OF_CONDUCT.md) - applies in all community spaces. If you are not familiar with our Code of Conduct policy, - take a minute to read it before making your first contribution. - -The ReactPy team welcomes contributions and contributors of all kinds - whether they -come as code changes, participation in the discussions, opening issues and pointing out -bugs, or simply sharing your work with your colleagues and friends. We’re excited to see -how you can help move this project and community forward! - -## Everyone Can Contribute! - -Trust us, there’s so many ways to support the project. We’re always looking for people who can: - -- Improve our documentation -- Teach and tell others about ReactPy -- Share ideas for new features -- Report bugs -- Participate in general discussions - -Still aren’t sure what you have to offer? Just [ask us](https://github.com/reactive-python/reactpy-router/discussions) and we’ll help you make your first contribution. - -## Development Environment - -For a developer installation from source be sure to install -[NPM](https://www.npmjs.com/) before running: - -```bash -git clone https://github.com/reactive-python/reactpy-router -cd reactpy-router -pip install -e . -r requirements.txt -``` - -This will install an ediable version of `reactpy-router` as well as tools you'll need -to work with this project. - -Of particular note is [`nox`](https://nox.thea.codes/en/stable/), which is used to -automate testing and other development tasks. - -## Running the Tests - -```bash -nox -t test -``` - -You can run the tests with a headed browser. - -```bash -nox -t test -- --headed -``` - -## Releasing This Package - -To release a new version of reactpy-router on PyPI: - -1. Install [`twine`](https://twine.readthedocs.io/en/latest/) with `pip install twine` -2. Update the `version = "x.y.z"` variable in `reactpy-router/__init__.py` -3. `git` add the changes to `__init__.py` and create a `git tag -a x.y.z -m 'comment'` -4. Build the Python package with `python setup.py sdist bdist_wheel` -5. Check the build artifacts `twine check --strict dist/*` -6. Upload the build artifacts to [PyPI](https://pypi.org/) `twine upload dist/*` - -To release a new version of `reactpy-router` on [NPM](https://www.npmjs.com/): - -1. Update `js/package.json` with new npm package version -2. Clean out prior builds `git clean -fdx` -3. Install and publish `npm install && npm publish` diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt new file mode 100644 index 0000000..6eb9552 --- /dev/null +++ b/docs/src/dictionary.txt @@ -0,0 +1,39 @@ +sanic +plotly +nox +WebSocket +WebSockets +changelog +async +pre +prefetch +prefetching +preloader +whitespace +refetch +refetched +refetching +html +jupyter +iframe +keyworded +stylesheet +stylesheets +unstyled +py +reactpy +asgi +postfixed +postprocessing +serializable +postprocessor +preprocessor +middleware +backends +backend +frontend +frontends +misconfiguration +misconfigurations +backhaul +sublicense diff --git a/docs/src/index.md b/docs/src/index.md index 351fd71..3630a87 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,18 +1,11 @@ -# ReactPy Router +## Install from PyPI -A URL router for [ReactPy](https://reactpy.dev). +Run the following command to install [`reactpy-router`](https://pypi.org/project/reactpy-router/) in your Python environment. -!!! note - - If you don't already know the basics of working with ReactPy, you should - [start there](https://reactpy.dev/docs/guides/getting-started/index.html). - -## Installation - -Use `pip` to install this package: - -```bash +```bash linenums="0" pip install reactpy-router ``` -[installer.records][] +## Done! + +You're now ready to start building your own ReactPy applications with URL routing. diff --git a/docs/src/tutorials/custom-router.md b/docs/src/learn/custom-router.md similarity index 100% rename from docs/src/tutorials/custom-router.md rename to docs/src/learn/custom-router.md diff --git a/docs/src/learn/hooks.md b/docs/src/learn/hooks.md new file mode 100644 index 0000000..3479ffc --- /dev/null +++ b/docs/src/learn/hooks.md @@ -0,0 +1,27 @@ +Several pre-fabricated hooks are provided to help integrate with routing features. You can learn more about them below. + +!!! abstract "Note" + + If you're not familiar what a hook is, you should [read the ReactPy docs](https://reactpy.dev/docs/guides/adding-interactivity/components-with-state/index.html#your-first-hook). + +--- + +## Use Query + +The [`use_query`][src.reactpy_router.use_query] hook can be used to access query parameters from the current location. It returns a dictionary of query parameters, where each value is a list of strings. + +=== "components.py" + + ```python + {% include "../../examples/python/use-query.py" %} + ``` + +## Use Parameters + +The [`use_params`][src.reactpy_router.use_params] hook can be used to access route parameters from the current location. It returns a dictionary of route parameters, where each value is mapped to a value that matches the type specified in the route path. + +=== "components.py" + + ```python + {% include "../../examples/python/use-params.py" %} + ``` diff --git a/docs/src/learn/routers-routes-and-links.md b/docs/src/learn/routers-routes-and-links.md new file mode 100644 index 0000000..af62578 --- /dev/null +++ b/docs/src/learn/routers-routes-and-links.md @@ -0,0 +1,67 @@ +We include built-in components that automatically handle routing, which enable Single Page Application (SPA) behavior. + +--- + +## Routers and Routes + +The [`simple.router`][src.reactpy_router.simple.router] component is one possible implementation of a [Router][src.reactpy_router.types.Router]. Routers takes a series of [route][src.reactpy_router.route] objects as positional arguments and render whatever element matches the current location. + +!!! abstract "Note" + + The current location is determined based on the browser's current URL and can be found + by checking the [`use_location`][reactpy.backend.hooks.use_location] hook. + +Here's a basic example showing how to use `#!python simple.router` with two routes. + +=== "components.py" + + ```python + {% include "../../examples/python/basic-routing.py" %} + ``` + +Here we'll note some special syntax in the route path for the second route. The `#!python "*"` is a wildcard that will match any path. This is useful for creating a "404" page that will be shown when no other route matches. + +### Simple Router + +The syntax for declaring routes with the [simple.router][src.reactpy_router.simple.router] is very similar to the syntax used by [`starlette`](https://www.starlette.io/routing/) (a popular Python web framework). As such route parameters are declared using the following syntax: + +```python linenums="0" +/my/route/{param} +/my/route/{param:type} +``` + +In this case, `#!python param` is the name of the route parameter and the optionally declared `#!python type` specifies what kind of parameter it is. The available parameter types and what patterns they match are are: + +| Type | Pattern | +| --- | --- | +| `#!python str` (default) | `#!python [^/]+` | +| `#!python int` | `#!python \d+` | +| `#!python float` | `#!python \d+(\.\d+)?` | +| `#!python uuid` | `#!python [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` | +| `#!python path` | `#!python .+` | + +So in practice these each might look like: + +```python linenums="0" +/my/route/{param} +/my/route/{param:int} +/my/route/{param:float} +/my/route/{param:uuid} +/my/route/{param:path} +``` + +Any route parameters collected from the current location then be accessed using the [`use_params`](#using-parameters) hook. + +!!! warning "Pitfall" + + While it is possible to use route parameters to capture values from query strings (such as `#!python /my/route/?foo={bar}`), this is not recommended. Instead, you should use the [`use_query`][src.reactpy_router.use_query] hook to access query string values. + +## Route Links + +Links between routes should be created using the [link][src.reactpy_router.link] component. This will allow ReactPy to handle the transition between routes and avoid a page reload. + +=== "components.py" + + ```python + {% include "../../examples/python/route-links.py" %} + ``` diff --git a/docs/src/learn/simple-application.md b/docs/src/learn/simple-application.md new file mode 100644 index 0000000..8f2a5b5 --- /dev/null +++ b/docs/src/learn/simple-application.md @@ -0,0 +1,88 @@ +

+ +Here you'll learn the various features of `reactpy-router` and how to use them. These examples will utilize the [`reactpy_router.simple.router`][src.reactpy_router.simple.router]. + +

+ +!!! abstract "Note" + + These docs assume you already know the basics of [ReacPy](https://reactpy.dev). + +--- + +Let's build a simple web application for viewing messages between several people. + +For the purposes of this tutorial we'll be working with the following data. + +```python linenums="0" +message_data = [ + {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, + {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, + {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, + {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, + {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, + {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, + {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, + {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, +] +``` + +In a more realistic application this data would be stored in a database, but for this tutorial we'll just keep it in memory. + +## Creating Basic Routes + +The first step is to create a basic router that will display the home page when the user navigates to the root of the application, and a "missing link" page for any other route. + +=== "components.py" + + ```python + {% include "../../examples/python/basic-routing.py" %} + ``` + +When navigating to [`http://127.0.0.1:8000``](http://127.0.0.1:8000) you should see `Home Page 🏠`. However, if you go to any other route you will instead see `Missing Link πŸ”—β€πŸ’₯`. + +With this foundation you can start adding more routes. + +=== "components.py" + + ```python + {% include "../../examples/python/basic-routing-more-routes.py" %} + ``` + +With this change you can now also go to [`/messages`](http://127.0.0.1:8000/messages) to see `Messages πŸ’¬`. + +## Using Route Links + +Instead of using the standard `#!python reactpy.html.a` element to create links to different parts of your application, use `#!python reactpy_router.link` instead. When users click links constructed using `#!python reactpy_router.link`, ReactPy will handle the transition and prevent a full page reload. + +=== "components.py" + + ```python + {% include "../../examples/python/route-links.py" %} + ``` + +Now, when you go to the home page, you can click `Messages` link to go to [`/messages`](http://127.0.0.1:8000/messages). + +## Adding Nested Routes + +Routes can be nested in order to construct more complicated application structures. + +=== "components.py" + + ```python + {% include "../../examples/python/nested-routes.py" %} + ``` + +## Adding Route Parameters + +In the example above we had to manually create a `#!python messages_with(...)` component for each conversation. This would be better accomplished by defining a single route that declares route parameters instead. + +Any parameters that have matched in the currently displayed route can then be consumed with the `#!python use_params` hook which returns a dictionary mapping the parameter names to their values. Note that parameters with a declared type will be converted to is in the parameters dictionary. So for example `#!python /my/route/{my_param:float}` would match `#!python /my/route/3.14` and have a parameter dictionary of `#!python {"my_param": 3.14}`. + +If we take this information and apply it to our growing example application we'd substitute the manually constructed `#!python /messages/with` routes with a single `#!python /messages/with/{names}` route. + +=== "components.py" + + ```python + {% include "../../examples/python/route-parameters.py" %} + ``` diff --git a/docs/src/reference.md b/docs/src/reference.md deleted file mode 100644 index aabc9b3..0000000 --- a/docs/src/reference.md +++ /dev/null @@ -1,5 +0,0 @@ -# Reference - -::: reactpy_router.core -::: reactpy_router.simple -::: reactpy_router.types diff --git a/docs/src/reference/core.md b/docs/src/reference/core.md new file mode 100644 index 0000000..26cf9e5 --- /dev/null +++ b/docs/src/reference/core.md @@ -0,0 +1 @@ +::: src.reactpy_router.core diff --git a/docs/src/reference/router.md b/docs/src/reference/router.md new file mode 100644 index 0000000..2fcea59 --- /dev/null +++ b/docs/src/reference/router.md @@ -0,0 +1 @@ +::: src.reactpy_router.simple diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md new file mode 100644 index 0000000..0482432 --- /dev/null +++ b/docs/src/reference/types.md @@ -0,0 +1 @@ +::: src.reactpy_router.types diff --git a/docs/src/tutorials/simple-app.md b/docs/src/tutorials/simple-app.md deleted file mode 100644 index 5f7fcbd..0000000 --- a/docs/src/tutorials/simple-app.md +++ /dev/null @@ -1,277 +0,0 @@ -# Simple Application - -Let's build a simple web application for viewing messages between several people. - -For the purposes of this tutorial we'll be working with the following data: - -```python -message_data = [ - {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, - {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, - {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, - {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, - {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, - {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, - {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, - {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, -] -``` - -In a more realistic application this data would be stored in a database, but for this -tutorial we'll just keep it in memory. - -## Basic Routing - -The first step is to create a basic router that will display the home page when the -user navigates to the root of the application, and a "missing link" page for any other -route: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple - -@component -def root(): - return simple.router( - route("/", html.h1("Home Page 🏠")), - route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), - ) - -run(root) -``` - -When navigating to http://127.0.0.1:8000 you should see "Home Page 🏠". However, if you -go to any other route (e.g. http://127.0.0.1:8000/missing) you will instead see the -"Missing Link πŸ”—β€πŸ’₯" page. - -With this foundation you can start adding more routes: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple - -@component -def root(): - return simple.router( - route("/", html.h1("Home Page 🏠")), - route("/messages", html.h1("Messages πŸ’¬")), - route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), - ) - -run(root) -``` - -With this change you can now also go to `/messages` to see "Messages πŸ’¬" displayed. - -## Route Links - -Instead of using the standard `` element to create links to different parts of your -application, use `reactpy_router.link` instead. When users click links constructed using -`reactpy_router.link`, instead of letting the browser navigate to the associated route, -ReactPy will more quickly handle the transition by avoiding the cost of a full page -load. - -```python -from reactpy import component, html, run -from reactpy_router import link, route, simple - -@component -def root(): - return simple.router( - route("/", home()), - route("/messages", html.h1("Messages πŸ’¬")), - route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), - ) - -@component -def home(): - return html.div( - html.h1("Home Page 🏠"), - link("Messages", to="/messages"), - ) - -run(root) -``` - -Now, when you go to the home page, you can click the link to go to `/messages`. - -## Nested Routes - -Routes can be nested in order to construct more complicated application structures: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple, link - -message_data = [ - {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, - {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, - {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, - {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, - {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, - {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, - {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, - {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, -] - -@component -def root(): - return simple.router( - route("/", home()), - route( - "/messages", - all_messages(), - # we'll improve upon these manually created routes in the next section... - route("/with/Alice", messages_with("Alice")), - route("/with/Alice-Bob", messages_with("Alice", "Bob")), - ), - route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), - ) - -@component -def home(): - return html.div( - html.h1("Home Page 🏠"), - link("Messages", to="/messages"), - ) - -@component -def all_messages(): - last_messages = { - ", ".join(msg["with"]): msg - for msg in sorted(message_data, key=lambda m: m["id"]) - } - return html.div( - html.h1("All Messages πŸ’¬"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - html.p( - link( - f"Conversation with: {', '.join(msg['with'])}", - to=f"/messages/with/{'-'.join(msg['with'])}", - ), - ), - f"{'' if msg['from'] is None else 'πŸ”΄'} {msg['message']}", - ) - for msg in last_messages.values() - ] - ), - ) - -@component -def messages_with(*names): - names = set(names) - messages = [msg for msg in message_data if set(msg["with"]) == names] - return html.div( - html.h1(f"Messages with {', '.join(names)} πŸ’¬"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - f"{msg['from'] or 'You'}: {msg['message']}", - ) - for msg in messages - ] - ), - ) - -run(root) -``` - -## Route Parameters - -In the example above we had to manually create a `messages_with(...)` component for each -conversation. This would be better accomplished by defining a single route that declares -["route parameters"](../usage.md#simple-router) instead. - -Any parameters that have matched in the currently displayed route can then be consumed -with the `use_params` hook which returns a dictionary mapping the parameter names to -their values. Note that parameters with a declared type will be converted to is in the -parameters dictionary. So for example `/my/route/{my_param:float}` would match -`/my/route/3.14` and have a parameter dictionary of `{"my_param": 3.14}`. - -If we take this information and apply it to our growing example application we'd -substitute the manually constructed `/messages/with` routes with a single -`/messages/with/{names}` route: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple, link -from reactpy_router.core import use_params - -message_data = [ - {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"}, - {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"}, - {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"}, - {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"}, - {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"}, - {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."}, - {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"}, - {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"}, -] - -@component -def root(): - return simple.router( - route("/", home()), - route( - "/messages", - all_messages(), - route("/with/{names}", messages_with()), # note the path param - ), - route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), - ) - -@component -def home(): - return html.div( - html.h1("Home Page 🏠"), - link("Messages", to="/messages"), - ) - -@component -def all_messages(): - last_messages = { - ", ".join(msg["with"]): msg - for msg in sorted(message_data, key=lambda m: m["id"]) - } - return html.div( - html.h1("All Messages πŸ’¬"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - html.p( - link( - f"Conversation with: {', '.join(msg['with'])}", - to=f"/messages/with/{'-'.join(msg['with'])}", - ), - ), - f"{'' if msg['from'] is None else 'πŸ”΄'} {msg['message']}", - ) - for msg in last_messages.values() - ] - ), - ) - -@component -def messages_with(): - names = set(use_params()["names"].split("-")) # and here we use the path param - messages = [msg for msg in message_data if set(msg["with"]) == names] - return html.div( - html.h1(f"Messages with {', '.join(names)} πŸ’¬"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - f"{msg['from'] or 'You'}: {msg['message']}", - ) - for msg in messages - ] - ), - ) - -run(root) -``` diff --git a/docs/src/usage.md b/docs/src/usage.md deleted file mode 100644 index e0f7bb2..0000000 --- a/docs/src/usage.md +++ /dev/null @@ -1,180 +0,0 @@ -# Usage - -!!! note - - The sections below assume you already know the basics of [ReacPy](https://reactpy.dev). - -Here you'll learn the various features of `reactpy-router` and how to use them. All examples -will utilize the [simple.router][reactpy_router.simple.router] (though you can [use your own](#custom-routers)). - -## Routers and Routes - -The [simple.router][reactpy_router.simple.router] component is one possible -implementation of a [Router][reactpy_router.types.Router]. Routers takes a series of -[Route][reactpy_router.types.Route] objects as positional arguments and render whatever -element matches the current location. For convenience, these `Route` objects are created -using the [route][reactpy_router.route] function. - -!!! note - - The current location is determined based on the browser's current URL and can be found - by checking the [use_location][reactpy.backend.hooks.use_location] hook. - -Here's a basic example showing how to use `simple.router` with two routes: - -```python -from reactpy import component, html, run -from reactpy_router import route, simple, use_location - -@component -def root(): - location = use_location() - return simple.router( - route("/", html.h1("Home Page 🏠")), - route("*", html.h1("Missing Link πŸ”—β€πŸ’₯")), - ) -``` - -Here we'll note some special syntax in the route path for the second route. The `*` is a -wildcard that will match any path. This is useful for creating a "404" page that will be -shown when no other route matches. - -### Simple Router - -The syntax for declaring routes with the [simple.router][reactpy_router.simple.router] -is very similar to the syntax used by [Starlette](https://www.starlette.io/routing/) (a -popular Python web framework). As such route parameters are declared using the following -syntax: - -``` -/my/route/{param} -/my/route/{param:type} -``` - -In this case, `param` is the name of the route parameter and the optionally declared -`type` specifies what kind of parameter it is. The available parameter types and what -patterns they match are are: - -- str (default) - `[^/]+` -- int - `\d+` -- float - `\d+(\.\d+)?` -- uuid - `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` -- path - `.+` - -!!! note - - The `path` type is special in that it will match any path, including `/` characters. - This is useful for creating routes that match a path prefix. - -So in practice these each might look like: - -``` -/my/route/{param} -/my/route/{param:int} -/my/route/{param:float} -/my/route/{param:uuid} -/my/route/{param:path} -``` - -Any route parameters collected from the current location then be accessed using the -[`use_params`](#using-parameters) hook. - -!!! note - - It's worth pointing out that, while you can use route parameters to capture values - from queryies (i.e. `?foo=bar`), this is not recommended. Instead, you should use - the [use_query][reactpy_router.use_query] hook to access query parameters. - -### Route Links - -Links between routes should be created using the [link][reactpy_router.link] component. -This will allow ReactPy to handle the transition between routes more quickly by avoiding -the cost of a full page load. - -```python -from reactpy import component, html, run, use_location -from reactpy_router import link, route, simple - -@component -def root(): - use_location() - return simple.router( - route( - "/", - html.div( - html.h1("Home Page 🏠"), - link(html.button("About"), to="/about"), - ), - ), - route("/about", html.h1("About Page πŸ“–")), - ) -``` - -## Hooks - -`reactpy-router` provides a number of hooks for working with the routes: - -- [`use_query`](#using-queries) - for accessing query parameters -- [`use_params`](#using-parameters) - for accessing route parameters - -If you're not familiar with hooks, you should -[read the docs](https://reactpy.dev/docs/guides/adding-interactivity/components-with-state/index.html#your-first-hook). - -### Using Queries - -The [use_query][reactpy_router.use_query] hook can be used to access query parameters -from the current location. It returns a dictionary of query parameters, where each value -is a list of strings. - -```python -from reactpy import component, html, run -from reactpy_router import link, route, simple, use_query - -@component -def root(): - use_location() - return simple.router( - route( - "/", - html.div( - html.h1("Home Page 🏠"), - link("Search", to="/search?q=reactpy"), - ), - ), - route("/about", html.h1("About Page πŸ“–")), - ) - -@component -def search(): - query = use_query() - return html.h1(f"Search Results for {query['q'][0]} πŸ”") -``` - -### Using Parameters - -The [use_params][reactpy_router.use_params] hook can be used to access route parameters -from the current location. It returns a dictionary of route parameters, where each value -is mapped to a value that matches the type specified in the route path. - -```python -from reactpy import component, html, run -from reactpy_router import link, route, simple, use_params - -@component -def root(): - return simple.router( - route( - "/", - html.div( - html.h1("Home Page 🏠"), - link("User 123", to="/user/123"), - ), - ), - route("/user/{id:int}", user()), - ) - -@component -def user(): - params = use_params() - return html.h1(f"User {params['id']} πŸ‘€") -``` diff --git a/js/README.md b/js/README.md deleted file mode 100644 index 1d6fc22..0000000 --- a/js/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# reactpy-router - -A URL router for ReactPy - -# Package Installation - -Requires [Node](https://nodejs.org/en/) to be installed: - -```bash -npm install --save reactpy-router -``` - -For a developer installation, `cd` into this directory and run: - -```bash -npm install -npm run build -``` - -This will install required dependencies and generate a Javascript bundle that is saved -to `reactpy-router/bundle.js`` and is distributed with the -associated Python package. diff --git a/noxfile.py b/noxfile.py index 1fddabb..1eebea2 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,73 +2,51 @@ from nox import Session, session -ROOT = Path(".") -REQUIREMENTS_DIR = ROOT / "requirements" - - -@session -def format(session: Session) -> None: - install_requirements(session, "check-style") - session.run("black", ".") - session.run("isort", ".") - - -@session -def docs(session: Session) -> None: - setup_docs(session) - session.run("mkdocs", "serve") - - -@session -def docs_build(session: Session) -> None: - setup_docs(session) - session.run("mkdocs", "build") - - -@session(tags=["test"]) -def test_style(session: Session) -> None: - install_requirements(session, "check-style") - session.run("black", "--check", ".") - session.run("isort", "--check", ".") - session.run("flake8", ".") - - -@session(tags=["test"]) -def test_types(session: Session) -> None: - install_requirements(session, "check-types") - session.run("mypy", "--strict", "reactpy_router") +ROOT_DIR = Path(__file__).parent @session(tags=["test"]) -def test_suite(session: Session) -> None: - install_requirements(session, "test-env") +def test_python(session: Session) -> None: + install_requirements_file(session, "test-env") + session.install(".[all]") session.run("playwright", "install", "chromium") posargs = session.posargs[:] - if "--no-cov" in session.posargs: - posargs.remove("--no-cov") - session.log("Coverage won't be checked") - session.install(".") - else: + if "--coverage" in posargs: posargs += ["--cov=reactpy_router", "--cov-report=term"] + posargs.remove("--coverage") session.install("-e", ".") + else: + session.log("Coverage won't be checked unless `-- --coverage` is defined.") session.run("pytest", "tests", *posargs) @session(tags=["test"]) -def test_javascript(session: Session) -> None: - session.chdir(ROOT / "js") - session.run("npm", "install", external=True) - session.run("npm", "run", "check") +def test_types(session: Session) -> None: + install_requirements_file(session, "check-types") + install_requirements_file(session, "pkg-deps") + session.run("mypy", "--show-error-codes", "src/reactpy_router", "tests") -def setup_docs(session: Session) -> None: - install_requirements(session, "build-docs") - session.install("-e", ".") - session.chdir("docs") +@session(tags=["test"]) +def test_style(session: Session) -> None: + install_requirements_file(session, "check-style") + session.run("black", ".", "--check") + session.run("ruff", "check", ".") + + +@session(tags=["test"]) +def test_javascript(session: Session) -> None: + install_requirements_file(session, "test-env") + session.chdir(ROOT_DIR / "src" / "js") + session.run("python", "-m", "nodejs.npm", "install", external=True) + session.run("python", "-m", "nodejs.npm", "run", "check") -def install_requirements(session: Session, name: str) -> None: - session.install("-r", str(REQUIREMENTS_DIR / f"{name}.txt")) +def install_requirements_file(session: Session, name: str) -> None: + session.install("--upgrade", "pip", "setuptools", "wheel") + file_path = ROOT_DIR / "requirements" / f"{name}.txt" + assert file_path.exists(), f"requirements file {file_path} does not exist" + session.install("-r", str(file_path)) diff --git a/pyproject.toml b/pyproject.toml index d645c38..763f3a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,22 @@ [build-system] -requires = ["setuptools>=40.8.0", "wheel"] +requires = ["setuptools>=42", "wheel", "nodejs-bin==18.4.0a4"] build-backend = "setuptools.build_meta" - -[tool.pytest.ini_options] -testpaths = "tests" -asyncio_mode = "auto" - - -[tool.isort] -profile = "black" - - [tool.mypy] ignore_missing_imports = true warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true +check_untyped_defs = true + +[tool.ruff.isort] +known-first-party = ["src", "tests"] + +[tool.ruff] +ignore = ["E501"] +extend-exclude = [".venv/*", ".eggs/*", ".nox/*", "build/*"] +line-length = 120 + +[tool.pytest.ini_options] +testpaths = "tests" +asyncio_mode = "auto" diff --git a/requirements.txt b/requirements.txt index 5893bf2..7c5eed9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -r requirements/build-docs.txt +-r requirements/build-pkg.txt -r requirements/check-style.txt -r requirements/check-types.txt --r requirements/nox-deps.txt -r requirements/pkg-deps.txt -r requirements/test-env.txt +-r requirements/test-run.txt diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt index 3f19165..0d2bca2 100644 --- a/requirements/build-docs.txt +++ b/requirements/build-docs.txt @@ -1,5 +1,11 @@ mkdocs -mkdocs-material -mkdocs-gen-files -mkdocs-literate-nav +mkdocs-git-revision-date-localized-plugin +mkdocs-material==9.4.0 +mkdocs-include-markdown-plugin +linkcheckmd +mkdocs-spellcheck[all] +mkdocs-git-authors-plugin +mkdocs-minify-plugin +mkdocs-section-index +mike mkdocstrings[python] diff --git a/requirements/build-pkg.txt b/requirements/build-pkg.txt new file mode 100644 index 0000000..88ec271 --- /dev/null +++ b/requirements/build-pkg.txt @@ -0,0 +1,3 @@ +twine +wheel +setuptools diff --git a/requirements/check-style.txt b/requirements/check-style.txt index 9a48a39..e4f6562 100644 --- a/requirements/check-style.txt +++ b/requirements/check-style.txt @@ -1,5 +1,2 @@ -black -flake8 -flake8-print -reactpy-flake8 -isort +black >=23,<24 +ruff diff --git a/requirements/test-env.txt b/requirements/test-env.txt index b6cdf49..4ddd635 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -3,3 +3,4 @@ pytest pytest-asyncio pytest-cov reactpy[testing,starlette] +nodejs-bin==18.4.0a4 diff --git a/requirements/nox-deps.txt b/requirements/test-run.txt similarity index 100% rename from requirements/nox-deps.txt rename to requirements/test-run.txt diff --git a/setup.cfg b/setup.cfg index 8d1ef40..e7be17a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,15 +1,6 @@ [bdist_wheel] universal=1 -[flake8] -ignore = E203, E266, E501, W503, F811, N802 -max-line-length = 88 -extend-exclude = - .nox - venv - .venv - tests/cases/* - [coverage:report] fail_under = 100 show_missing = True diff --git a/setup.py b/setup.py index f3c5f79..c643fa1 100644 --- a/setup.py +++ b/setup.py @@ -1,32 +1,33 @@ from __future__ import print_function -import os -import shutil -import subprocess import sys +import traceback +from distutils import log +from pathlib import Path -from setuptools import find_packages, setup +from nodejs import npm +from setuptools import find_namespace_packages, setup from setuptools.command.develop import develop from setuptools.command.sdist import sdist -# the name of the project +# ----------------------------------------------------------------------------- +# Basic Constants +# ----------------------------------------------------------------------------- name = "reactpy_router" - -# basic paths used to gather files -here = os.path.abspath(os.path.dirname(__file__)) -package_dir = os.path.join(here, name) +root_dir = Path(__file__).parent +src_dir = root_dir / "src" +package_dir = src_dir / name # ----------------------------------------------------------------------------- # General Package Info # ----------------------------------------------------------------------------- - - package = { "name": name, "python_requires": ">=3.9", - "packages": find_packages(exclude=["tests*"]), - "description": "A URL router for ReactPy", + "packages": find_namespace_packages(src_dir), + "package_dir": {"": "src"}, + "description": "A URL router for ReactPy.", "author": "Ryan Morshead", "author_email": "ryan.morshead@gmail.com", "url": "https://github.com/reactive-python/reactpy-router", @@ -38,9 +39,9 @@ "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets", "Typing :: Typed", @@ -49,38 +50,30 @@ # ----------------------------------------------------------------------------- -# Requirements +# Library Version # ----------------------------------------------------------------------------- - - -requirements = [] -with open(os.path.join(here, "requirements", "pkg-deps.txt"), "r") as f: - for line in map(str.strip, f): - if not line.startswith("#"): - requirements.append(line) -package["install_requires"] = requirements +for line in (package_dir / "__init__.py").read_text().split("\n"): + if line.startswith("__version__ = "): + package["version"] = eval(line.split("=", 1)[1]) + break +else: + print(f"No version found in {package_dir}/__init__.py") + sys.exit(1) # ----------------------------------------------------------------------------- -# Library Version +# Requirements # ----------------------------------------------------------------------------- - -with open(os.path.join(package_dir, "__init__.py")) as init_file: - for line in init_file: - if line.split("=", 1)[0].strip() == "__version__": - package["version"] = eval(line.split("=", 1)[1]) - break - else: - print("No version found in %s/__init__.py" % package_dir) # noqa: T201 - sys.exit(1) +requirements: list[str] = [] +with (root_dir / "requirements" / "pkg-deps.txt").open() as f: + requirements.extend(line for line in map(str.strip, f) if not line.startswith("#")) +package["install_requires"] = requirements # ----------------------------------------------------------------------------- # Library Description # ----------------------------------------------------------------------------- - - -with open(os.path.join(here, "README.md")) as f: +with (root_dir / "README.md").open() as f: long_description = f.read() package["long_description"] = long_description @@ -90,16 +83,26 @@ # ---------------------------------------------------------------------------- # Build Javascript # ---------------------------------------------------------------------------- - - -def build_javascript_first(cls): - class Command(cls): +def build_javascript_first(build_cls: type): + class Command(build_cls): def run(self): - npm = shutil.which("npm") # this is required on windows - if npm is None: - raise RuntimeError("NPM is not installed.") - for cmd_str in [f"{npm} install", f"{npm} run build"]: - subprocess.check_call(cmd_str.split(), cwd=os.path.join(here, "js")) + js_dir = str(src_dir / "js") + + log.info("Installing Javascript...") + result = npm.call(["install"], cwd=js_dir) + if result != 0: + log.error(traceback.format_exc()) + log.error("Failed to install Javascript") + raise RuntimeError("Failed to install Javascript") + + log.info("Building Javascript...") + result = npm.call(["run", "build"], cwd=js_dir) + if result != 0: + log.error(traceback.format_exc()) + log.error("Failed to build Javascript") + raise RuntimeError("Failed to build Javascript") + + log.info("Successfully built Javascript") super().run() return Command @@ -121,9 +124,7 @@ def run(self): # ----------------------------------------------------------------------------- -# Install It +# Installation # ----------------------------------------------------------------------------- - - if __name__ == "__main__": setup(**package) diff --git a/js/.eslintrc.json b/src/js/.eslintrc.json similarity index 100% rename from js/.eslintrc.json rename to src/js/.eslintrc.json diff --git a/js/package-lock.json b/src/js/package-lock.json similarity index 100% rename from js/package-lock.json rename to src/js/package-lock.json diff --git a/js/package.json b/src/js/package.json similarity index 100% rename from js/package.json rename to src/js/package.json diff --git a/js/rollup.config.js b/src/js/rollup.config.js similarity index 100% rename from js/rollup.config.js rename to src/js/rollup.config.js diff --git a/js/src/index.js b/src/js/src/index.js similarity index 100% rename from js/src/index.js rename to src/js/src/index.js diff --git a/reactpy_router/__init__.py b/src/reactpy_router/__init__.py similarity index 100% rename from reactpy_router/__init__.py rename to src/reactpy_router/__init__.py diff --git a/reactpy_router/core.py b/src/reactpy_router/core.py similarity index 98% rename from reactpy_router/core.py rename to src/reactpy_router/core.py index 9d8f7a2..490a78c 100644 --- a/reactpy_router/core.py +++ b/src/reactpy_router/core.py @@ -19,7 +19,7 @@ from reactpy.backend.hooks import ConnectionContext, use_connection from reactpy.backend.types import Connection, Location from reactpy.core.types import VdomChild, VdomDict -from reactpy.types import ComponentType, Context, Location +from reactpy.types import ComponentType, Context from reactpy.web.module import export, module_from_file from reactpy_router.types import Route, RouteCompiler, Router, RouteResolver diff --git a/reactpy_router/py.typed b/src/reactpy_router/py.typed similarity index 100% rename from reactpy_router/py.typed rename to src/reactpy_router/py.typed diff --git a/reactpy_router/simple.py b/src/reactpy_router/simple.py similarity index 100% rename from reactpy_router/simple.py rename to src/reactpy_router/simple.py diff --git a/reactpy_router/types.py b/src/reactpy_router/types.py similarity index 100% rename from reactpy_router/types.py rename to src/reactpy_router/types.py diff --git a/tests/test_core.py b/tests/test_core.py index 5f05f5c..77577b3 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,7 @@ +from typing import Any + from reactpy import Ref, component, html, use_location from reactpy.testing import DisplayFixture - from reactpy_router import link, route, simple, use_params, use_query @@ -39,7 +40,7 @@ def sample(): root_element = await display.root_element() except AttributeError: root_element = await display.page.wait_for_selector( - f"#display-{display._next_view_id}", state="attached" + f"#display-{display._next_view_id}", state="attached" # type: ignore ) assert not await root_element.inner_html() @@ -99,7 +100,7 @@ def sample(): async def test_use_params(display: DisplayFixture): - expected_params = {} + expected_params: dict[str, Any] = {} @component def check_params(): @@ -135,7 +136,7 @@ def sample(): async def test_use_query(display: DisplayFixture): - expected_query = {} + expected_query: dict[str, Any] = {} @component def check_query():