diff --git a/.github/docs-update.yml b/.github/docs-update.yml
new file mode 100644
index 00000000..e52b581a
--- /dev/null
+++ b/.github/docs-update.yml
@@ -0,0 +1,143 @@
+name: Publish Documentation
+
+on:
+ push:
+ branches:
+ - main
+ - docs-update
+ tags:
+ - '**'
+
+env:
+ COLUMNS: 150
+ PDM_DEPS: 'urllib3<2'
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - run: pip install -r src/python-fastui/requirements/all.txt
+ - run: pip install src/python-fastui
+
+ - run: npm install
+
+ - uses: pre-commit/action@v3.0.1
+ with:
+ extra_args: --all-files
+ env:
+ SKIP: no-commit-to-branch
+
+ test:
+ name: test ${{ matrix.python-version }} on ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-13, macos-latest]
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
+ exclude:
+ # Python 3.8 and 3.9 are not available on macOS 14
+ - os: macos-13
+ python-version: '3.10'
+ - os: macos-13
+ python-version: '3.11'
+ - os: macos-13
+ python-version: '3.12'
+ - os: macos-latest
+ python-version: '3.8'
+ - os: macos-latest
+ python-version: '3.9'
+
+ runs-on: ${{ matrix.os }}
+
+ env:
+ PYTHON: ${{ matrix.python-version }}
+ OS: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: set up python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - run: pip install -r src/python-fastui/requirements/test.txt
+ - run: pip install -r src/python-fastui/requirements/pyproject.txt
+ - run: pip install -e src/python-fastui
+
+ - run: coverage run -m pytest src
+ # display coverage and fail if it's below 80%, which shouldn't happen
+ - run: coverage report --fail-under=80
+
+ # test demo on 3.11 and 3.12, these tests are intentionally omitted from coverage
+ - if: matrix.python-version == '3.11' || matrix.python-version == '3.12'
+ run: pytest demo/tests.py
+
+ - run: coverage xml
+
+ - uses: codecov/codecov-action@v4
+ with:
+ file: ./coverage.xml
+ env_vars: PYTHON,OS
+
+ publish:
+ # Compare with the docs-build job in .github/workflows/ci.yml
+ needs: [lint, test]
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: checkout docs-site
+ uses: actions/checkout@v4
+ with:
+ ref: docs-site
+
+ - name: checkout current branch
+ uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+
+ - name: install
+ run: |
+ pip install --upgrade pip
+ pip install -r requirements/docs.txt
+ pip install --extra-index-url https://pydantic:${PPPR_TOKEN}@pppr.pydantic.dev/simple/ mkdocs-material mkdocstrings-python griffe-typedoc mkdocstrings-typescript
+ npm install
+ npm install -g typedoc
+ env:
+ PPPR_TOKEN: ${{ secrets.PPPR_TOKEN }}
+
+ - run: python -c 'import docs.plugins.main'
+
+ - name: Set git credentials
+ run: |
+ git config --global user.name "${{ github.actor }}"
+ git config --global user.email "${{ github.actor }}@users.noreply.github.com"
+
+ - run: mike deploy -b docs-site dev --push
+ if: "github.ref == 'refs/heads/main'"
+
+ - if: "github.ref == 'refs/heads/docs-update' || startsWith(github.ref, 'refs/tags/')"
+ id: check-version
+ uses: samuelcolvin/check-python-version@v4.1
+ with:
+ version_file_path: 'pydantic/version.py'
+ skip_env_check: true
+
+ - run: mike deploy -b docs-site ${{ steps.check-version.outputs.VERSION_MAJOR_MINOR }} latest --update-aliases --push
+ if: "(github.ref == 'refs/heads/docs-update' || startsWith(github.ref, 'refs/tags/')) && !fromJSON(steps.check-version.outputs.IS_PRERELEASE)"
+ env:
+ PYDANTIC_VERSION: v${{ steps.check-version.outputs.VERSION }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0c68c501..83a16615 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,6 +35,29 @@ jobs:
env:
SKIP: no-commit-to-branch
+ docs-build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+
+ - name: install
+ run: |
+ pip install --upgrade pip
+ pip install -r requirements/docs.txt
+ pip install --extra-index-url https://pydantic:${PPPR_TOKEN}@pppr.pydantic.dev/simple/ mkdocs-material mkdocstrings-python griffe-typedoc mkdocstrings-typescript
+ npm install
+ npm install -g typedoc
+ env:
+ PPPR_TOKEN: ${{ secrets.PPPR_TOKEN }}
+
+ - name: build site
+ run: mkdocs build --strict
+
test:
name: test ${{ matrix.python-version }} on ${{ matrix.os }}
strategy:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1d8d4b62..49bbffc5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,6 +4,7 @@ repos:
hooks:
- id: no-commit-to-branch
- id: check-yaml
+ args: ['--unsafe']
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
@@ -27,6 +28,7 @@ repos:
types_or: [javascript, jsx, ts, tsx, css, json, markdown]
entry: npm run prettier
language: system
+ exclude: '^docs/.*'
- id: js-lint
name: js-lint
types_or: [ts, tsx]
diff --git a/Makefile b/Makefile
index 023f415c..b30c3f3a 100644
--- a/Makefile
+++ b/Makefile
@@ -46,5 +46,13 @@ typescript-models:
dev:
uvicorn demo:app --reload --reload-dir .
+.PHONY: docs
+docs:
+ mkdocs build
+
+.PHONY: serve
+serve:
+ mkdocs serve
+
.PHONY: all
all: testcov lint
diff --git a/build-docs.sh b/build-docs.sh
new file mode 100755
index 00000000..22f3875a
--- /dev/null
+++ b/build-docs.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -e
+set -x
+
+python3 -V
+
+python3 -m pip install -r ./requirements/docs.txt
+pip install --extra-index-url https://pydantic:$PPPR_TOKEN@pppr.pydantic.dev/simple/ mkdocs-material mkdocstrings-python griffe-typedoc mkdocstrings-typescript
+npm install
+npm install -g typedoc
+
+python3 -m mkdocs build
diff --git a/docs/api/python_components.md b/docs/api/python_components.md
new file mode 100644
index 00000000..1d8b5663
--- /dev/null
+++ b/docs/api/python_components.md
@@ -0,0 +1,47 @@
+# Python Components
+
+::: fastui.components
+ handler: python
+ options:
+ inherited_members: true
+ docstring_options:
+ ignore_init_summary: false
+ members:
+ - Text
+ - Paragraph
+ - PageTitle
+ - Div
+ - Page
+ - Heading
+ - Markdown
+ - Code
+ - Json
+ - Button
+ - Link
+ - LinkList
+ - Navbar
+ - Modal
+ - ServerLoad
+ - Image
+ - Iframe
+ - FireEvent
+ - Error
+ - Spinner
+ - Toast
+ - Custom
+ - Table
+ - Pagination
+ - Display
+ - Details
+ - Form
+ - FormField
+ - ModelForm
+ - Footer
+ - AnyComponent
+ - FormFieldBoolean
+ - FormFieldFile
+ - FormFieldInput
+ - FormFieldSelect
+ - FormFieldSelectSearch
+
+
diff --git a/docs/api/typescript_components.md b/docs/api/typescript_components.md
new file mode 100644
index 00000000..29d706a6
--- /dev/null
+++ b/docs/api/typescript_components.md
@@ -0,0 +1,7 @@
+# TypeScript Components
+
+!!! warning "🚧 Work in Progress"
+ This page is a work in progress.
+
+::: @pydantic/fastui
+ handler: typescript
diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png
new file mode 100644
index 00000000..dac6c476
Binary files /dev/null and b/docs/assets/favicon.png differ
diff --git a/docs/assets/logo-white.svg b/docs/assets/logo-white.svg
new file mode 100644
index 00000000..61cc5bdb
--- /dev/null
+++ b/docs/assets/logo-white.svg
@@ -0,0 +1,5 @@
+
diff --git a/docs/extra/tweaks.css b/docs/extra/tweaks.css
new file mode 100644
index 00000000..0f6f7dba
--- /dev/null
+++ b/docs/extra/tweaks.css
@@ -0,0 +1,5 @@
+/* Revert hue value to that of pre mkdocs-material v9.4.0 */
+[data-md-color-scheme='slate'] {
+ --md-hue: 230;
+ --md-default-bg-color: hsla(230, 15%, 21%, 1);
+}
diff --git a/docs/guide.md b/docs/guide.md
new file mode 100644
index 00000000..8a5c4ccd
--- /dev/null
+++ b/docs/guide.md
@@ -0,0 +1,2 @@
+!!! warning "🚧 Work in Progress"
+ This page is a work in progress.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..92b07c5a
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,141 @@
+# FastUI
+
+## The Principle
+
+FastUI is a new way to build web application user interfaces defined by declarative Python code.
+
+This means:
+
+- **If you're a Python developer** — you can build responsive web applications using React without writing a single line of JavaScript, or touching `npm`.
+- **If you're a frontend developer** — you can concentrate on building magical components that are truly reusable, no copy-pasting components for each view.
+- **For everyone** — a true separation of concerns, the backend defines the entire application; while the frontend is free to implement just the user interface
+
+At its heart, FastUI is a set of matching [Pydantic](https://docs.pydantic.dev) models and TypeScript interfaces that allow you to define a user interface. This interface is validated at build time by TypeScript and pyright/mypy and at runtime by Pydantic.
+
+You can see a simple demo of an application built with FastUI [here](https://fastui-demo.onrender.com).
+
+## The Practice - Installation
+
+FastUI is made up of 4 things:
+
+- [`fastui` PyPI package](https://pypi.python.org/pypi/fastui) — Pydantic models for UI components, and some utilities. While it works well with [FastAPI](https://fastapi.tiangolo.com) it doesn't depend on FastAPI, and most of it could be used with any python web framework.
+- [`@pydantic/fastui` npm package](https://www.npmjs.com/package/@pydantic/fastui) — a React TypeScript package that lets you reuse the machinery and types of FastUI while implementing your own components
+- [`@pydantic/fastui-bootstrap` npm package](https://www.npmjs.com/package/@pydantic/fastui-bootstrap) — implementation/customisation of all FastUI components using [Bootstrap](https://getbootstrap.com)
+- [`@pydantic/fastui-prebuilt` npm package](https://www.jsdelivr.com/package/npm/@pydantic/fastui-prebuilt) (available on [jsdelivr.com CDN](https://www.jsdelivr.com/package/npm/@pydantic/fastui-prebuilt)) providing a pre-built version of the FastUI React app so you can use it without installing any npm packages or building anything yourself. The Python package provides a simple HTML page to serve this app.
+
+## Usage
+
+Here's a simple but complete FastAPI application that uses FastUI to show some user profiles:
+
+```python
+from datetime import date
+
+from fastapi import FastAPI, HTTPException
+from fastapi.responses import HTMLResponse
+from fastui import FastUI, AnyComponent, prebuilt_html, components as c
+from fastui.components.display import DisplayMode, DisplayLookup
+from fastui.events import GoToEvent, BackEvent
+from pydantic import BaseModel, Field
+
+app = FastAPI()
+
+
+class User(BaseModel):
+ id: int
+ name: str
+ dob: date = Field(title='Date of Birth')
+
+
+# define some users
+users = [
+ User(id=1, name='John', dob=date(1990, 1, 1)),
+ User(id=2, name='Jack', dob=date(1991, 1, 1)),
+ User(id=3, name='Jill', dob=date(1992, 1, 1)),
+ User(id=4, name='Jane', dob=date(1993, 1, 1)),
+]
+
+
+@app.get("/api/", response_model=FastUI, response_model_exclude_none=True)
+def users_table() -> list[AnyComponent]:
+ """
+ Show a table of four users, `/api` is the endpoint the frontend will connect to
+ when a user visits `/` to fetch components to render.
+ """
+ return [
+ c.Page( # Page provides a basic container for components
+ components=[
+ c.Heading(text='Users', level=2), # renders `
Users
`
+ c.Table(
+ data=users,
+ # define two columns for the table
+ columns=[
+ # the first is the users, name rendered as a link to their profile
+ DisplayLookup(field='name', on_click=GoToEvent(url='/user/{id}/')),
+ # the second is the date of birth, rendered as a date
+ DisplayLookup(field='dob', mode=DisplayMode.date),
+ ],
+ ),
+ ]
+ ),
+ ]
+
+
+@app.get("/api/user/{user_id}/", response_model=FastUI, response_model_exclude_none=True)
+def user_profile(user_id: int) -> list[AnyComponent]:
+ """
+ User profile page, the frontend will fetch this when the user visits `/user/{id}/`.
+ """
+ try:
+ user = next(u for u in users if u.id == user_id)
+ except StopIteration:
+ raise HTTPException(status_code=404, detail="User not found")
+ return [
+ c.Page(
+ components=[
+ c.Heading(text=user.name, level=2),
+ c.Link(components=[c.Text(text='Back')], on_click=BackEvent()),
+ c.Details(data=user),
+ ]
+ ),
+ ]
+
+
+@app.get('/{path:path}')
+async def html_landing() -> HTMLResponse:
+ """Simple HTML page which serves the React app, comes last as it matches all paths."""
+ return HTMLResponse(prebuilt_html(title='FastUI Demo'))
+```
+
+Which renders like this:
+
+
+
+Of course, that's a very simple application, the [full demo](https://fastui-demo.onrender.com) is more complete.
+
+### The Principle (long version)
+
+FastUI is an implementation of the RESTful principle; but not as it's usually understood, instead I mean the principle defined in the original [PhD dissertation](https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) by Roy Fielding, and excellently summarised in [this essay on htmx.org](https://htmx.org/essays/how-did-rest-come-to-mean-the-opposite-of-rest/) (HTMX people, I'm sorry to use your article to promote React which I know you despise 🙏).
+
+The RESTful principle as described in the HTMX article is that the frontend doesn't need to (and shouldn't) know anything about the application you're building. Instead, it should just provide all the components you need to construct the interface, the backend can then tell the frontend what to do.
+
+Think of your frontend as a puppet, and the backend as the hand within it — the puppet doesn't need to know what to say, that's kind of the point.
+
+Building an application this way has a number of significant advantages:
+
+- You only need to write code in one place to build a new feature — add a new view, change the behavior of an existing view or alter the URL structure
+- Deploying the front and backend can be completely decoupled, provided the frontend knows how to render all the components the backend is going to ask it to use, you're good to go
+- You should be able to reuse a rich set of opensource components, they should end up being better tested and more reliable than anything you could build yourself, this is possible because the components need no context about how they're going to be used (note: since FastUI is brand new, this isn't true yet, hopefully we get there)
+- We can use Pydantic, TypeScript and JSON Schema to provide guarantees that the two sides are communicating with an agreed schema
+
+In the abstract, FastUI is like the opposite of GraphQL but with the same goal — GraphQL lets frontend developers extend an application without any new backend development; FastUI lets backend developers extend an application without any new frontend development.
+
+#### Beyond Python and React
+
+Of course, this principle shouldn't be limited to Python and React applications — provided we use the same set of agreed schemas and encoding to communicate, we should be able to use any frontend and backend that implements the schema. Interchangeably.
+
+This could mean:
+
+- Implementing a web frontend using another JS framework like Vue — lots of work, limited value
+- Implementing a web frontend using an edge server, so the browser just sees HTML — lots of work but very valuable
+- Implementing frontends for other platforms like mobile or IOT — lots of work, no idea if it's actually a good idea?
+- Implementing the component models in another language like Rust or Go — since there's actually not that much code in the backend, so this would be a relatively small and mechanical task
diff --git a/docs/plugins.py b/docs/plugins.py
new file mode 100644
index 00000000..06f99315
--- /dev/null
+++ b/docs/plugins.py
@@ -0,0 +1,74 @@
+import os
+import re
+
+from typing import Match
+
+from mkdocs.config import Config
+from mkdocs.structure.files import Files
+from mkdocs.structure.pages import Page
+
+try:
+ import pytest
+except ImportError:
+ pytest = None
+
+
+def on_pre_build(config: Config):
+ pass
+
+
+def on_files(files: Files, config: Config) -> Files:
+ return remove_files(files)
+
+
+def remove_files(files: Files) -> Files:
+ to_remove = []
+ for file in files:
+ if file.src_path in {'plugins.py'}:
+ to_remove.append(file)
+ elif file.src_path.startswith('__pycache__/'):
+ to_remove.append(file)
+
+ for f in to_remove:
+ files.remove(f)
+
+ return files
+
+
+def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str:
+ markdown = remove_code_fence_attributes(markdown)
+ return add_version(markdown, page)
+
+
+def add_version(markdown: str, page: Page) -> str:
+ if page.file.src_uri == 'index.md':
+ version_ref = os.getenv('GITHUB_REF')
+ if version_ref and version_ref.startswith('refs/tags/'):
+ version = re.sub('^refs/tags/', '', version_ref.lower())
+ url = f'https://github.com/pydantic/FastUI/releases/tag/{version}'
+ version_str = f'Documentation for version: [{version}]({url})'
+ elif sha := os.getenv('GITHUB_SHA'):
+ sha = sha[:7]
+ url = f'https://github.com/pydantic/FastUI/commit/{sha}'
+ version_str = f'Documentation for development version: [{sha}]({url})'
+ else:
+ version_str = 'Documentation for development version'
+ markdown = re.sub(r'{{ *version *}}', version_str, markdown)
+ return markdown
+
+
+def remove_code_fence_attributes(markdown: str) -> str:
+ """
+ There's no way to add attributes to code fences that works with both pycharm and mkdocs, hence we use
+ `py key="value"` to provide attributes to pytest-examples, then remove those attributes here.
+
+ https://youtrack.jetbrains.com/issue/IDEA-297873 & https://python-markdown.github.io/extensions/fenced_code_blocks/
+ """
+
+ def remove_attrs(match: Match[str]) -> str:
+ suffix = re.sub(
+ r' (?:test|lint|upgrade|group|requires|output|rewrite_assert)=".+?"', '', match.group(2), flags=re.M
+ )
+ return f'{match.group(1)}{suffix}'
+
+ return re.sub(r'^( *``` *py)(.*)', remove_attrs, markdown, flags=re.M)
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..691434af
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,99 @@
+site_name: FastUI
+site_description: Build web application user interfaces defined by declarative Python code.
+site_url: https://docs.pydantic.dev/fastui/
+
+theme:
+ name: 'material'
+ palette:
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: pink
+ accent: pink
+ toggle:
+ icon: material/lightbulb-outline
+ name: "Switch to dark mode"
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: pink
+ accent: pink
+ toggle:
+ icon: material/lightbulb
+ name: "Switch to light mode"
+ features:
+ - content.code.annotate
+ - content.tabs.link
+ - content.code.copy
+ - announce.dismiss
+ - navigation.tabs
+ - search.suggest
+ - search.highlight
+ logo: assets/logo-white.svg
+ favicon: assets/favicon.png
+
+repo_name: pydantic/FastUI
+repo_url: https://github.com/pydantic/FastUI
+edit_uri: ''
+
+# https://www.mkdocs.org/user-guide/configuration/#validation
+validation:
+ omitted_files: warn
+ absolute_links: warn
+ unrecognized_links: warn
+
+extra_css:
+ - 'extra/tweaks.css'
+
+# TODO: add flarelytics support
+# extra_javascript:
+# - '/flarelytics/client.js'
+
+markdown_extensions:
+ - toc:
+ permalink: true
+ - admonition
+ - pymdownx.details
+ - pymdownx.extra
+ - pymdownx.superfences
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - attr_list
+ - md_in_html
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+extra:
+ version:
+ provider: mike
+watch:
+ - src
+plugins:
+ - mike:
+ alias_type: symlink
+ canonical_version: latest
+ - search
+ - mkdocstrings:
+ handlers:
+ python:
+ paths:
+ - src/python-fastui
+ options:
+ members_order: source
+ separate_signature: true
+ docstring_options:
+ ignore_init_summary: true
+ merge_init_into_class: true
+ show_signature_annotations: true
+ signature_crossrefs: true
+ - mkdocs-simple-hooks:
+ hooks:
+ on_pre_build: 'docs.plugins:on_pre_build'
+ on_files: 'docs.plugins:on_files'
+ on_page_markdown: 'docs.plugins:on_page_markdown'
+nav:
+ - Introduction: index.md
+ - Guide: guide.md
+ - API Documentation:
+ - Python Components: api/python_components.md
+ - TypeScript Components: api/typescript_components.md
diff --git a/package-lock.json b/package-lock.json
index 1c67808a..5cc21c61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,10 @@
"workspaces": [
"src/*"
],
+ "dependencies": {
+ "prettier": "^3.2.5",
+ "typedoc": "^0.25.13"
+ },
"devDependencies": {
"@types/node": "^20.9.1",
"@types/react": "^18.2.15",
@@ -22,7 +26,6 @@
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-simple-import-sort": "^10.0.0",
"json-schema-to-typescript": "^13.1.1",
- "prettier": "^3.0.3",
"typescript": "^5.0.2"
}
},
@@ -1715,6 +1718,11 @@
"node": ">=8"
}
},
+ "node_modules/ansi-sequence-parser": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz",
+ "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg=="
+ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -1932,8 +1940,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/binary-extensions": {
"version": "2.2.0",
@@ -4232,6 +4239,11 @@
"json5": "lib/cli.js"
}
},
+ "node_modules/jsonc-parser": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
+ "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA=="
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -4355,6 +4367,11 @@
"es5-ext": "~0.10.2"
}
},
+ "node_modules/lunr": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
+ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="
+ },
"node_modules/markdown-table": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz",
@@ -4364,6 +4381,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/marked": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
+ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/mdast-util-find-and-replace": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz",
@@ -5559,10 +5587,9 @@
}
},
"node_modules/prettier": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
- "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
- "dev": true,
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
+ "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -6146,6 +6173,17 @@
"node": ">=8"
}
},
+ "node_modules/shiki": {
+ "version": "0.14.7",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz",
+ "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==",
+ "dependencies": {
+ "ansi-sequence-parser": "^1.1.0",
+ "jsonc-parser": "^3.2.0",
+ "vscode-oniguruma": "^1.7.0",
+ "vscode-textmate": "^8.0.0"
+ }
+ },
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -6528,11 +6566,52 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/typedoc": {
+ "version": "0.25.13",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz",
+ "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==",
+ "dependencies": {
+ "lunr": "^2.3.9",
+ "marked": "^4.3.0",
+ "minimatch": "^9.0.3",
+ "shiki": "^0.14.7"
+ },
+ "bin": {
+ "typedoc": "bin/typedoc"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "peerDependencies": {
+ "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x"
+ }
+ },
+ "node_modules/typedoc/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/typedoc/node_modules/minimatch": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+ "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
- "dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6761,6 +6840,16 @@
}
}
},
+ "node_modules/vscode-oniguruma": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
+ "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA=="
+ },
+ "node_modules/vscode-textmate": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
+ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg=="
+ },
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@@ -6954,7 +7043,7 @@
},
"src/npm-fastui": {
"name": "@pydantic/fastui",
- "version": "0.0.21",
+ "version": "0.0.22",
"license": "MIT",
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
@@ -6971,7 +7060,7 @@
},
"src/npm-fastui-bootstrap": {
"name": "@pydantic/fastui-bootstrap",
- "version": "0.0.21",
+ "version": "0.0.22",
"license": "MIT",
"dependencies": {
"bootstrap": "^5.3.2",
@@ -6981,12 +7070,12 @@
"sass": "^1.69.5"
},
"peerDependencies": {
- "@pydantic/fastui": "0.0.21"
+ "@pydantic/fastui": "0.0.22"
}
},
"src/npm-fastui-prebuilt": {
"name": "@pydantic/fastui-prebuilt",
- "version": "0.0.21",
+ "version": "0.0.22",
"license": "MIT",
"devDependencies": {
"@vitejs/plugin-react-swc": "^3.3.2",
diff --git a/package.json b/package.json
index 98639d0a..6036751c 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,10 @@
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-simple-import-sort": "^10.0.0",
"json-schema-to-typescript": "^13.1.1",
- "prettier": "^3.0.3",
"typescript": "^5.0.2"
+ },
+ "dependencies": {
+ "prettier": "^3.2.5",
+ "typedoc": "^0.25.13"
}
}
diff --git a/requirements/docs.in b/requirements/docs.in
new file mode 100644
index 00000000..90cc494b
--- /dev/null
+++ b/requirements/docs.in
@@ -0,0 +1,7 @@
+mike
+mkdocs
+mkdocs-material
+mkdocs-simple-hooks
+mkdocstrings[python]
+mkdocs-redirects
+mkdocs-material-extensions
diff --git a/requirements/docs.txt b/requirements/docs.txt
new file mode 100644
index 00000000..78515091
--- /dev/null
+++ b/requirements/docs.txt
@@ -0,0 +1,122 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile --output-file=requirements/docs.txt requirements/docs.in
+#
+babel==2.14.0
+ # via mkdocs-material
+certifi==2024.2.2
+ # via requests
+charset-normalizer==3.3.2
+ # via requests
+click==8.1.7
+ # via
+ # mkdocs
+ # mkdocstrings
+colorama==0.4.6
+ # via
+ # griffe
+ # mkdocs-material
+ghp-import==2.1.0
+ # via mkdocs
+griffe==0.44.0
+ # via mkdocstrings-python
+idna==3.7
+ # via requests
+importlib-metadata==7.1.0
+ # via mike
+importlib-resources==6.4.0
+ # via mike
+jinja2==3.1.3
+ # via
+ # mike
+ # mkdocs
+ # mkdocs-material
+ # mkdocstrings
+markdown==3.6
+ # via
+ # mkdocs
+ # mkdocs-autorefs
+ # mkdocs-material
+ # mkdocstrings
+ # pymdown-extensions
+markupsafe==2.1.5
+ # via
+ # jinja2
+ # mkdocs
+ # mkdocs-autorefs
+ # mkdocstrings
+mergedeep==1.3.4
+ # via mkdocs
+mike==2.0.0
+ # via -r requirements/docs.in
+mkdocs==1.5.3
+ # via
+ # -r requirements/docs.in
+ # mike
+ # mkdocs-autorefs
+ # mkdocs-material
+ # mkdocs-redirects
+ # mkdocs-simple-hooks
+ # mkdocstrings
+mkdocs-autorefs==1.0.1
+ # via mkdocstrings
+mkdocs-material==9.5.18
+ # via -r requirements/docs.in
+mkdocs-material-extensions==1.3.1
+ # via
+ # -r requirements/docs.in
+ # mkdocs-material
+mkdocs-redirects==1.2.1
+ # via -r requirements/docs.in
+mkdocs-simple-hooks==0.1.5
+ # via -r requirements/docs.in
+mkdocstrings[python]==0.24.3
+ # via
+ # -r requirements/docs.in
+ # mkdocstrings-python
+mkdocstrings-python==1.10.0
+ # via mkdocstrings
+packaging==24.0
+ # via mkdocs
+paginate==0.5.6
+ # via mkdocs-material
+pathspec==0.12.1
+ # via mkdocs
+platformdirs==4.2.0
+ # via
+ # mkdocs
+ # mkdocstrings
+pygments==2.17.2
+ # via mkdocs-material
+pymdown-extensions==10.8
+ # via
+ # mkdocs-material
+ # mkdocstrings
+pyparsing==3.1.2
+ # via mike
+python-dateutil==2.9.0.post0
+ # via ghp-import
+pyyaml==6.0.1
+ # via
+ # mike
+ # mkdocs
+ # pymdown-extensions
+ # pyyaml-env-tag
+pyyaml-env-tag==0.1
+ # via mkdocs
+regex==2024.4.16
+ # via mkdocs-material
+requests==2.31.0
+ # via mkdocs-material
+six==1.16.0
+ # via python-dateutil
+urllib3==2.2.1
+ # via requests
+verspec==0.1.0
+ # via mike
+watchdog==4.0.0
+ # via mkdocs
+zipp==3.18.1
+ # via importlib-metadata
diff --git a/src/npm-fastui-bootstrap/typedoc.json b/src/npm-fastui-bootstrap/typedoc.json
new file mode 100644
index 00000000..4d479e43
--- /dev/null
+++ b/src/npm-fastui-bootstrap/typedoc.json
@@ -0,0 +1,5 @@
+{
+ "extends": ["../../typedoc.base.json"],
+ "entryPointStrategy": "expand",
+ "entryPoints": ["src"]
+}
diff --git a/src/npm-fastui-prebuilt/typedoc.json b/src/npm-fastui-prebuilt/typedoc.json
new file mode 100644
index 00000000..4d479e43
--- /dev/null
+++ b/src/npm-fastui-prebuilt/typedoc.json
@@ -0,0 +1,5 @@
+{
+ "extends": ["../../typedoc.base.json"],
+ "entryPointStrategy": "expand",
+ "entryPoints": ["src"]
+}
diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts
index e304607f..f3096397 100644
--- a/src/npm-fastui/src/models.d.ts
+++ b/src/npm-fastui/src/models.d.ts
@@ -58,6 +58,9 @@ export type JsonData =
}
export type AnyEvent = PageEvent | GoToEvent | BackEvent | AuthEvent
export type NamedStyle = 'primary' | 'secondary' | 'warning'
+/**
+ * Display mode for a value.
+ */
export type DisplayMode =
| 'auto'
| 'plain'
@@ -70,35 +73,47 @@ export type DisplayMode =
| 'inline_code'
export type SelectOptions = SelectOption[] | SelectGroup[]
+/**
+ * Text component that displays a string.
+ */
export interface Text {
text: string
type: 'Text'
}
+/**
+ * Paragraph component that displays a string as a paragraph.
+ */
export interface Paragraph {
text: string
className?: ClassName
type: 'Paragraph'
}
/**
- * This sets the title of the HTML page via the `document.title` property.
+ * Sets the title of the HTML page via the `document.title` property.
*/
export interface PageTitle {
text: string
type: 'PageTitle'
}
+/**
+ * A generic container component.
+ */
export interface Div {
components: FastProps[]
className?: ClassName
type: 'Div'
}
/**
- * Similar to `container` in many UI frameworks, this should be a reasonable root component for most pages.
+ * Similar to `container` in many UI frameworks, this acts as a root component for most pages.
*/
export interface Page {
components: FastProps[]
className?: ClassName
type: 'Page'
}
+/**
+ * Heading component.
+ */
export interface Heading {
text: string
level: 1 | 2 | 3 | 4 | 5 | 6
@@ -106,12 +121,18 @@ export interface Heading {
className?: ClassName
type: 'Heading'
}
+/**
+ * Markdown component that renders markdown text.
+ */
export interface Markdown {
text: string
codeStyle?: string
className?: ClassName
type: 'Markdown'
}
+/**
+ * Code component that renders code with syntax highlighting.
+ */
export interface Code {
text: string
language?: string
@@ -119,11 +140,17 @@ export interface Code {
className?: ClassName
type: 'Code'
}
+/**
+ * JSON component that renders JSON data.
+ */
export interface Json {
value: JsonData
className?: ClassName
type: 'JSON'
}
+/**
+ * Button component.
+ */
export interface Button {
text: string
onClick?: AnyEvent
@@ -159,6 +186,9 @@ export interface AuthEvent {
url?: string
type: 'auth'
}
+/**
+ * Link component.
+ */
export interface Link {
components: FastProps[]
onClick?: PageEvent | GoToEvent | BackEvent | AuthEvent
@@ -168,12 +198,18 @@ export interface Link {
className?: ClassName
type: 'Link'
}
+/**
+ * List of Link components.
+ */
export interface LinkList {
links: Link[]
mode?: 'tabs' | 'vertical' | 'pagination'
className?: ClassName
type: 'LinkList'
}
+/**
+ * Navbar component used for moving between pages.
+ */
export interface Navbar {
title?: string
titleEvent?: PageEvent | GoToEvent | BackEvent | AuthEvent
@@ -182,12 +218,18 @@ export interface Navbar {
className?: ClassName
type: 'Navbar'
}
+/**
+ * Footer component.
+ */
export interface Footer {
links: Link[]
extraText?: string
className?: ClassName
type: 'Footer'
}
+/**
+ * Modal component that displays a modal dialog.
+ */
export interface Modal {
title: string
body: FastProps[]
@@ -209,6 +251,9 @@ export interface ServerLoad {
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
type: 'ServerLoad'
}
+/**
+ * Image container component.
+ */
export interface Image {
src: string
alt?: string
@@ -228,6 +273,9 @@ export interface Image {
className?: ClassName
type: 'Image'
}
+/**
+ * Iframe component that displays content from a URL.
+ */
export interface Iframe {
src: string
title?: string
@@ -238,6 +286,9 @@ export interface Iframe {
sandbox?: string
type: 'Iframe'
}
+/**
+ * Video component that displays a video or multiple videos.
+ */
export interface Video {
sources: string[]
autoplay?: boolean
@@ -247,14 +298,20 @@ export interface Video {
poster?: string
width?: string | number
height?: string | number
- type: 'Video'
className?: ClassName
+ type: 'Video'
}
+/**
+ * Fire an event.
+ */
export interface FireEvent {
event: AnyEvent
message?: string
type: 'FireEvent'
}
+/**
+ * Utility component used to display an error.
+ */
export interface Error {
title: string
description: string
@@ -263,11 +320,17 @@ export interface Error {
type: 'Error'
children?: ReactNode
}
+/**
+ * Spinner component that displays a loading spinner.
+ */
export interface Spinner {
text?: string
className?: ClassName
type: 'Spinner'
}
+/**
+ * Custom component that allows for special data to be rendered.
+ */
export interface Custom {
data: JsonData
subType: string
@@ -275,6 +338,9 @@ export interface Custom {
className?: ClassName
type: 'Custom'
}
+/**
+ * Table component.
+ */
export interface Table {
data: DataModel[]
columns: DisplayLookup[]
@@ -295,13 +361,16 @@ export interface DisplayLookup {
field: string
tableWidthPercent?: number
}
+/**
+ * Pagination component to use with tables.
+ */
export interface Pagination {
page: number
pageSize: number
total: number
+ pageQueryParam?: string
className?: ClassName
type: 'Pagination'
- pageQueryParam?: string
pageCount: number
}
/**
@@ -314,12 +383,18 @@ export interface Display {
value: JsonData
type: 'Display'
}
+/**
+ * Details associated with displaying a data model.
+ */
export interface Details {
data: DataModel
fields: DisplayLookup[]
className?: ClassName
type: 'Details'
}
+/**
+ * Form component.
+ */
export interface Form {
submitUrl: string
initial?: {
@@ -342,6 +417,9 @@ export interface Form {
)[]
type: 'Form'
}
+/**
+ * Form field for basic input.
+ */
export interface FormFieldInput {
name: string
title: string[] | string
@@ -357,6 +435,9 @@ export interface FormFieldInput {
autocomplete?: string
type: 'FormFieldInput'
}
+/**
+ * Form field for text area input.
+ */
export interface FormFieldTextarea {
name: string
title: string[] | string
@@ -373,6 +454,9 @@ export interface FormFieldTextarea {
autocomplete?: string
type: 'FormFieldTextarea'
}
+/**
+ * Form field for boolean input.
+ */
export interface FormFieldBoolean {
name: string
title: string[] | string
@@ -386,6 +470,9 @@ export interface FormFieldBoolean {
mode?: 'checkbox' | 'switch'
type: 'FormFieldBoolean'
}
+/**
+ * Form field for file input.
+ */
export interface FormFieldFile {
name: string
title: string[] | string
@@ -399,6 +486,9 @@ export interface FormFieldFile {
accept?: string
type: 'FormFieldFile'
}
+/**
+ * Form field for select input.
+ */
export interface FormFieldSelect {
name: string
title: string[] | string
@@ -424,6 +514,9 @@ export interface SelectGroup {
label: string
options: SelectOption[]
}
+/**
+ * Form field for searchable select input.
+ */
export interface FormFieldSelectSearch {
name: string
title: string[] | string
@@ -440,6 +533,9 @@ export interface FormFieldSelectSearch {
placeholder?: string
type: 'FormFieldSelectSearch'
}
+/**
+ * Form component generated from a Pydantic model.
+ */
export interface ModelForm {
submitUrl: string
initial?: {
@@ -462,6 +558,9 @@ export interface ModelForm {
| FormFieldSelectSearch
)[]
}
+/**
+ * Toast component that displays a toast message (small temporary message).
+ */
export interface Toast {
title: string
body: FastProps[]
diff --git a/src/npm-fastui/typedoc.json b/src/npm-fastui/typedoc.json
new file mode 100644
index 00000000..e0896402
--- /dev/null
+++ b/src/npm-fastui/typedoc.json
@@ -0,0 +1,8 @@
+// TODO: we really want the entryPoint to be src, but mkdocstrings-typescript doesn't fully support this option yet,
+// as we can't specify the path within the package from which we want to pull docs, so right now
+// we resort to more specific entryPoints
+{
+ "extends": ["../../typedoc.base.json"],
+ "entryPointStrategy": "expand",
+ "entryPoints": ["src/models.d.ts"]
+}
diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py
index 6594863e..622a02c5 100644
--- a/src/python-fastui/fastui/components/__init__.py
+++ b/src/python-fastui/fastui/components/__init__.py
@@ -1,8 +1,6 @@
"""
Component definitions.
-NOTE: all imports should be "simple" so the namespace of the module is polluted as little as possible.
-
All CamelCase names in the namespace should be components.
"""
import typing as _t
@@ -49,6 +47,7 @@
'FireEvent',
'Error',
'Spinner',
+ 'Toast',
'Custom',
# then we include components from other files
'Table',
@@ -71,47 +70,81 @@
class Text(_p.BaseModel, extra='forbid'):
+ """Text component that displays a string."""
+
text: str
+ """The text to display."""
+
type: _t.Literal['Text'] = 'Text'
+ """The type of the component. Always 'Text'."""
class Paragraph(_p.BaseModel, extra='forbid'):
+ """Paragraph component that displays a string as a paragraph."""
+
text: str
+ """The text to display."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the paragraph's HTML component."""
+
type: _t.Literal['Paragraph'] = 'Paragraph'
+ """The type of the component. Always 'Paragraph'."""
class PageTitle(_p.BaseModel, extra='forbid'):
- """
- This sets the title of the HTML page via the `document.title` property.
- """
+ """Sets the title of the HTML page via the `document.title` property."""
text: str
+ """The text to set as the page title."""
+
type: _t.Literal['PageTitle'] = 'PageTitle'
+ """The type of the component. Always 'PageTitle'."""
class Div(_p.BaseModel, extra='forbid'):
+ """A generic container component."""
+
components: '_t.List[AnyComponent]'
+ """List of components to render inside the div."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the div's HTML component."""
+
type: _t.Literal['Div'] = 'Div'
+ """The type of the component. Always 'Div'."""
class Page(_p.BaseModel, extra='forbid'):
- """
- Similar to `container` in many UI frameworks, this should be a reasonable root component for most pages.
- """
+ """Similar to `container` in many UI frameworks, this acts as a root component for most pages."""
components: '_t.List[AnyComponent]'
+ """List of components to render on the page."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the page's HTML component."""
+
type: _t.Literal['Page'] = 'Page'
+ """The type of the component. Always 'Page'."""
class Heading(_p.BaseModel, extra='forbid'):
+ """Heading component."""
+
text: str
+ """The text to display in the heading."""
+
level: _t.Literal[1, 2, 3, 4, 5, 6] = 1
+ """The level of the heading. 1 is the largest, 6 is the smallest."""
+
html_id: _t.Union[str, None] = _p.Field(default=None, serialization_alias='htmlId')
+ """Optional HTML ID to apply to the heading's HTML component."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the page's HTML component."""
+
type: _t.Literal['Heading'] = 'Heading'
+ """The type of the component. Always 'Heading'."""
@classmethod
def __get_pydantic_json_schema__(
@@ -123,67 +156,152 @@ def __get_pydantic_json_schema__(
return json_schema
-# see https://github.com/PrismJS/prism-themes
-# and https://cdn.jsdelivr.net/npm/react-syntax-highlighter@15.5.0/dist/esm/styles/prism/index.js
CodeStyle = _te.Annotated[_t.Union[str, None], _p.Field(serialization_alias='codeStyle')]
+"""
+Code style to apply to a `Code` component.
+
+Attributes:
+ codeStyle: The code style to apply. If None, no style is applied.
+
+See Also:
+ - [PrismJS Themes](https://github.com/PrismJS/prism-themes)
+ - [PrismJS Theme Index](https://cdn.jsdelivr.net/npm/react-syntax-highlighter@15.5.0/dist/esm/styles/prism/index.js)
+"""
class Markdown(_p.BaseModel, extra='forbid'):
+ """Markdown component that renders markdown text."""
+
text: str
+ """The markdown text to render."""
+
code_style: CodeStyle = None
+ """Optional code style to apply to the markdown text."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the page's HTML component."""
+
type: _t.Literal['Markdown'] = 'Markdown'
+ """The type of the component. Always 'Markdown'."""
class Code(_p.BaseModel, extra='forbid'):
+ """Code component that renders code with syntax highlighting."""
+
text: str
+ """The code to render."""
+
language: _t.Union[str, None] = None
+ """Optional language of the code. If None, no syntax highlighting is applied."""
+
code_style: CodeStyle = None
+ """Optional code style to apply to the code."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the page's HTML component."""
+
type: _t.Literal['Code'] = 'Code'
+ """The type of the component. Always 'Code'."""
class Json(_p.BaseModel, extra='forbid'):
+ """JSON component that renders JSON data."""
+
value: _types.JsonData
+ """The JSON data to render."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the page's HTML component."""
+
type: _t.Literal['JSON'] = 'JSON'
+ """The type of the component. Always 'JSON'."""
class Button(_p.BaseModel, extra='forbid'):
+ """Button component."""
+
text: str
+ """The text to display on the button."""
+
on_click: _t.Union[events.AnyEvent, None] = _p.Field(default=None, serialization_alias='onClick')
+ """Optional event to trigger when the button is clicked."""
+
html_type: _t.Union[_t.Literal['button', 'reset', 'submit'], None] = _p.Field(
default=None, serialization_alias='htmlType'
)
+ """Optional HTML type of the button. If None, defaults to 'button'."""
+
named_style: _class_name.NamedStyleField = None
+ """Optional named style to apply to the button."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the button's HTML component."""
+
type: _t.Literal['Button'] = 'Button'
+ """The type of the component. Always 'Button'."""
class Link(_p.BaseModel, extra='forbid'):
+ """Link component."""
+
components: '_t.List[AnyComponent]'
+ """List of components to render attached to the link."""
+
on_click: _t.Union[events.AnyEvent, None] = _p.Field(default=None, serialization_alias='onClick')
+ """Optional event to trigger when the link is clicked."""
+
mode: _t.Union[_t.Literal['navbar', 'footer', 'tabs', 'vertical', 'pagination'], None] = None
+ """Optional mode of the link."""
+
active: _t.Union[str, bool, None] = None
+ """Optional active state of the link."""
+
locked: _t.Union[bool, None] = None
+ """Optional locked state of the link."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the link's HTML component."""
+
type: _t.Literal['Link'] = 'Link'
+ """The type of the component. Always 'Link'."""
class LinkList(_p.BaseModel, extra='forbid'):
+ """List of Link components."""
+
links: _t.List[Link]
+ """List of links to render."""
+
mode: _t.Union[_t.Literal['tabs', 'vertical', 'pagination'], None] = None
+ """Optional mode of the link list."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the link list's HTML component."""
+
type: _t.Literal['LinkList'] = 'LinkList'
+ """The type of the component. Always 'LinkList'."""
class Navbar(_p.BaseModel, extra='forbid'):
+ """Navbar component used for moving between pages."""
+
title: _t.Union[str, None] = None
+ """Optional title to display in the navbar."""
+
title_event: _t.Union[events.AnyEvent, None] = _p.Field(default=None, serialization_alias='titleEvent')
+ """Optional event to trigger when the title is clicked. Often used to navigate to the home page."""
+
start_links: _t.List[Link] = _p.Field(default=[], serialization_alias='startLinks')
+ """List of links to render at the start of the navbar."""
+
end_links: _t.List[Link] = _p.Field(default=[], serialization_alias='endLinks')
+ """List of links to render at the end of the navbar."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the navbar's HTML component."""
+
type: _t.Literal['Navbar'] = 'Navbar'
+ """The type of the component. Always 'Navbar'."""
@classmethod
def __get_pydantic_json_schema__(
@@ -196,41 +314,86 @@ def __get_pydantic_json_schema__(
class Footer(_p.BaseModel, extra='forbid'):
+ """Footer component."""
+
links: _t.List[Link]
+ """List of links to render in the footer."""
+
extra_text: _t.Union[str, None] = _p.Field(default=None, serialization_alias='extraText')
+ """Optional extra text to display in the footer."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the footer's HTML component."""
+
type: _t.Literal['Footer'] = 'Footer'
+ """The type of the component. Always 'Footer'."""
class Modal(_p.BaseModel, extra='forbid'):
+ """Modal component that displays a modal dialog."""
+
title: str
+ """The text displayed on the modal trigger button."""
+
body: '_t.List[AnyComponent]'
+ """List of components to render in the modal body."""
+
footer: '_t.Union[_t.List[AnyComponent], None]' = None
+ """Optional list of components to render in the modal footer."""
+
open_trigger: _t.Union[events.PageEvent, None] = _p.Field(default=None, serialization_alias='openTrigger')
+ """Optional event to trigger when the modal is opened."""
+
open_context: _t.Union[events.ContextType, None] = _p.Field(default=None, serialization_alias='openContext')
+ """Optional context to pass to the open trigger event."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the modal's HTML component."""
+
type: _t.Literal['Modal'] = 'Modal'
+ """The type of the component. Always 'Modal'."""
class ServerLoad(_p.BaseModel, extra='forbid'):
- """
- A component that will be replaced by the server with the component returned by the given URL.
- """
+ """A component that will be replaced by the server with the component returned by the given URL."""
path: str
+ """The URL to load the component from."""
+
load_trigger: _t.Union[events.PageEvent, None] = _p.Field(default=None, serialization_alias='loadTrigger')
+ """Optional event to trigger when the component is loaded."""
+
components: '_t.Union[_t.List[AnyComponent], None]' = None
+ """Optional list of components to render while the server is loading the new component(s)."""
+
sse: _t.Union[bool, None] = None
+ """Optional flag to enable server-sent events (SSE) for the server load."""
+
sse_retry: _t.Union[int, None] = _p.Field(default=None, serialization_alias='sseRetry')
+ """Optional time in milliseconds to retry the SSE connection."""
+
method: _t.Union[_t.Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'], None] = None
+ """Optional HTTP method to use when loading the component."""
+
type: _t.Literal['ServerLoad'] = 'ServerLoad'
+ """The type of the component. Always 'ServerLoad'."""
class Image(_p.BaseModel, extra='forbid'):
+ """Image container component."""
+
src: str
+ """The URL of the image to display."""
+
alt: _t.Union[str, None] = None
+ """Optional alt text for the image."""
+
width: _t.Union[str, int, None] = None
+ """Optional width used to display the image."""
+
height: _t.Union[str, int, None] = None
+ """Optional height used to display the image."""
+
referrer_policy: _t.Union[
_t.Literal[
'no-referrer',
@@ -244,48 +407,115 @@ class Image(_p.BaseModel, extra='forbid'):
],
None,
] = _p.Field(None, serialization_alias='referrerPolicy')
+ """Optional referrer policy for the image. Specifies what information to send when fetching the image.
+
+ For more info, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy."""
+
loading: _t.Union[_t.Literal['eager', 'lazy'], None] = None
+ """Optional loading strategy for the image."""
+
on_click: _t.Union[events.AnyEvent, None] = _p.Field(default=None, serialization_alias='onClick')
+ """Optional event to trigger when the image is clicked."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the image's HTML component."""
+
type: _t.Literal['Image'] = 'Image'
+ """The type of the component. Always 'Image'."""
class Iframe(_p.BaseModel, extra='forbid'):
+ """Iframe component that displays content from a URL."""
+
src: _p.HttpUrl
+ """The URL of the content to display."""
+
title: _t.Union[str, None] = None
+ """Optional title for the iframe."""
+
width: _t.Union[str, int, None] = None
+ """Optional width used to display the iframe."""
+
height: _t.Union[str, int, None] = None
+ """Optional height used to display the iframe."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the iframe's HTML component."""
+
srcdoc: _t.Union[str, None] = None
+ """Optional HTML content to display in the iframe."""
+
sandbox: _t.Union[str, None] = None
+ """Optional sandbox policy for the iframe. Specifies restrictions on the HTML content in the iframe."""
+
type: _t.Literal['Iframe'] = 'Iframe'
+ """The type of the component. Always 'Iframe'."""
class Video(_p.BaseModel, extra='forbid'):
+ """Video component that displays a video or multiple videos."""
+
sources: _t.List[_p.AnyUrl]
+ """List of URLs to the video sources."""
+
autoplay: _t.Union[bool, None] = None
+ """Optional flag to enable autoplay for the video."""
+
controls: _t.Union[bool, None] = None
+ """Optional flag to enable controls (pause, play, etc) for the video."""
+
loop: _t.Union[bool, None] = None
+ """Optional flag to enable looping for the video."""
+
muted: _t.Union[bool, None] = None
+ """Optional flag to mute the video."""
+
poster: _t.Union[_p.AnyUrl, None] = None
+ """Optional URL to an image to display as the video poster (what is shown when the video is loading or until the user plays it)."""
+
width: _t.Union[str, int, None] = None
+ """Optional width used to display the video."""
+
height: _t.Union[str, int, None] = None
- type: _t.Literal['Video'] = 'Video'
+ """Optional height used to display the video."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the video's HTML component."""
+
+ type: _t.Literal['Video'] = 'Video'
+ """The type of the component. Always 'Video'."""
class FireEvent(_p.BaseModel, extra='forbid'):
+ """Fire an event."""
+
event: events.AnyEvent
- message: _t.Union[str, None] = None # defaults to blank
+ """The event to fire."""
+
+ message: _t.Union[str, None] = None
+ """Optional message to display when the event is fired. Defaults to a blank message."""
+
type: _t.Literal['FireEvent'] = 'FireEvent'
+ """The type of the component. Always 'FireEvent'."""
class Error(_p.BaseModel, extra='forbid'):
+ """Utility component used to display an error."""
+
title: str
+ """The title of the error."""
+
description: str
+ """The description of the error."""
+
status_code: _t.Union[int, None] = _p.Field(None, serialization_alias='statusCode')
+ """Optional status code of the error."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the error's HTML component."""
+
type: _t.Literal['Error'] = 'Error'
+ """The type of the component. Always 'Error'."""
@classmethod
def __get_pydantic_json_schema__(
@@ -298,14 +528,28 @@ def __get_pydantic_json_schema__(
class Spinner(_p.BaseModel, extra='forbid'):
+ """Spinner component that displays a loading spinner."""
+
text: _t.Union[str, None] = None
+ """Optional text to display with the spinner."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the spinner's HTML component."""
+
type: _t.Literal['Spinner'] = 'Spinner'
+ """The type of the component. Always 'Spinner'."""
class Toast(_p.BaseModel, extra='forbid'):
+ """Toast component that displays a toast message (small temporary message)."""
+
title: str
+ """The title of the toast."""
+
body: '_t.List[AnyComponent]'
+ """List of components to render in the toast body."""
+
+ # TODO: change these before the release (top left, center, end, etc). Can be done with the toast bug fix.
position: _t.Union[
_t.Literal[
'top-start',
@@ -320,18 +564,38 @@ class Toast(_p.BaseModel, extra='forbid'):
],
None,
] = None
+ """Optional position of the toast."""
+
open_trigger: _t.Union[events.PageEvent, None] = _p.Field(default=None, serialization_alias='openTrigger')
+ """Optional event to trigger when the toast is opened."""
+
open_context: _t.Union[events.ContextType, None] = _p.Field(default=None, serialization_alias='openContext')
+ """Optional context to pass to the open trigger event."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the toast's HTML component."""
+
type: _t.Literal['Toast'] = 'Toast'
+ """The type of the component. Always 'Toast'."""
class Custom(_p.BaseModel, extra='forbid'):
+ """Custom component that allows for special data to be rendered."""
+
data: _types.JsonData
+ """The data to render in the custom component."""
+
sub_type: str = _p.Field(serialization_alias='subType')
+ """The sub-type of the custom component."""
+
library: _t.Union[str, None] = None
+ """Optional library to use for the custom component."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the custom component's HTML component."""
+
type: _t.Literal['Custom'] = 'Custom'
+ """The type of the component. Always 'Custom'."""
AnyComponent = _te.Annotated[
@@ -370,3 +634,6 @@ class Custom(_p.BaseModel, extra='forbid'):
],
_p.Field(discriminator='type'),
]
+"""Union of all components.
+
+Pydantic discriminator field is set to 'type' to allow for efficient serialization and deserialization of the components."""
diff --git a/src/python-fastui/fastui/components/display.py b/src/python-fastui/fastui/components/display.py
index 6c4ff822..3afd0347 100644
--- a/src/python-fastui/fastui/components/display.py
+++ b/src/python-fastui/fastui/components/display.py
@@ -15,6 +15,8 @@
class DisplayMode(str, enum.Enum):
+ """Display mode for a value."""
+
auto = 'auto' # default, same as None below
plain = 'plain'
datetime = 'datetime'
@@ -27,37 +29,54 @@ class DisplayMode(str, enum.Enum):
class DisplayBase(pydantic.BaseModel, ABC, defer_build=True):
+ """Base class for display components."""
+
mode: _t.Union[DisplayMode, None] = None
+ """Display mode for the value."""
+
title: _t.Union[str, None] = None
+ """Title to display for the value."""
+
on_click: _t.Union[events.AnyEvent, None] = pydantic.Field(default=None, serialization_alias='onClick')
+ """Event to trigger when the value is clicked."""
class DisplayLookup(DisplayBase, extra='forbid'):
- """
- Description of how to display a value looked up from data, either in a table or detail view.
- """
+ """Description of how to display a value looked up from data, either in a table or detail view."""
field: str
- # percentage width - 0 to 100, specific to tables
+ """Field to display."""
+
table_width_percent: _t.Union[_te.Annotated[int, _at.Interval(ge=0, le=100)], None] = pydantic.Field(
default=None, serialization_alias='tableWidthPercent'
)
+ """Percentage width - 0 to 100, specific to tables."""
class Display(DisplayBase, extra='forbid'):
- """
- Description of how to display a value, either in a table or detail view.
- """
+ """Description of how to display a value, either in a table or detail view."""
value: _types.JsonData
+ """Value to display."""
+
type: _t.Literal['Display'] = 'Display'
+ """The type of the component. Always 'Display'."""
class Details(pydantic.BaseModel, extra='forbid'):
+ """Details associated with displaying a data model."""
+
data: pydantic.SerializeAsAny[_types.DataModel]
+ """Data model to display."""
+
fields: _t.Union[_t.List[DisplayLookup], None] = None
+ """Fields to display."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the details component."""
+
type: _t.Literal['Details'] = 'Details'
+ """The type of the component. Always 'Details'."""
@pydantic.model_validator(mode='after')
def _fill_fields(self) -> _te.Self:
diff --git a/src/python-fastui/fastui/components/forms.py b/src/python-fastui/fastui/components/forms.py
index a18a9c4c..d607de12 100644
--- a/src/python-fastui/fastui/components/forms.py
+++ b/src/python-fastui/fastui/components/forms.py
@@ -15,84 +15,186 @@
class BaseFormField(pydantic.BaseModel, ABC, defer_build=True):
+ """Base class for form fields."""
+
name: str
+ """Name of the field."""
+
title: _t.Union[_t.List[str], str]
+ """Title of the field to display. Can be a list of strings for multi-line titles."""
+
required: bool = False
+ """Whether the field is required. Defaults to False."""
+
error: _t.Union[str, None] = None
+ """Error message to display if the field is invalid."""
+
locked: bool = False
+ """Whether the field is locked. Defaults to False."""
+
description: _t.Union[str, None] = None
+ """Description of the field."""
+
display_mode: _t.Union[_t.Literal['default', 'inline'], None] = pydantic.Field(
default=None, serialization_alias='displayMode'
)
+ """Display mode for the field."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the field's HTML component."""
class FormFieldInput(BaseFormField):
+ """Form field for basic input."""
+
html_type: InputHtmlType = pydantic.Field(default='text', serialization_alias='htmlType')
+ """HTML input type for the field."""
+
initial: _t.Union[str, float, None] = None
+ """Initial value for the field."""
+
placeholder: _t.Union[str, None] = None
+ """Placeholder text for the field."""
+
autocomplete: _t.Union[str, None] = None
+ """Autocomplete value for the field."""
+
type: _t.Literal['FormFieldInput'] = 'FormFieldInput'
+ """The type of the component. Always 'FormFieldInput'."""
class FormFieldTextarea(BaseFormField):
+ """Form field for text area input."""
+
rows: _t.Union[int, None] = None
+ """Number of rows for the text area."""
+
cols: _t.Union[int, None] = None
+ """Number of columns for the text area."""
+
initial: _t.Union[str, None] = None
+ """Initial value for the text area."""
+
placeholder: _t.Union[str, None] = None
+ """Placeholder text for the text area."""
+
autocomplete: _t.Union[str, None] = None
+ """Autocomplete value for the text area."""
+
type: _t.Literal['FormFieldTextarea'] = 'FormFieldTextarea'
+ """The type of the component. Always 'FormFieldTextarea'."""
class FormFieldBoolean(BaseFormField):
+ """Form field for boolean input."""
+
initial: _t.Union[bool, None] = None
+ """Initial value for the field."""
+
mode: _t.Literal['checkbox', 'switch'] = 'checkbox'
+ """Mode for the boolean field."""
+
type: _t.Literal['FormFieldBoolean'] = 'FormFieldBoolean'
+ """The type of the component. Always 'FormFieldBoolean'."""
class FormFieldFile(BaseFormField):
+ """Form field for file input."""
+
multiple: _t.Union[bool, None] = None
+ """Whether multiple files can be selected."""
+
accept: _t.Union[str, None] = None
+ """Accepted file types."""
+
type: _t.Literal['FormFieldFile'] = 'FormFieldFile'
+ """The type of the component. Always 'FormFieldFile'."""
class FormFieldSelect(BaseFormField):
+ """Form field for select input."""
+
options: forms.SelectOptions
+ """Options for the select field."""
+
multiple: _t.Union[bool, None] = None
+ """Whether multiple options can be selected."""
+
initial: _t.Union[_t.List[str], str, None] = None
+ """Initial value for the field."""
+
vanilla: _t.Union[bool, None] = None
+ """Whether to use a vanilla (plain) select element."""
+
placeholder: _t.Union[str, None] = None
+ """Placeholder text for the field."""
+
autocomplete: _t.Union[str, None] = None
+ """Autocomplete value for the field."""
+
type: _t.Literal['FormFieldSelect'] = 'FormFieldSelect'
+ """The type of the component. Always 'FormFieldSelect'."""
class FormFieldSelectSearch(BaseFormField):
+ """Form field for searchable select input."""
+
search_url: str = pydantic.Field(serialization_alias='searchUrl')
+ """URL to search for options."""
+
multiple: _t.Union[bool, None] = None
+ """Whether multiple options can be selected."""
+
initial: _t.Union[forms.SelectOption, None] = None
- # time in ms to debounce requests by, defaults to 300ms
+ """Initial value for the field."""
+
debounce: _t.Union[int, None] = None
+ """Time in milliseconds to debounce requests by. Defaults to 300ms."""
+
placeholder: _t.Union[str, None] = None
+ """Placeholder text for the field."""
+
type: _t.Literal['FormFieldSelectSearch'] = 'FormFieldSelectSearch'
+ """The type of the component. Always 'FormFieldSelectSearch'."""
FormField = _t.Union[
FormFieldInput, FormFieldTextarea, FormFieldBoolean, FormFieldFile, FormFieldSelect, FormFieldSelectSearch
]
+"""Union of all form field types."""
class BaseForm(pydantic.BaseModel, ABC, defer_build=True, extra='forbid'):
+ """Base class for forms."""
+
submit_url: str = pydantic.Field(serialization_alias='submitUrl')
+ """URL to submit the form data to."""
+
initial: _t.Union[_t.Dict[str, _types.JsonData], None] = None
+ """Initial values for the form fields, mapping field names to values."""
+
method: _t.Literal['POST', 'GOTO', 'GET'] = 'POST'
+ """HTTP method to use for the form submission."""
+
display_mode: _t.Union[_t.Literal['default', 'page', 'inline'], None] = pydantic.Field(
default=None, serialization_alias='displayMode'
)
+ """Display mode for the form."""
+
submit_on_change: _t.Union[bool, None] = pydantic.Field(default=None, serialization_alias='submitOnChange')
+ """Whether to submit the form on change."""
+
submit_trigger: _t.Union[events.PageEvent, None] = pydantic.Field(default=None, serialization_alias='submitTrigger')
+ """Event to trigger form submission."""
+
loading: '_t.Union[_t.List[AnyComponent], None]' = None
+ """Components to display while the form is submitting."""
+
footer: '_t.Union[_t.List[AnyComponent], None]' = None
+ """Components to display in the form footer."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the form's HTML component."""
@pydantic.model_validator(mode='after')
def default_footer(self) -> _te.Self:
@@ -102,16 +204,26 @@ def default_footer(self) -> _te.Self:
class Form(BaseForm):
+ """Form component."""
+
form_fields: _t.List[FormField] = pydantic.Field(serialization_alias='formFields')
+ """List of form fields."""
+
type: _t.Literal['Form'] = 'Form'
+ """The type of the component. Always 'Form'."""
FormFieldsModel = _t.TypeVar('FormFieldsModel', bound=pydantic.BaseModel)
class ModelForm(BaseForm):
+ """Form component generated from a Pydantic model."""
+
model: _t.Type[pydantic.BaseModel] = pydantic.Field(exclude=True)
+ """Pydantic model from which to generate the form."""
+
type: _t.Literal['ModelForm'] = 'ModelForm'
+ """The type of the component. Always 'ModelForm'."""
@pydantic.computed_field(alias='formFields')
def form_fields(self) -> _t.List[FormField]:
diff --git a/src/python-fastui/fastui/components/tables.py b/src/python-fastui/fastui/components/tables.py
index 28bfd95f..53d35ce7 100644
--- a/src/python-fastui/fastui/components/tables.py
+++ b/src/python-fastui/fastui/components/tables.py
@@ -12,12 +12,25 @@
class Table(pydantic.BaseModel, extra='forbid'):
+ """Table component."""
+
data: _t.Sequence[pydantic.SerializeAsAny[_types.DataModel]]
+ """Sequence of data models to display in the table."""
+
columns: _t.Union[_t.List[display.DisplayLookup], None] = None
+ """List of columns to display in the table. If not provided, columns will be inferred from the data model."""
+
data_model: _t.Union[_t.Type[pydantic.BaseModel], None] = pydantic.Field(default=None, exclude=True)
+ """Data model to use for the table. If not provided, the model will be inferred from the first data item."""
+
no_data_message: _t.Union[str, None] = pydantic.Field(default=None, serialization_alias='noDataMessage')
+ """Message to display when there is no data."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the paragraph's HTML component."""
+
type: _t.Literal['Table'] = 'Table'
+ """The type of the component. Always 'Table'."""
@pydantic.model_validator(mode='after')
def _fill_columns(self) -> _te.Self:
@@ -54,12 +67,25 @@ def __get_pydantic_json_schema__(
class Pagination(pydantic.BaseModel):
+ """Pagination component to use with tables."""
+
page: int
+ """The current page number."""
+
page_size: int = pydantic.Field(serialization_alias='pageSize')
+ """The number of items per page."""
+
total: int
+ """The total number of items."""
+
+ page_query_param: str = pydantic.Field('page', serialization_alias='pageQueryParam')
+ """The query parameter to use for the page number."""
+
class_name: _class_name.ClassNameField = None
+ """Optional class name to apply to the pagination's HTML component."""
+
type: _t.Literal['Pagination'] = 'Pagination'
- page_query_param: str = pydantic.Field('page', serialization_alias='pageQueryParam')
+ """The type of the component. Always 'Pagination'."""
@pydantic.computed_field(alias='pageCount')
def page_count(self) -> int:
diff --git a/src/python-fastui/fastui/types.py b/src/python-fastui/fastui/types.py
index ea573557..4906ee55 100644
--- a/src/python-fastui/fastui/types.py
+++ b/src/python-fastui/fastui/types.py
@@ -5,6 +5,7 @@
from pydantic_core import core_schema
+# TODO: replace with https://docs.pydantic.dev/dev/api/types/#pydantic.types.JsonValue, maybe?
class JsonDataSchema:
@staticmethod
def __get_pydantic_json_schema__(
diff --git a/typedoc.base.json b/typedoc.base.json
new file mode 100644
index 00000000..daa58cb2
--- /dev/null
+++ b/typedoc.base.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://typedoc.org/schema.json",
+ "includeVersion": true
+}
diff --git a/typedoc.json b/typedoc.json
new file mode 100644
index 00000000..5cf9ad29
--- /dev/null
+++ b/typedoc.json
@@ -0,0 +1,5 @@
+{
+ "extends": ["./typedoc.base.json"],
+ "entryPointStrategy": "packages",
+ "entryPoints": ["./src/*"]
+}