├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── actions
│ ├── setup-npm
│ │ └── action.yaml
│ └── setup-python
│ │ └── action.yaml
├── dependabot.yml
└── workflows
│ ├── lint.yaml
│ ├── release.yaml
│ └── test.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .python-version
├── LICENSE
├── Makefile
├── README.md
├── README_CN.md
├── docs
├── .dockerignore
├── .gitignore
├── .python-version
├── Dockerfile
├── README.md
├── docs-ui
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .prettierrc
│ ├── README.md
│ ├── components.json
│ ├── index.html
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── postcss.config.js
│ ├── public
│ │ └── vite.svg
│ ├── src
│ │ ├── App.tsx
│ │ ├── component-resolver.tsx
│ │ ├── components
│ │ │ ├── code-block.tsx
│ │ │ └── markdown.tsx
│ │ ├── globals.css
│ │ ├── lib
│ │ │ └── utils.ts
│ │ ├── main.tsx
│ │ └── vite-env.d.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── pyproject.toml
├── requirements-dev.lock
├── requirements.lock
├── requirements.txt
├── src
│ └── documentation
│ │ ├── __init__.py
│ │ ├── app
│ │ ├── __init__.py
│ │ ├── group__docs
│ │ │ ├── __init__.py
│ │ │ ├── actions
│ │ │ │ ├── __init__.py
│ │ │ │ └── dynamic__action_type
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── page.py
│ │ │ ├── component.py
│ │ │ ├── components
│ │ │ │ ├── __init__.py
│ │ │ │ ├── data_grid
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── route.py
│ │ │ │ ├── deferred_fetch
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── route.py
│ │ │ │ ├── dynamic__component_type
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── page.py
│ │ │ │ └── form
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── route.py
│ │ │ ├── docs
│ │ │ │ ├── __init__.py
│ │ │ │ └── dynamic__content_name
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── page.py
│ │ │ ├── layout.py
│ │ │ └── learn
│ │ │ │ ├── __init__.py
│ │ │ │ └── dynamic__content_name
│ │ │ │ ├── __init__.py
│ │ │ │ └── page.py
│ │ ├── layout.py
│ │ └── page.py
│ │ ├── components.py
│ │ ├── config.py
│ │ ├── content
│ │ ├── __init__.py
│ │ ├── custom-component.md
│ │ ├── form.md
│ │ ├── installation.md
│ │ ├── introduction.md
│ │ ├── project-structure.md
│ │ ├── routing.md
│ │ ├── sitemap.md
│ │ ├── tailwindcss.md
│ │ └── tutorial.md
│ │ └── main.py
└── vercel.json
├── examples
└── todo
│ ├── .gitignore
│ ├── .python-version
│ ├── Makefile
│ ├── README.md
│ ├── pyproject.toml
│ ├── requirements-dev.lock
│ ├── requirements.lock
│ └── src
│ └── todo
│ ├── __init__.py
│ ├── app
│ ├── __init__.py
│ ├── layout.py
│ ├── page.py
│ └── route.py
│ ├── main.py
│ └── storage.py
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── pyproject.toml
├── requirements-dev.lock
├── requirements.lock
├── src
├── npm-flect-prebuilt
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .prettierrc
│ ├── README.md
│ ├── components.json
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ └── vite.svg
│ ├── src
│ │ ├── App.tsx
│ │ ├── globals.css
│ │ ├── lib
│ │ │ └── utils.ts
│ │ ├── main.tsx
│ │ └── vite-env.d.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── npm-flect
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .prettierrc
│ ├── README.md
│ ├── components.json
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── postcss.config.js
│ ├── public
│ │ └── vite.svg
│ ├── src
│ │ ├── application.tsx
│ │ ├── components
│ │ │ ├── action-resolver.tsx
│ │ │ ├── component-resolver.tsx
│ │ │ ├── flect
│ │ │ │ ├── any-component.tsx
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── code-block.tsx
│ │ │ │ ├── container.tsx
│ │ │ │ ├── copy-button.tsx
│ │ │ │ ├── custom.tsx
│ │ │ │ ├── data-grid.tsx
│ │ │ │ ├── deferred-fetch.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── display.tsx
│ │ │ │ ├── form.tsx
│ │ │ │ ├── heading.tsx
│ │ │ │ ├── link.tsx
│ │ │ │ ├── link.types.ts
│ │ │ │ ├── markdown.tsx
│ │ │ │ ├── nav-link.tsx
│ │ │ │ ├── outlet.tsx
│ │ │ │ ├── paragraph.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ └── text.tsx
│ │ │ ├── index.ts
│ │ │ ├── routing.tsx
│ │ │ └── ui
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── command.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── form.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── sonner.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ └── tooltip.tsx
│ │ ├── contexts
│ │ │ ├── action-resolver.tsx
│ │ │ ├── component-resolver.tsx
│ │ │ └── config.tsx
│ │ ├── globals.css
│ │ ├── hooks
│ │ │ ├── use-action.ts
│ │ │ └── use-dispatch-action-listen.ts
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── actions.ts
│ │ │ ├── ajv-resolver.ts
│ │ │ └── utils.ts
│ │ ├── types.d.ts
│ │ └── vite-env.d.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
└── python-flect
│ ├── src
│ └── flect
│ │ ├── __init__.py
│ │ ├── actions.py
│ │ ├── application.py
│ │ ├── component
│ │ ├── __init__.py
│ │ ├── components.py
│ │ ├── data_grid.py
│ │ ├── display.py
│ │ └── form.py
│ │ ├── config.py
│ │ ├── constants.py
│ │ ├── head.py
│ │ ├── py.typed
│ │ ├── render.py
│ │ ├── response.py
│ │ ├── routing
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── server.py
│ │ └── sort.py
│ │ ├── sitemap.py
│ │ ├── types.py
│ │ ├── utils.py
│ │ └── version.py
│ └── tests
│ ├── __init__.py
│ ├── app
│ ├── __init__.py
│ ├── layout.py
│ ├── page.py
│ ├── segment1
│ │ ├── __init__.py
│ │ ├── dynamic__segment_id
│ │ │ ├── __init__.py
│ │ │ ├── page.py
│ │ │ └── route.py
│ │ ├── group__segment2
│ │ │ ├── __init__.py
│ │ │ ├── layout.py
│ │ │ └── segment3
│ │ │ │ ├── __init__.py
│ │ │ │ ├── page.py
│ │ │ │ └── route.py
│ │ ├── layout.py
│ │ ├── page.py
│ │ └── segment2
│ │ │ ├── __init__.py
│ │ │ ├── page.py
│ │ │ └── route.py
│ └── utils.py
│ ├── conftest.py
│ ├── test_head.py
│ ├── test_render.py
│ ├── test_routing
│ ├── __init__.py
│ ├── test_client.py
│ ├── test_server.py
│ └── test_sort.py
│ ├── test_sitemap.py
│ └── test_utils.py
└── tsconfig.json
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 |
28 | - OS: [e.g. iOS]
29 | - Browser [e.g. chrome, safari]
30 | - Version [e.g. 22]
31 |
32 | **Smartphone (please complete the following information):**
33 |
34 | - Device: [e.g. iPhone6]
35 | - OS: [e.g. iOS8.1]
36 | - Browser [e.g. stock browser, safari]
37 | - Version [e.g. 22]
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/actions/setup-npm/action.yaml:
--------------------------------------------------------------------------------
1 | name: 'Setup NPM'
2 | runs:
3 | using: 'composite'
4 | steps:
5 | - uses: pnpm/action-setup@v3
6 | with:
7 | version: 8
8 | - name: Install dependencies
9 | shell: bash
10 | run: pnpm install
11 |
--------------------------------------------------------------------------------
/.github/actions/setup-python/action.yaml:
--------------------------------------------------------------------------------
1 | name: 'Setup Python'
2 | runs:
3 | using: 'composite'
4 | steps:
5 | - uses: actions/setup-python@v5
6 | with:
7 | python-version: '3.11'
8 | - name: Install dependencies
9 | shell: bash
10 | run: curl -sSf https://rye-up.com/get | RYE_NO_AUTO_INSTALL=1 RYE_INSTALL_OPTION="--yes" bash
11 | - name: Add shims
12 | shell: bash
13 | run: echo "$HOME/.rye/shims" >> $GITHUB_PATH
14 | - name: Install
15 | shell: bash
16 | run: make install
17 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Keep GitHub Actions up to date with GitHub's Dependabot...
2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
4 | version: 2
5 | updates:
6 | - package-ecosystem: github-actions
7 | directory: /
8 | groups:
9 | github-actions:
10 | patterns:
11 | - '*' # Group all Actions updates into a single larger pull request
12 | schedule:
13 | interval: monthly
14 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | lint:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: ./.github/actions/setup-python
12 | - uses: ./.github/actions/setup-npm
13 |
14 | - run: pre-commit run --show-diff-on-failure --color=always --all-files
15 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - '[0-9]+.[0-9]+.[0-9]+'
6 | workflow_dispatch:
7 | inputs:
8 | version:
9 | type: string
10 | description: 'Release version'
11 | required: true
12 |
13 | jobs:
14 | release:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: ./.github/actions/setup-python
20 | - uses: ./.github/actions/setup-npm
21 |
22 | - run: pnpm build
23 | - run: rye build
24 | - run: rye publish --token ${{ secrets.PYPI_TOKEN }} -y
25 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | name: test ${{ matrix.python-version }} on ${{ matrix.os }}
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | os: [ubuntu, macos]
12 | python-version: ['3.9', '3.10', '3.11', '3.12']
13 |
14 | runs-on: ${{ matrix.os }}-latest
15 |
16 | env:
17 | PYTHON: ${{ matrix.python-version }}
18 | OS: ${{ matrix.os }}
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - name: set up python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: ${{ matrix.python-version }}
27 |
28 | - run: sed '/-e/d' requirements-dev.lock > requirements.txt
29 | - run: pip install -r requirements.txt
30 | - run: pip install -e .
31 |
32 | - run: export PYTHONPATH=docs/src && coverage run -m pytest
33 | # display coverage and fail if it's below 80%, which shouldn't happen
34 | - run: coverage report --fail-under=80
35 |
36 | - run: coverage xml
37 |
38 | - uses: codecov/codecov-action@v4
39 | with:
40 | file: ./coverage.xml
41 | env_vars: PYTHON,OS
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | wheels/
22 | share/python-wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | *.py,cover
49 | .hypothesis/
50 | .pytest_cache/
51 | cover/
52 |
53 | # Translations
54 | *.mo
55 | *.pot
56 |
57 | # Django stuff:
58 | *.log
59 | local_settings.py
60 | db.sqlite3
61 | db.sqlite3-journal
62 |
63 | # Flask stuff:
64 | instance/
65 | .webassets-cache
66 |
67 | # Scrapy stuff:
68 | .scrapy
69 |
70 | # Sphinx documentation
71 | docs/_build/
72 |
73 | # PyBuilder
74 | .pybuilder/
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | # For a library or package, you might want to ignore these files since the code is
86 | # intended to run in multiple environments; otherwise, check them in:
87 | # .python-version
88 |
89 | # pipenv
90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
93 | # install all needed dependencies.
94 | #Pipfile.lock
95 |
96 | # poetry
97 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
98 | # This is especially recommended for binary packages to ensure reproducibility, and is more
99 | # commonly ignored for libraries.
100 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
101 | #poetry.lock
102 |
103 | # pdm
104 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
105 | #pdm.lock
106 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
107 | # in version control.
108 | # https://pdm-project.org/#use-with-ide
109 | .pdm.toml
110 | .pdm-python
111 | .pdm-build/
112 |
113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
114 | __pypackages__/
115 |
116 | # Celery stuff
117 | celerybeat-schedule
118 | celerybeat.pid
119 |
120 | # SageMath parsed files
121 | *.sage.py
122 |
123 | # Environments
124 | .env
125 | .venv
126 | env/
127 | venv/
128 | ENV/
129 | env.bak/
130 | venv.bak/
131 |
132 | # Spyder project settings
133 | .spyderproject
134 | .spyproject
135 |
136 | # Rope project settings
137 | .ropeproject
138 |
139 | # mkdocs documentation
140 | /site
141 |
142 | # mypy
143 | .mypy_cache/
144 | .dmypy.json
145 | dmypy.json
146 |
147 | # Pyre type checker
148 | .pyre/
149 |
150 | # pytype static type analyzer
151 | .pytype/
152 |
153 | # Cython debug symbols
154 | cython_debug/
155 |
156 | # PyCharm
157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
159 | # and can be added to the global gitignore or merged into this file. For a more nuclear
160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
161 | .idea/
162 | .ruff_cache/
163 |
164 | .DS_Store
165 |
166 | node_modules/
167 | .vercel
168 | static/
169 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.3.0
4 | hooks:
5 | - id: check-yaml
6 | - id: check-toml
7 | - id: end-of-file-fixer
8 | - id: trailing-whitespace
9 |
10 | - repo: local
11 | hooks:
12 | - id: python-format
13 | name: python-format
14 | types_or: [python]
15 | entry: make format
16 | language: system
17 | pass_filenames: false
18 |
19 | - id: python-typecheck
20 | name: python-typecheck
21 | types_or: [python]
22 | entry: make typecheck
23 | language: system
24 | pass_filenames: false
25 |
26 | - id: js-prettier
27 | name: js-prettier
28 | types_or: [javascript, jsx, ts, tsx, css, json, markdown]
29 | entry: npm run prettier
30 | language: system
31 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.9
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 chaoying
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | path = src/python-flect/src/flect/
2 | docs_path = docs/src
3 |
4 | .PHONY: install
5 | install:
6 | rye sync
7 | rye install pre-commit
8 | pre-commit install
9 |
10 | .PHONY: format
11 | format:
12 | rye run ruff check --fix-only $(path) docs src/python-flect/
13 | rye run ruff format $(path) docs src/python-flect/
14 |
15 | .PHONY: lint
16 | lint:
17 | rye run ruff check $(path) docs src/python-flect/
18 | rye run ruff format --check $(path) docs src/python-flect/
19 |
20 | .PHONY: typecheck
21 | typecheck:
22 | rye run pyright
23 |
24 |
25 | .PHONY: test
26 | test:
27 | export PYTHONPATH=$(docs_path) && rye run coverage run -m pytest
28 |
29 |
30 | .PHONY: testcov
31 | testcov: test
32 | coverage html
33 |
34 |
35 | .PHONY: build
36 | build:
37 | npm run build
38 |
39 | .PHONY: dev
40 | dev:
41 | export PYTHONPATH=$(docs_path) && uvicorn docs.src.documentation.main:app --reload --reload-dir .
42 |
--------------------------------------------------------------------------------
/docs/.dockerignore:
--------------------------------------------------------------------------------
1 | fly.toml
2 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 | fly.toml
3 |
--------------------------------------------------------------------------------
/docs/.python-version:
--------------------------------------------------------------------------------
1 | 3.11.1
2 |
--------------------------------------------------------------------------------
/docs/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-alpine
2 |
3 | WORKDIR /app
4 |
5 | ENV PYTHONPATH=src
6 |
7 | # Install dependencies
8 | COPY ./requirements.lock .
9 | RUN sed '/-e/d' requirements.lock > requirements.txt
10 | RUN pip install -r requirements.txt
11 |
12 | # Copy source
13 | COPY . .
14 |
15 | CMD uvicorn src.documentation.main:app --host 0.0.0.0 --port 8080
16 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # flect docs
2 |
3 | ## Deploy Your Own
4 |
5 | [](https://vercel.com/new/clone?repository-url=https://github.com/Chaoyingz/flect/tree/main/docs)
6 |
--------------------------------------------------------------------------------
/docs/docs-ui/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:react-hooks/recommended",
8 | ],
9 | ignorePatterns: ["dist", ".eslintrc.cjs"],
10 | parser: "@typescript-eslint/parser",
11 | plugins: ["react-refresh"],
12 | rules: {
13 | "react-refresh/only-export-components": [
14 | "warn",
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/docs/docs-ui/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/docs/docs-ui/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-tailwindcss"]
3 | }
4 |
--------------------------------------------------------------------------------
/docs/docs-ui/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default {
18 | // other rules...
19 | parserOptions: {
20 | ecmaVersion: "latest",
21 | sourceType: "module",
22 | project: ["./tsconfig.json", "./tsconfig.node.json"],
23 | tsconfigRootDir: __dirname,
24 | },
25 | };
26 | ```
27 |
28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
31 |
--------------------------------------------------------------------------------
/docs/docs-ui/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/docs/docs-ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/docs-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs-ui",
3 | "description": "Your project description",
4 | "version": "0.1.10",
5 | "main": "dist/index.html",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "tsc && vite build",
10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "class-variance-authority": "^0.7.0",
15 | "clsx": "^2.1.0",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "react-markdown": "^9.0.1",
19 | "react-syntax-highlighter": "npm:@fengkx/react-syntax-highlighter@15.6.1",
20 | "remark-gfm": "^4.0.0",
21 | "tailwind-merge": "^2.2.1",
22 | "tailwindcss-animate": "^1.0.7",
23 | "@chaoying/flect": "workspace:*"
24 | },
25 | "devDependencies": {
26 | "@types/react": "^18.2.64",
27 | "@types/react-dom": "^18.2.21",
28 | "@types/react-syntax-highlighter": "^15.5.11",
29 | "@typescript-eslint/eslint-plugin": "^7.1.1",
30 | "@typescript-eslint/parser": "^7.1.1",
31 | "@vitejs/plugin-react": "^4.2.1",
32 | "autoprefixer": "^10.4.18",
33 | "eslint": "^8.57.0",
34 | "eslint-plugin-react-hooks": "^4.6.0",
35 | "eslint-plugin-react-refresh": "^0.4.5",
36 | "postcss": "^8.4.35",
37 | "prettier": "^3.2.5",
38 | "prettier-plugin-tailwindcss": "^0.5.12",
39 | "tailwindcss": "^3.4.1",
40 | "typescript": "^5.2.2",
41 | "vite": "^5.1.6"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docs/docs-ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/docs/docs-ui/public/vite.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/docs-ui/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Flect,
3 | ActionResolverProvider,
4 | ComponentResolverProvider,
5 | getMetaContent,
6 | } from "@chaoying/flect";
7 | import {
8 | FlectActionResolver,
9 | FlectComponentResolver,
10 | } from "@chaoying/flect/components";
11 | import { DocsUIComponentResolver } from "@/component-resolver";
12 |
13 | function App() {
14 | return (
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default App;
31 |
--------------------------------------------------------------------------------
/docs/docs-ui/src/component-resolver.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentResolver } from "@chaoying/flect";
2 | import { Markdown, MarkdownProps } from "@/components/markdown.tsx";
3 |
4 | type AnyComponentProps = MarkdownProps;
5 |
6 | export const DocsUIComponentResolver: ComponentResolver = (
7 | props: AnyComponentProps,
8 | ) => {
9 | switch (props.subType) {
10 | case "markdown":
11 | return ;
12 | default:
13 | return null;
14 | }
15 | };
16 | DocsUIComponentResolver.package = "docs-ui";
17 |
--------------------------------------------------------------------------------
/docs/docs-ui/src/components/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
2 | import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
3 | import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx";
4 | import dracula from "react-syntax-highlighter/dist/esm/styles/prism/dracula";
5 | import { CopyButton } from "@chaoying/flect/components";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | SyntaxHighlighter.registerLanguage("python", python);
10 | SyntaxHighlighter.registerLanguage("tsx", tsx);
11 |
12 | export type CodeBlockProps = {
13 | package: "docs-ui";
14 | type: "code-block";
15 | subType: "code-block";
16 | className?: string;
17 | text: string;
18 | language?: string;
19 | };
20 |
21 | export function CodeBlock(props: CodeBlockProps) {
22 | return (
23 |
24 |
30 |
37 | {props.text}
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/docs/docs-ui/src/components/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import ReactMarkdown, { Components } from "react-markdown";
3 | import remarkGfm from "remark-gfm";
4 | import { CodeBlock } from "@/components/code-block";
5 |
6 | export interface MarkdownProps {
7 | package: "docs-ui";
8 | type: "markdown";
9 | subType: "markdown";
10 | className?: string;
11 | text: string;
12 | }
13 |
14 | export function Markdown(props: MarkdownProps) {
15 | const { text, className } = props;
16 | const components: Components = {
17 | code({ children, className }) {
18 | const language = /language-(\w+)/.exec(className || "");
19 | if (!language) {
20 | return {children}
;
21 | }
22 | return (
23 |
30 | );
31 | },
32 | };
33 |
34 | return (
35 |
43 | {text}
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/docs/docs-ui/src/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/docs/docs-ui/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/docs/docs-ui/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.tsx";
4 | import "@/globals.css";
5 |
6 | ReactDOM.createRoot(document.getElementById("root")!).render(
7 |
8 |
9 | ,
10 | );
11 |
--------------------------------------------------------------------------------
/docs/docs-ui/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/docs/docs-ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{ts,tsx}",
6 | "./components/**/*.{ts,tsx}",
7 | "./app/**/*.{ts,tsx}",
8 | "./src/**/*.{ts,tsx}",
9 | ],
10 | // prefix: 'fp',
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "2rem",
15 | screens: {
16 | "2xl": "1400px",
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: "0" },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: "0" },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | },
75 | },
76 | safelist: [
77 | {
78 | pattern: /^(w|max-w|h|max-h)-/,
79 | },
80 | {
81 | pattern: /^(flex|grid)-/,
82 | },
83 | {
84 | pattern: /^(m|p)\w?-\d/,
85 | },
86 | {
87 | pattern: /^gap-\d/,
88 | },
89 | {
90 | pattern: /^text-(\w{2}|\d{1}\w{2})$/,
91 | },
92 | {
93 | pattern: /^(font|justify)-/,
94 | },
95 | "border-b",
96 | "text-center",
97 | "invisible",
98 | "absolute",
99 | "overflow-hidden",
100 | "underline",
101 | "min-h-screen",
102 | ],
103 | corePlugins: {
104 | preflight: false,
105 | },
106 | plugins: [require("tailwindcss-animate")],
107 | };
108 |
--------------------------------------------------------------------------------
/docs/docs-ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | }
8 | },
9 | "include": ["src"],
10 | "references": [{ "path": "./tsconfig.node.json" }]
11 | }
12 |
--------------------------------------------------------------------------------
/docs/docs-ui/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/docs/docs-ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig, splitVendorChunkPlugin } from "vite";
3 | import react from "@vitejs/plugin-react";
4 |
5 | const serverConfig = {
6 | host: true,
7 | port: 3000,
8 | proxy: {
9 | "/flect": "http://localhost:8000/",
10 | },
11 | };
12 |
13 | export default defineConfig({
14 | plugins: [react(), splitVendorChunkPlugin()],
15 | resolve: {
16 | alias: {
17 | "@": path.resolve(__dirname, "./src"),
18 | },
19 | },
20 | server: serverConfig,
21 | preview: serverConfig,
22 | build: {
23 | rollupOptions: {
24 | output: {
25 | entryFileNames: `assets/[name].js`,
26 | chunkFileNames: `assets/[name].js`,
27 | assetFileNames: `assets/[name].[ext]`,
28 | },
29 | plugins: [],
30 | },
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/docs/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "docs"
3 | version = "0.1.0"
4 | description = "flect docs"
5 | authors = [
6 | { name = "Chaoying", email = "chaunceywe@gmail.com" }
7 | ]
8 | dependencies = [
9 | "flect>=0.2.10",
10 | "pydantic-settings>=2.2.1",
11 | ]
12 | readme = "README.md"
13 | requires-python = ">= 3.9"
14 |
15 | [build-system]
16 | requires = ["hatchling"]
17 | build-backend = "hatchling.build"
18 |
19 | [tool.rye]
20 | managed = true
21 | dev-dependencies = []
22 |
23 | [tool.hatch.metadata]
24 | allow-direct-references = true
25 |
26 | [tool.hatch.build.targets.wheel]
27 | packages = ["src/docs"]
28 |
--------------------------------------------------------------------------------
/docs/requirements-dev.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 |
10 | -e file:.
11 | annotated-types==0.6.0
12 | # via pydantic
13 | anyio==4.3.0
14 | # via starlette
15 | click==8.1.7
16 | # via uvicorn
17 | fastapi==0.110.0
18 | # via flect
19 | flect==0.2.10
20 | # via docs
21 | h11==0.14.0
22 | # via uvicorn
23 | idna==3.6
24 | # via anyio
25 | pydantic==2.6.4
26 | # via fastapi
27 | # via flect
28 | # via pydantic-settings
29 | pydantic-core==2.16.3
30 | # via pydantic
31 | pydantic-settings==2.2.1
32 | # via docs
33 | pyromark==0.3.0
34 | # via flect
35 | python-dotenv==1.0.1
36 | # via pydantic-settings
37 | sniffio==1.3.1
38 | # via anyio
39 | starlette==0.36.3
40 | # via fastapi
41 | typing-extensions==4.10.0
42 | # via fastapi
43 | # via pydantic
44 | # via pydantic-core
45 | uvicorn==0.29.0
46 | # via flect
47 |
--------------------------------------------------------------------------------
/docs/requirements.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 |
10 | -e file:.
11 | annotated-types==0.6.0
12 | # via pydantic
13 | anyio==4.3.0
14 | # via starlette
15 | click==8.1.7
16 | # via uvicorn
17 | fastapi==0.110.0
18 | # via flect
19 | flect==0.2.10
20 | # via docs
21 | h11==0.14.0
22 | # via uvicorn
23 | idna==3.6
24 | # via anyio
25 | pydantic==2.6.4
26 | # via fastapi
27 | # via flect
28 | # via pydantic-settings
29 | pydantic-core==2.16.3
30 | # via pydantic
31 | pydantic-settings==2.2.1
32 | # via docs
33 | pyromark==0.3.0
34 | # via flect
35 | python-dotenv==1.0.1
36 | # via pydantic-settings
37 | sniffio==1.3.1
38 | # via anyio
39 | starlette==0.36.3
40 | # via fastapi
41 | typing-extensions==4.10.0
42 | # via fastapi
43 | # via pydantic
44 | # via pydantic-core
45 | uvicorn==0.29.0
46 | # via flect
47 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | flect
2 | pydantic_settings
3 |
--------------------------------------------------------------------------------
/docs/src/documentation/__init__.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 | CONTENT_DIR = pathlib.Path(__file__).parent / "content"
4 |
--------------------------------------------------------------------------------
/docs/src/documentation/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/actions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/actions/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/actions/dynamic__action_type/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/actions/dynamic__action_type/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/component.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from flect import components as c
4 | from pydantic import BaseModel
5 | from pydantic_core import PydanticUndefined
6 |
7 |
8 | class APIReference(BaseModel):
9 | prop: str
10 | type: str
11 | default: Optional[str]
12 | description: Optional[str]
13 |
14 |
15 | def get_api_reference_section(component: c.AnyComponent) -> c.Container:
16 | props = []
17 | for field, filed_info in component.model_fields.items():
18 | if field in ["type", "type"]:
19 | continue
20 | if field == "children":
21 | filed_info.annotation = "flect.components.AnyComponents"
22 | filed_info.default = "[]"
23 | filed_info.description = "The children of the component."
24 | if filed_info.default == PydanticUndefined:
25 | filed_info.default = "None"
26 | filed_info.default = str(filed_info.default)
27 | props.append(
28 | APIReference(
29 | prop=field,
30 | type=str(filed_info.annotation),
31 | default=filed_info.default,
32 | description=filed_info.description,
33 | )
34 | )
35 | return c.Container(
36 | tag="section",
37 | children=[
38 | c.Heading(
39 | level=2,
40 | text="API Reference",
41 | class_name="text-2xl mb-6 border-b pb-2",
42 | ),
43 | c.Table(model=APIReference, datasets=props),
44 | ],
45 | )
46 |
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/components/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/components/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/components/data_grid/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/components/data_grid/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/components/data_grid/route.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 |
4 | from fastapi.encoders import jsonable_encoder
5 | from flect.actions import Notify
6 | from flect.response import ActionResponse
7 |
8 | from documentation.app.group__docs.components.dynamic__component_type.page import DataGridExampleModel
9 |
10 |
11 | async def post(form: DataGridExampleModel) -> ActionResponse:
12 | await asyncio.sleep(0.1)
13 | return ActionResponse(
14 | action=Notify(
15 | title="You submitted the following values:",
16 | description=json.dumps(jsonable_encoder(form), indent=2),
17 | )
18 | )
19 |
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/components/deferred_fetch/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/components/deferred_fetch/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/components/deferred_fetch/route.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from flect import components as c
4 | from flect.response import PageResponse
5 |
6 |
7 | async def get() -> PageResponse:
8 | await asyncio.sleep(0.1)
9 | return PageResponse(
10 | body=c.Container(
11 | tag="div",
12 | children=[
13 | c.Heading(
14 | level=1,
15 | text="Hello deferred fetch!",
16 | ),
17 | ],
18 | )
19 | )
20 |
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/components/dynamic__component_type/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/components/dynamic__component_type/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/components/form/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/components/form/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/components/form/route.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 |
4 | from fastapi.encoders import jsonable_encoder
5 | from flect.actions import Notify
6 | from flect.response import ActionResponse
7 |
8 | from documentation.app.group__docs.components.dynamic__component_type.page import FormExampleModel
9 |
10 |
11 | async def post(form: FormExampleModel) -> ActionResponse:
12 | await asyncio.sleep(0.1)
13 | return ActionResponse(
14 | action=Notify(
15 | title="You submitted the following values:",
16 | description=json.dumps(jsonable_encoder(form), indent=2),
17 | )
18 | )
19 |
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/docs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/docs/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/docs/dynamic__content_name/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/docs/dynamic__content_name/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/docs/dynamic__content_name/page.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 |
3 | from fastapi import Path, Request
4 | from flect import PageResponse
5 | from flect import components as c
6 | from flect.constants import ROOT_ROUTE_PREFIX
7 | from flect.head import Head
8 | from flect.sitemap import Sitemap
9 |
10 | from documentation import CONTENT_DIR
11 | from documentation.app.group__docs.layout import get_docs_pager
12 | from documentation.components import Markdown
13 |
14 |
15 | async def sitemap(dynamic_url: str) -> list[Sitemap]:
16 | return [
17 | Sitemap(
18 | url=dynamic_url.format(content_name=content_name),
19 | last_modified=None,
20 | change_frequency=None,
21 | priority=None,
22 | )
23 | for content_name in ["introduction", "installation", "tutorial"]
24 | ]
25 |
26 |
27 | async def page(
28 | request: Request,
29 | content_name: Annotated[str, Path(..., description="The content name to render")],
30 | ) -> PageResponse:
31 | return PageResponse(
32 | head=Head(
33 | title=content_name,
34 | ),
35 | body=c.Container(
36 | tag="div",
37 | children=[
38 | Markdown.from_file(CONTENT_DIR / f"{content_name}.md"),
39 | get_docs_pager(current_link=request.url.path.replace(ROOT_ROUTE_PREFIX, "")),
40 | ],
41 | ),
42 | )
43 |
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/learn/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/learn/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/learn/dynamic__content_name/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/app/group__docs/learn/dynamic__content_name/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/app/group__docs/learn/dynamic__content_name/page.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 |
3 | from fastapi import Path, Request
4 | from flect import PageResponse
5 | from flect import components as c
6 | from flect.constants import ROOT_ROUTE_PREFIX
7 | from flect.head import Head
8 | from flect.sitemap import Sitemap
9 |
10 | from documentation import CONTENT_DIR
11 | from documentation.app.group__docs.layout import get_docs_pager
12 | from documentation.components import Markdown
13 |
14 |
15 | async def sitemap(dynamic_url: str) -> list[Sitemap]:
16 | return [
17 | Sitemap(
18 | url=dynamic_url.format(content_name=content_name),
19 | last_modified=None,
20 | change_frequency=None,
21 | priority=None,
22 | )
23 | for content_name in ["project-structure", "routing", "form", "custom-component", "tailwindcss", "sitemap"]
24 | ]
25 |
26 |
27 | async def page(
28 | request: Request,
29 | content_name: Annotated[str, Path(..., description="The content name to render")],
30 | ) -> PageResponse:
31 | return PageResponse(
32 | head=Head(
33 | title=f"Learn {content_name}",
34 | ),
35 | body=c.Container(
36 | tag="div",
37 | children=[
38 | Markdown.from_file(CONTENT_DIR / f"{content_name}.md"),
39 | get_docs_pager(current_link=request.url.path.replace(ROOT_ROUTE_PREFIX, "")),
40 | ],
41 | ),
42 | )
43 |
--------------------------------------------------------------------------------
/docs/src/documentation/app/page.py:
--------------------------------------------------------------------------------
1 | from flect import PageResponse
2 | from flect import components as c
3 |
4 |
5 | async def page() -> PageResponse:
6 | return PageResponse(
7 | body=c.Container(
8 | tag="section",
9 | class_name="container",
10 | children=[
11 | c.Container(
12 | tag="section",
13 | class_name="flex justify-center py-32 flex-col items-center",
14 | children=[
15 | c.Heading(
16 | level=1,
17 | text="Turning ideas into web app fast",
18 | class_name="text-center text-5xl font-bold",
19 | ),
20 | c.Text(
21 | text="flect enables you to quickly create full-stack web applications using Python.",
22 | class_name="text-center text-xl mt-8 w-1/2",
23 | ),
24 | c.Container(
25 | tag="div",
26 | class_name="flex gap-4 mt-8",
27 | children=[
28 | c.Link(
29 | href="/docs/introduction/",
30 | children=[
31 | c.Button(
32 | children=[c.Text(text="Get Started")],
33 | )
34 | ],
35 | ),
36 | c.Link(
37 | href="https://github.com/Chaoyingz/flect",
38 | children=[
39 | c.Button(
40 | children=[c.Text(text="GitHub")],
41 | variant="outline",
42 | )
43 | ],
44 | ),
45 | ],
46 | ),
47 | ],
48 | )
49 | ],
50 | )
51 | )
52 |
--------------------------------------------------------------------------------
/docs/src/documentation/components.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | from typing import Literal, Optional
3 |
4 | import pyromark
5 | from flect import components as c
6 | from typing_extensions import Self
7 |
8 |
9 | class BaseUIComponent(c.Custom):
10 | package: Literal["docs-ui"] = "docs-ui"
11 | sub_type: Literal["badge"] = "markdown"
12 |
13 |
14 | class Markdown(BaseUIComponent):
15 | class_name: Optional[str] = None
16 | text: str
17 |
18 | def render_to_html(self) -> str:
19 | return f"""\
20 | {pyromark.markdown(self.text)}
21 | """
22 |
23 | @classmethod
24 | def from_file(cls, path: pathlib.Path, class_name: Optional[str] = None) -> Self:
25 | try:
26 | return cls(text=path.read_text(), class_name=class_name)
27 | except FileNotFoundError:
28 | return cls(text="Markdown file not found.", class_name=class_name)
29 |
--------------------------------------------------------------------------------
/docs/src/documentation/config.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 |
3 | try:
4 | from pydantic_settings import BaseSettings
5 | except ImportError:
6 | from pydantic import BaseModel as BaseSettings
7 |
8 |
9 | class Settings(BaseSettings):
10 | debug: bool = False
11 | google_measurement_id: str = ""
12 |
13 |
14 | @lru_cache
15 | def get_settings():
16 | return Settings()
17 |
18 |
19 | settings = get_settings()
20 |
--------------------------------------------------------------------------------
/docs/src/documentation/content/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/docs/src/documentation/content/__init__.py
--------------------------------------------------------------------------------
/docs/src/documentation/content/form.md:
--------------------------------------------------------------------------------
1 | # Form
2 |
3 | The flect framework allows you to create forms with ease.
4 |
5 | ## Defining Your Form Model
6 |
7 | To define your form model, you can use Python's `Annotated` and `pydantic`'s `BaseModel` and `Field`. Here's an example:
8 |
9 | ```python
10 | from pydantic import BaseModel
11 | from flect.form import Input
12 |
13 | class FormExampleModel(BaseModel):
14 | # Use Annotated to define the field
15 | name: str = Input(placeholder="Enter your username", pattern=r"^[a-zA-Z0-9]+$", min_items=2, max_items=10)
16 | ```
17 |
18 | In this example, we define a `name` field with a placeholder text "Enter your username". The field accepts alphanumeric characters with a minimum length of 2 and a maximum length of 10.
19 |
20 | ## Using the Form Model
21 |
22 | Once you've defined your form model, you can use it in your form like so:
23 |
24 | ```python
25 | from flect import components as c
26 | from flect import PageResponse
27 |
28 | def page() -> PageResponse:
29 | return PageResponse(
30 | body=c.Container(
31 | tag="div",
32 | children=[
33 | c.Form(
34 | model=FormExampleModel,
35 | submit_url="/"
36 | )
37 | ]
38 | )
39 | )
40 | ```
41 |
42 | In this example, we create a `PageResponse` that contains a `Container` with a single child, a `Form` that uses the `FormExampleModel` we defined earlier. The form's `submit_url` is set to the root ("/").
43 |
44 | ## Adding Server Logic
45 |
46 | After defining the form and its model, you can add server logic to handle the form submission. Here's a simple example:
47 |
48 | ```python
49 | from flect.response import ActionResponse
50 |
51 | async def post(form: FormExampleModel) -> ActionResponse:
52 | # handle the form
53 | ```
54 |
55 | In this function, we define an asynchronous function `post` that accepts a `FormExampleModel` instance and returns an `ActionResponse`. You can add your form handling logic within this function.
56 |
--------------------------------------------------------------------------------
/docs/src/documentation/content/installation.md:
--------------------------------------------------------------------------------
1 | # Installation Guide
2 |
3 | This guide will help you install the necessary dependencies and assist you in setting up your application.
4 |
5 | ## Requirements
6 |
7 | - Python 3.9 or higher
8 |
9 | ## Basic Installation
10 |
11 | If you prefer to start a project from scratch, you can directly install the flect.
12 |
13 | 1. **Install flect**
14 |
15 | To install the flect, run the following command in your terminal:
16 |
17 | ```console
18 | pip install flect
19 | ```
20 |
21 | ## Installation Using Project Template (Recommended)
22 |
23 | For those who prefer to use a project template to kickstart their application, follow these steps:
24 |
25 | ### Additional Requirements
26 |
27 | - Rye (https://rye-up.com/) for dependency management
28 | - Cookiecutter (https://cookiecutter.readthedocs.io/en/stable/) for project scaffolding
29 |
30 | ### Steps
31 |
32 | 1. **Create Project Structure**
33 |
34 | Use Cookiecutter to create the project structure by running:
35 |
36 | ```console
37 | cookiecutter https://github.com/Chaoyingz/cookiecutter-flect
38 | ```
39 |
40 | 2. **Navigate to Project Directory**
41 |
42 | Change into your project's directory:
43 |
44 | ```console
45 | cd {project_slug}
46 | ```
47 |
48 | 3. **Synchronize Dependencies**
49 |
50 | Use Rye to synchronize your project's dependencies:
51 |
52 | ```console
53 | rye sync
54 | ```
55 |
56 | 4. **Run Your Application**
57 |
58 | Start your application with Uvicorn:
59 |
60 | ```console
61 | make dev
62 | ```
63 |
64 | 5. **Visit Your Application**
65 |
66 | Open your browser and visit http://127.0.0.1:8000 you will see the following page.
67 |
68 | 
69 |
70 | By following these steps, you can either set up your project from scratch or use a pre-defined template to get started quickly.
71 |
--------------------------------------------------------------------------------
/docs/src/documentation/content/introduction.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | Welcome to the flect framework documentation!
4 |
5 | ### What is flect?
6 |
7 | flect is a Python framework for building full-stack web applications. It constructs user interfaces by utilizing Pydantic
8 | models in the backend that correspond to the properties of React components in the frontend. This integration enables
9 | quick development of interactive and beautiful UIs using Python.
10 |
11 | The key features are:
12 |
13 | - **Fast development**: Write your entire app with Python, seamlessly integrating backend logic and frontend UI.
14 | - **Easy Form Validation**: Define a single Pydantic model for seamless and consistent form validation across your app, enhancing development speed and reducing potential errors.
15 | - **Client-Side Routing**: Fast, smooth page transitions without reloads.
16 | - **Folder-Based Routing**: Easy route management through folder structure.
17 | - **SEO Friendly**: Supports server-side rendering for better search engine visibility.
18 |
19 | This project is inspired by the [FastUI](https://github.com/pydantic/FastUI) and [Nextjs](https://nextjs.org/) framework.
20 |
21 | ## Why use flect?
22 |
23 | flect enables developers to harness the combined power of Python and JavaScript ecosystems, facilitating the creation of web applications with efficiency and ease:
24 |
25 | > If you're a Python developer — you can build responsive web applications using React without writing a single line of JavaScript, or touching npm.
26 | >
27 | > If you're a frontend developer — you can concentrate on building magical components that are truly reusable, no copy-pasting components for each view.
28 | >
29 | > For everyone — a true separation of concerns, the backend defines the entire application; while the frontend is free to implement just the user interface.
30 | >
31 | > — _From FastUI_
32 |
33 | ## License
34 |
35 | This project is licensed under the terms of the MIT license.
36 |
--------------------------------------------------------------------------------
/docs/src/documentation/content/project-structure.md:
--------------------------------------------------------------------------------
1 | # flect project structure
2 |
3 | This document provides an overview of the project structure for a flect application, with a focus on routing conventions.
4 |
5 | ## Routing Conventions
6 |
7 | Understanding the routing conventions is crucial for navigating through a flect application. Here are the key components and their functions:
8 |
9 | ### Routing Files
10 |
11 | - `layout.py`: This file defines the User Interface (UI) shared across all routes. It sets the common layout for different pages in the application.
12 |
13 | - `page.py`: This file is responsible for the UI unique to a specific route. Each unique page in the application corresponds to a `page.py` file.
14 |
15 | - `route.py`: This file defines the API endpoint for a specific route.
16 |
17 | ### Nested Routes
18 |
19 | Nested routes allow for a hierarchical structure in the application's navigation:
20 |
21 | - `folder`: Represents a route segment.
22 |
23 | - `folder/folder`: Represents a nested route segment. It's a sub-route within a main route segment.
24 |
25 | ### Dynamic Routes
26 |
27 | Dynamic routes allow for more flexible navigation paths:
28 |
29 | - `dynamic__{folder}`: Represents a dynamic route segment. The route can change based on certain conditions or parameters.
30 |
31 | ### Route Groups
32 |
33 | Route groups help organize related routes without affecting the actual routing:
34 |
35 | - `group__{folder}`: Represents a group of related routes. This does not affect the routing but helps in organizing similar routes together.
36 |
--------------------------------------------------------------------------------
/docs/src/documentation/content/routing.md:
--------------------------------------------------------------------------------
1 | # Routing
2 |
3 | Routing serves as the cornerstone of every application. This guide will delve into the crucial aspects of web routing and the best practices for managing routes in a Flex application.
4 |
5 | ## Route Segments
6 |
7 | Route segments are represented by folders within a route, each corresponding to a specific segment of a URL path.
8 |
9 | Consider this folder hierarchy:
10 |
11 | ```console
12 | app
13 | ├── layout.py
14 | ├── page.py
15 | └── dashboard
16 | ├── page.py
17 | └── users
18 | └── page.py
19 | ```
20 |
21 | In this layout, `dashboard` and `users` function as route segments. To access the `users` page within `dashboard`, the URL path would be `/dashboard/users`.
22 |
23 | Each `page.py` file is associated with a distinct URL path, determined by its position in the directory structure:
24 |
25 | - `app/page.py` corresponds to the root URL path (`/`), displaying the default page when visiting the application's base URL.
26 | - `app/dashboard/page.py` is linked to `/dashboard/`, showing the relevant page upon navigation to this path.
27 | - `app/dashboard/users/page.py` maps to `/dashboard/users/`, presenting the appropriate page for this path.
28 |
29 | This setup allows each `page.py` file to represent a unique route, with the folder structure directly reflecting the routing architecture of the application. It simplifies the management and comprehension of your application's routes, making it intuitive and straightforward.
30 |
31 | ## Defining Routing Files
32 |
33 | ### layout.py
34 |
35 | A layout defines a user interface (UI) shared across multiple routes, maintaining its state and interactivity across navigation without needing to re-render. Layouts can also be nested.
36 |
37 | To define a layout:
38 |
39 | ```python
40 | from flect import PageResponse
41 | from flect import components as c
42 |
43 | # The layout function should be named "layout" and take an "outlet" parameter
44 | async def layout(outlet: c.AnyComponent = c.Outlet()) -> PageResponse:
45 | return PageResponse(
46 | body=c.Container(
47 | tag="div",
48 | class_name="flex",
49 | children=[outlet], # the outlet renders the layout's content
50 | )
51 | )
52 | ```
53 |
54 | ### page.py
55 |
56 | A page represents a UI specific to a route, with each distinct page corresponding to a separate `page.py` file.
57 |
58 | To define a page:
59 |
60 | ```python
61 | from flect import PageResponse
62 | from flect import components as c
63 |
64 | async def page() -> PageResponse:
65 | return PageResponse(
66 | body=c.Container(
67 | tag="div",
68 | )
69 | )
70 | ```
71 |
72 | ### routing.py
73 |
74 | The `routing.py` file outlines your application's routes, often used for handling forms. For instance, you can define a POST endpoint to process form submissions as follows:
75 |
76 | ```python
77 | from pydantic import BaseModel
78 | from flect.actions import Notify
79 | from flect.form import Input
80 | from flect.response import ActionResponse
81 |
82 | class FormExampleModel(BaseModel):
83 | username: str = Input(
84 | placeholder="Enter your username", default="", pattern=r"^[a-zA-Z0-9]+$", min_items=2, max_items=10
85 | )
86 |
87 | async def post(form: FormExampleModel) -> ActionResponse:
88 | return ActionResponse(
89 | action=Notify(
90 | title=f"You submitted username: {form.username}",
91 | )
92 | )
93 | ```
94 |
95 | Here, the route name corresponds to the method name, such as `post` for processing POST requests.
96 |
97 | This approach offers a clear and scalable framework for managing your application's routing.
98 |
--------------------------------------------------------------------------------
/docs/src/documentation/content/sitemap.md:
--------------------------------------------------------------------------------
1 | # Sitemap Integration in flect
2 |
3 | Sitemaps play a crucial role in optimizing website visibility for search engines by providing a roadmap of all accessible paths within your application.
4 |
5 | By default, flect automatically parses all the paths in your app to generate a sitemap. For dynamic routes, you have the flexibility to define a function within your page files that generates links for inclusion in the sitemap.
6 |
7 | Here's how you can define a sitemap function for dynamic routes in Python:
8 |
9 | ```python
10 | from flect.sitemap import Sitemap
11 |
12 | async def sitemap(dynamic_url: str) -> list[Sitemap]:
13 | return [
14 | Sitemap(
15 | url=dynamic_url.format(slug_name=slug_name),
16 | last_modified=None,
17 | change_frequency=None,
18 | priority=None,
19 | )
20 | for slug_name in ["x", "y", "z"]
21 | ]
22 | ```
23 |
24 | In this example, `slug_name` represents the variable portion of the dynamic route, and `["x", "y", "z"]` illustrates all possible values for `slug_name`. This approach allows you to easily include dynamic content in your sitemap, ensuring that search engines can discover and index these pages efficiently.
25 |
--------------------------------------------------------------------------------
/docs/src/documentation/content/tailwindcss.md:
--------------------------------------------------------------------------------
1 | # TailwindCSS in flect
2 |
3 | flect enables the use of [TailwindCSS](https://tailwindcss.com/) within Python to define styles. This integration is facilitated through TailwindCSS's [safelist](https://tailwindcss.com/docs/content-configuration#safelisting-classes) feature, allowing you to utilize a variety of TailwindCSS class names directly in Python.
4 |
5 | Below is a list of TailwindCSS class names you can use in Python, supported by the safelist configuration:
6 |
7 | ```js
8 | safelist: [
9 | 'sr-only',
10 | {
11 | pattern: /^(w|max-w|h|max-h)-/,
12 | },
13 | {
14 | pattern: /^(flex|grid)-/,
15 | },
16 | {
17 | pattern: /^(m|p)\w?-\d/,
18 | },
19 | {
20 | pattern: /^gap-\d/,
21 | },
22 | {
23 | pattern: /^text-(\w{2}|\d{1}\w{2})$/,
24 | },
25 | {
26 | pattern: /^(font|justify)-/,
27 | },
28 | 'border-b',
29 | 'text-center',
30 | 'invisible',
31 | 'absolute',
32 | 'overflow-hidden',
33 | 'underline',
34 | 'min-h-screen',
35 | ]
36 | ```
37 |
38 | This configuration covers a wide range of utility classes for spacing, sizing, typography, flexbox, grid, visibility, positioning, overflow control, text decoration, and more. Additionally, you have the option to modify or extend this list through custom component definitions, providing a flexible way to tailor TailwindCSS styling within your flect applications.
39 |
--------------------------------------------------------------------------------
/docs/src/documentation/main.py:
--------------------------------------------------------------------------------
1 | from flect import flect
2 |
3 | from documentation import app as document_app
4 | from documentation.config import settings
5 |
6 | # PREBUILT_URI = str(pathlib.Path(__file__).parent.parent.parent / "docs-ui" / "dist" / "assets")
7 | PREBUILT_URI = "https://unpkg.com/docs-ui@0.1.10/dist/assets"
8 |
9 |
10 | app = flect(
11 | document_app,
12 | docs_url="/documentation",
13 | debug=settings.debug,
14 | prebuilt_uri=PREBUILT_URI,
15 | )
16 |
--------------------------------------------------------------------------------
/docs/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "devCommand": "uvicorn src.documentation.main:app --reload --reload-dir .",
3 | "env": {
4 | "PYTHONPATH": "src"
5 | },
6 | "builds": [
7 | {
8 | "src": "src/documentation/main.py",
9 | "use": "@vercel/python"
10 | }
11 | ],
12 | "routes": [
13 | {
14 | "src": "/(.*)",
15 | "dest": "src/documentation/main.py"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/examples/todo/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 | todos.json
163 |
--------------------------------------------------------------------------------
/examples/todo/.python-version:
--------------------------------------------------------------------------------
1 | 3.9
2 |
--------------------------------------------------------------------------------
/examples/todo/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: dev
2 | dev:
3 | export PYTHONPATH=src && uvicorn src.todo.main:app --reload
4 |
--------------------------------------------------------------------------------
/examples/todo/README.md:
--------------------------------------------------------------------------------
1 | # cookiecutter-flect
2 |
3 | Describe your project here.
4 |
--------------------------------------------------------------------------------
/examples/todo/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "todo"
3 | version = "0.1.0"
4 | description = "todo"
5 | authors = []
6 | dependencies = [
7 | "flect",
8 | ]
9 | readme = "README.md"
10 | requires-python = ">= 3.9"
11 |
12 | [build-system]
13 | requires = ["hatchling"]
14 | build-backend = "hatchling.build"
15 |
16 | [tool.rye]
17 | managed = true
18 | dev-dependencies = []
19 |
20 | [tool.hatch.metadata]
21 | allow-direct-references = true
22 |
23 | [tool.hatch.build.targets.wheel]
24 | packages = ["src/todo"]
25 |
--------------------------------------------------------------------------------
/examples/todo/requirements-dev.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 |
10 | -e file:.
11 | annotated-types==0.6.0
12 | # via pydantic
13 | anyio==4.3.0
14 | # via starlette
15 | click==8.1.7
16 | # via uvicorn
17 | exceptiongroup==1.2.0
18 | # via anyio
19 | fastapi==0.110.0
20 | # via flect
21 | flect==0.1.1
22 | # via todo
23 | h11==0.14.0
24 | # via uvicorn
25 | idna==3.6
26 | # via anyio
27 | pydantic==2.6.3
28 | # via fastapi
29 | # via flect
30 | pydantic-core==2.16.3
31 | # via pydantic
32 | pyromark==0.3.0
33 | # via flect
34 | sniffio==1.3.1
35 | # via anyio
36 | starlette==0.36.3
37 | # via fastapi
38 | typing-extensions==4.10.0
39 | # via anyio
40 | # via fastapi
41 | # via pydantic
42 | # via pydantic-core
43 | # via starlette
44 | # via uvicorn
45 | uvicorn==0.27.1
46 | # via flect
47 |
--------------------------------------------------------------------------------
/examples/todo/requirements.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 |
10 | -e file:.
11 | annotated-types==0.6.0
12 | # via pydantic
13 | anyio==4.3.0
14 | # via starlette
15 | click==8.1.7
16 | # via uvicorn
17 | exceptiongroup==1.2.0
18 | # via anyio
19 | fastapi==0.110.0
20 | # via flect
21 | flect==0.1.1
22 | # via todo
23 | h11==0.14.0
24 | # via uvicorn
25 | idna==3.6
26 | # via anyio
27 | pydantic==2.6.3
28 | # via fastapi
29 | # via flect
30 | pydantic-core==2.16.3
31 | # via pydantic
32 | pyromark==0.3.0
33 | # via flect
34 | sniffio==1.3.1
35 | # via anyio
36 | starlette==0.36.3
37 | # via fastapi
38 | typing-extensions==4.10.0
39 | # via anyio
40 | # via fastapi
41 | # via pydantic
42 | # via pydantic-core
43 | # via starlette
44 | # via uvicorn
45 | uvicorn==0.27.1
46 | # via flect
47 |
--------------------------------------------------------------------------------
/examples/todo/src/todo/__init__.py:
--------------------------------------------------------------------------------
1 | def hello() -> str:
2 | return "Hello from cookiecutter-flect!"
3 |
--------------------------------------------------------------------------------
/examples/todo/src/todo/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/examples/todo/src/todo/app/__init__.py
--------------------------------------------------------------------------------
/examples/todo/src/todo/app/layout.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 |
3 | from flect import PageResponse
4 | from flect import components as c
5 |
6 |
7 | async def layout(outlet: c.AnyComponent = c.Outlet()) -> PageResponse:
8 | return PageResponse(
9 | body=c.Container(
10 | tag="div",
11 | children=[
12 | c.Container(
13 | tag="header",
14 | class_name="h-14 border-b text-sm flex items-center px-6",
15 | children=[
16 | c.Link(
17 | href="/",
18 | children=[
19 | c.Text(
20 | text="Todo",
21 | class_name="font-medium text-xl",
22 | )
23 | ],
24 | ),
25 | ]
26 | ),
27 | c.Container(
28 | tag="main",
29 | class_name="p-12",
30 | children=[outlet]
31 | ),
32 | c.Container(
33 | tag="footer",
34 | class_name="h-14 border-t text-sm flex items-center px-6",
35 | children=[
36 | c.Text(
37 | text="Made with ❤️ by flect",
38 | )
39 | ],
40 | )
41 | ],
42 | )
43 | )
44 |
--------------------------------------------------------------------------------
/examples/todo/src/todo/app/page.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from flect import PageResponse
6 | from flect import components as c
7 | from flect import form
8 |
9 | from todo.storage import storage
10 |
11 |
12 | class TodoInCreate(BaseModel):
13 | name: str = form.Input(placeholder="Enter task name...", max_length=16)
14 |
15 |
16 | class TodoInDB(TodoInCreate):
17 | id: str = Field(default_factory=lambda: str(uuid.uuid4()))
18 |
19 |
20 | async def page() -> PageResponse:
21 | todos = [TodoInDB(**todo) for todo in storage.list()]
22 | return PageResponse(
23 | body=c.Container(
24 | tag="section",
25 | children=[
26 | c.Form(
27 | model=TodoInCreate,
28 | submit_url="/",
29 | class_name="mb-5 border p-5",
30 | ),
31 | c.Table(
32 | datasets=todos,
33 | )
34 | ],
35 | ),
36 | )
37 |
--------------------------------------------------------------------------------
/examples/todo/src/todo/app/route.py:
--------------------------------------------------------------------------------
1 | from flect.actions import Notify
2 | from flect.response import ActionResponse
3 |
4 | from todo.app.page import TodoInCreate, TodoInDB
5 | from todo.storage import storage
6 |
7 |
8 | async def post(form: TodoInCreate) -> ActionResponse:
9 | storage.insert(TodoInDB(**form.dict()).model_dump())
10 | return ActionResponse(
11 | action=Notify(title="success", style="success", description=f"todo {form.name} created."),
12 | )
13 |
--------------------------------------------------------------------------------
/examples/todo/src/todo/main.py:
--------------------------------------------------------------------------------
1 | from flect import flect
2 |
3 | from todo import app as todo_app
4 |
5 | app = flect(todo_app)
6 |
--------------------------------------------------------------------------------
/examples/todo/src/todo/storage.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 |
5 | class Storage:
6 | JSON_URI = Path(__file__).parent / "todos.json"
7 |
8 | def __init__(self) -> None:
9 | if not self.JSON_URI.exists():
10 | self.JSON_URI.touch(exist_ok=True)
11 |
12 | def insert(self, item: dict) -> None:
13 | with self.JSON_URI.open("a") as f:
14 | f.write(f"{json.dumps(item)}\n")
15 |
16 | def list(self) -> list[dict]:
17 | with self.JSON_URI.open("r") as f:
18 | return [json.loads(line) for line in f]
19 |
20 |
21 | storage = Storage()
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flect",
3 | "type": "module",
4 | "workspaces": [
5 | "src/*"
6 | ],
7 | "scripts": {
8 | "dev": "npm run --workspace=@chaoying/npm-flect build --watch",
9 | "build": "npm run --workspaces build",
10 | "prettier": "prettier --write",
11 | "lint": "eslint src --ext .ts,.tsx --report-unused-disable-directives --max-warnings 0",
12 | "lint-fix": "npm run lint -- --fix",
13 | "format": "npm run prettier -- . && npm run lint-fix"
14 | },
15 | "prettier": {
16 | "singleQuote": true,
17 | "semi": false,
18 | "trailingComma": "all",
19 | "tabWidth": 2,
20 | "printWidth": 119,
21 | "bracketSpacing": true
22 | },
23 | "devDependencies": {
24 | "@types/node": "^20.11.24",
25 | "@types/react": "^18.2.61",
26 | "@types/react-dom": "^18.2.19",
27 | "@typescript-eslint/eslint-plugin": "^6.21.0",
28 | "@typescript-eslint/parser": "^6.21.0",
29 | "eslint": "^8.57.0",
30 | "eslint-config-prettier": "^9.1.0",
31 | "eslint-config-standard": "^17.1.0",
32 | "eslint-plugin-react": "^7.34.0",
33 | "eslint-plugin-react-hooks": "^4.6.0",
34 | "eslint-plugin-react-refresh": "^0.4.5",
35 | "eslint-plugin-simple-import-sort": "^10.0.0",
36 | "prettier": "^3.2.5",
37 | "typescript": "^5.3.3"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'src/*'
3 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "flect"
3 | version = "0.2.10"
4 | description = "Turning ideas into web app fast."
5 | authors = [
6 | {name = "Chaoying", email = "chaunceywe@gmail.com"},
7 | ]
8 | dependencies = [
9 | "fastapi>=0.108.0",
10 | "uvicorn>=0.25.0",
11 | "pydantic>=2.6.1",
12 | "pyromark>=0.3.0",
13 | ]
14 | requires-python = ">=3.9"
15 | readme = "README.md"
16 | license = {text = "MIT"}
17 | classifiers = [
18 | "Development Status :: 4 - Beta",
19 | "Topic :: Internet",
20 | "License :: OSI Approved :: MIT License",
21 | "Programming Language :: Python :: 3",
22 | "Programming Language :: Python :: 3 :: Only",
23 | "Programming Language :: Python :: 3.9",
24 | "Programming Language :: Python :: 3.10",
25 | "Programming Language :: Python :: 3.11",
26 | "Programming Language :: Python :: 3.12",
27 | "Intended Audience :: Developers",
28 | "Intended Audience :: Information Technology",
29 | "Framework :: Pydantic :: 2",
30 | "Framework :: FastAPI",
31 | ]
32 |
33 | [project.urls]
34 | Homepage = "https://github.com/Chaoyingz/flect"
35 | Documentation = "https://flect.celerforge.com/"
36 | Source = "https://github.com/Chaoyingz/flect"
37 |
38 | [build-system]
39 | requires = ["hatchling"]
40 | build-backend = "hatchling.build"
41 |
42 | [tool.rye]
43 | managed = true
44 | dev-dependencies = [
45 | "ruff>=0.1.12",
46 | "pyright>=1.1.347",
47 | "coverage>=7.4.1",
48 | "pytest>=7.4.4",
49 | "pytest-asyncio>=0.23.4",
50 | "httpx>=0.27.0",
51 | ]
52 |
53 | [tool.hatch.build]
54 | artifacts = ["src/python-flect/src/static"]
55 | only-include = ["src/python-flect/src/static", "src/python-flect/src/flect"]
56 |
57 | [tool.hatch.build.targets.wheel]
58 | packages = ["src/python-flect/src/flect"]
59 |
60 | [tool.ruff]
61 | line-length = 120
62 | target-version = "py39"
63 |
64 | [tool.ruff.lint]
65 | extend-select = ["Q", "RUF100", "UP", "I"]
66 |
67 | [tool.pyright]
68 | include = ["src/python-flect/src/flect"]
69 | extraPaths = ["src/python-flect/src/flect"]
70 |
71 | [tool.coverage.run]
72 | omit = ["src/python-flect/tests/app/"]
73 |
74 | [tool.pytest.ini_options]
75 | asyncio_mode = "auto"
76 | addopts = "-s -p no:warnings"
77 | cache_dir = ".pytest_cache/"
78 | testpaths = [
79 | "src/python-flect/tests",
80 | "src/python-flect/src/flect",
81 | ]
82 |
--------------------------------------------------------------------------------
/requirements-dev.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 |
10 | -e file:.
11 | annotated-types==0.6.0
12 | # via pydantic
13 | anyio==4.3.0
14 | # via httpx
15 | # via starlette
16 | certifi==2024.2.2
17 | # via httpcore
18 | # via httpx
19 | click==8.1.7
20 | # via uvicorn
21 | coverage==7.4.3
22 | exceptiongroup==1.2.0
23 | # via anyio
24 | # via pytest
25 | fastapi==0.110.0
26 | # via flect
27 | h11==0.14.0
28 | # via httpcore
29 | # via uvicorn
30 | httpcore==1.0.4
31 | # via httpx
32 | httpx==0.27.0
33 | idna==3.6
34 | # via anyio
35 | # via httpx
36 | iniconfig==2.0.0
37 | # via pytest
38 | nodeenv==1.8.0
39 | # via pyright
40 | packaging==23.2
41 | # via pytest
42 | pluggy==1.4.0
43 | # via pytest
44 | pydantic==2.6.4
45 | # via fastapi
46 | # via flect
47 | pydantic-core==2.16.3
48 | # via pydantic
49 | pyright==1.1.351
50 | pyromark==0.3.0
51 | # via flect
52 | pytest==8.0.2
53 | # via pytest-asyncio
54 | pytest-asyncio==0.23.5
55 | ruff==0.2.2
56 | setuptools==69.1.1
57 | # via nodeenv
58 | sniffio==1.3.1
59 | # via anyio
60 | # via httpx
61 | starlette==0.36.3
62 | # via fastapi
63 | tomli==2.0.1
64 | # via pytest
65 | typing-extensions==4.10.0
66 | # via anyio
67 | # via fastapi
68 | # via pydantic
69 | # via pydantic-core
70 | # via starlette
71 | # via uvicorn
72 | uvicorn==0.27.1
73 | # via flect
74 |
--------------------------------------------------------------------------------
/requirements.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 |
10 | -e file:.
11 | annotated-types==0.6.0
12 | # via pydantic
13 | anyio==4.3.0
14 | # via starlette
15 | click==8.1.7
16 | # via uvicorn
17 | exceptiongroup==1.2.0
18 | # via anyio
19 | fastapi==0.110.0
20 | # via flect
21 | h11==0.14.0
22 | # via uvicorn
23 | idna==3.6
24 | # via anyio
25 | pydantic==2.6.4
26 | # via fastapi
27 | # via flect
28 | pydantic-core==2.16.3
29 | # via pydantic
30 | pyromark==0.3.0
31 | # via flect
32 | sniffio==1.3.1
33 | # via anyio
34 | starlette==0.36.3
35 | # via fastapi
36 | typing-extensions==4.10.0
37 | # via anyio
38 | # via fastapi
39 | # via pydantic
40 | # via pydantic-core
41 | # via starlette
42 | # via uvicorn
43 | uvicorn==0.27.1
44 | # via flect
45 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:react-hooks/recommended",
8 | ],
9 | ignorePatterns: ["dist", ".eslintrc.cjs"],
10 | parser: "@typescript-eslint/parser",
11 | plugins: ["react-refresh"],
12 | rules: {
13 | "react-refresh/only-export-components": [
14 | "warn",
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-tailwindcss"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default {
18 | // other rules...
19 | parserOptions: {
20 | ecmaVersion: "latest",
21 | sourceType: "module",
22 | project: ["./tsconfig.json", "./tsconfig.node.json"],
23 | tsconfigRootDir: __dirname,
24 | },
25 | };
26 | ```
27 |
28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
31 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@chaoying/flect-prebuilt",
3 | "description": "flect is a Python framework for building full-stack web applications.",
4 | "version": "0.2.10",
5 | "type": "module",
6 | "main": "dist/index.html",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "tsc && vite build",
10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "class-variance-authority": "^0.7.0",
15 | "clsx": "^2.1.0",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "react-syntax-highlighter": "^15.5.0",
19 | "tailwind-merge": "^2.2.1",
20 | "tailwindcss-animate": "^1.0.7",
21 | "@chaoying/flect": "workspace:*"
22 | },
23 | "devDependencies": {
24 | "@types/react": "^18.2.64",
25 | "@types/react-dom": "^18.2.21",
26 | "@types/react-syntax-highlighter": "^15.5.11",
27 | "@typescript-eslint/eslint-plugin": "^7.1.1",
28 | "@typescript-eslint/parser": "^7.1.1",
29 | "@vitejs/plugin-react": "^4.2.1",
30 | "autoprefixer": "^10.4.18",
31 | "eslint": "^8.57.0",
32 | "eslint-plugin-react-hooks": "^4.6.0",
33 | "eslint-plugin-react-refresh": "^0.4.5",
34 | "postcss": "^8.4.35",
35 | "prettier": "^3.2.5",
36 | "prettier-plugin-tailwindcss": "^0.5.12",
37 | "rollup-plugin-base-url": "^0.0.2",
38 | "tailwindcss": "^3.4.1",
39 | "typescript": "^5.2.2",
40 | "vite": "^5.1.6"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/public/vite.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Flect,
3 | ActionResolverProvider,
4 | ComponentResolverProvider,
5 | getMetaContent,
6 | } from "@chaoying/flect";
7 | import {
8 | FlectActionResolver,
9 | FlectComponentResolver,
10 | } from "@chaoying/flect/components";
11 |
12 | function App() {
13 | return (
14 |
15 |
16 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default App;
28 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/src/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.tsx";
4 | import "@/globals.css";
5 |
6 | ReactDOM.createRoot(document.getElementById("root")!).render(
7 |
8 |
9 | ,
10 | );
11 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{ts,tsx}",
6 | "./components/**/*.{ts,tsx}",
7 | "./app/**/*.{ts,tsx}",
8 | "./src/**/*.{ts,tsx}",
9 | ],
10 | // prefix: 'fp',
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "2rem",
15 | screens: {
16 | "2xl": "1400px",
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: "0" },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: "0" },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | },
75 | },
76 | safelist: [
77 | {
78 | pattern: /^(w|max-w|h|max-h)-/,
79 | },
80 | {
81 | pattern: /^(flex|grid)-/,
82 | },
83 | {
84 | pattern: /^(m|p)\w?-\d/,
85 | },
86 | {
87 | pattern: /^gap-\d/,
88 | },
89 | {
90 | pattern: /^text-(\w{2}|\d{1}\w{2})$/,
91 | },
92 | {
93 | pattern: /^(font|justify)-/,
94 | },
95 | "border-b",
96 | "text-center",
97 | "invisible",
98 | "absolute",
99 | "overflow-hidden",
100 | "underline",
101 | "min-h-screen",
102 | "text-primary",
103 | "text-primary-foreground",
104 | "bg-primary",
105 | "bg-primary-foreground",
106 | ],
107 | corePlugins: {
108 | preflight: false,
109 | },
110 | plugins: [require("tailwindcss-animate")],
111 | };
112 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | }
8 | },
9 | "include": ["src"],
10 | "references": [{ "path": "./tsconfig.node.json" }]
11 | }
12 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/src/npm-flect-prebuilt/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig } from "vite";
3 | import react from "@vitejs/plugin-react";
4 | import { splitVendorChunkPlugin } from "vite";
5 |
6 | const serverConfig = {
7 | host: true,
8 | port: 3000,
9 | proxy: {
10 | "/flect": "http://localhost:8000/",
11 | },
12 | watch: {
13 | ignored: ["!**/node_modules/@chaoying/flect/**"],
14 | },
15 | };
16 |
17 | export default defineConfig({
18 | plugins: [react(), splitVendorChunkPlugin()],
19 | resolve: {
20 | alias: {
21 | "@": path.resolve(__dirname, "./src"),
22 | },
23 | },
24 | server: serverConfig,
25 | optimizeDeps: {
26 | exclude: ["@chaoying/flect"],
27 | },
28 | preview: serverConfig,
29 | build: {
30 | rollupOptions: {
31 | output: {
32 | entryFileNames: `assets/[name].js`,
33 | chunkFileNames: `assets/[name].js`,
34 | assetFileNames: `assets/[name].[ext]`,
35 | },
36 | plugins: [],
37 | },
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/src/npm-flect/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:react-hooks/recommended",
8 | ],
9 | ignorePatterns: ["dist", ".eslintrc.cjs"],
10 | parser: "@typescript-eslint/parser",
11 | plugins: ["react-refresh"],
12 | rules: {
13 | "react-refresh/only-export-components": [
14 | "warn",
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/src/npm-flect/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/src/npm-flect/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-tailwindcss"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/npm-flect/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default {
18 | // other rules...
19 | parserOptions: {
20 | ecmaVersion: "latest",
21 | sourceType: "module",
22 | project: ["./tsconfig.json", "./tsconfig.node.json"],
23 | tsconfigRootDir: __dirname,
24 | },
25 | };
26 | ```
27 |
28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
31 |
--------------------------------------------------------------------------------
/src/npm-flect/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/npm-flect/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@chaoying/flect",
3 | "description": "flect is a Python framework for building full-stack web applications.",
4 | "author": "chaoying",
5 | "license": "MIT",
6 | "private": false,
7 | "version": "0.2.10",
8 | "type": "module",
9 | "main": "dist/index.js",
10 | "types": "dist/index.d.ts",
11 | "exports": {
12 | ".": {
13 | "import": "./dist/index.js",
14 | "require": "./dist/index.umd.cjs",
15 | "types": "./dist/index.d.ts"
16 | },
17 | "./components": {
18 | "import": "./dist/components.js",
19 | "types": "./dist/components/index.d.ts"
20 | }
21 | },
22 | "scripts": {
23 | "dev": "vite",
24 | "build": "tsc && vite build --emptyOutDir",
25 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
26 | "preview": "vite preview"
27 | },
28 | "dependencies": {
29 | "@fengkx/react-syntax-highlighter": "15.6.1",
30 | "@hookform/resolvers": "^3.3.4",
31 | "@radix-ui/react-avatar": "^1.0.4",
32 | "@radix-ui/react-checkbox": "^1.0.4",
33 | "@radix-ui/react-dialog": "^1.0.5",
34 | "@radix-ui/react-label": "^2.0.2",
35 | "@radix-ui/react-popover": "^1.0.7",
36 | "@radix-ui/react-select": "^2.0.0",
37 | "@radix-ui/react-slot": "^1.0.2",
38 | "@radix-ui/react-tooltip": "^1.0.7",
39 | "ajv": "^8.12.0",
40 | "ajv-errors": "^3.0.0",
41 | "class-variance-authority": "^0.7.0",
42 | "clsx": "^2.1.0",
43 | "cmdk": "0.2.1",
44 | "lucide-react": "^0.302.0",
45 | "react": "^18.2.0",
46 | "react-dom": "^18.2.0",
47 | "react-hook-form": "^7.51.1",
48 | "react-markdown": "^9.0.1",
49 | "react-router-dom": "^6.22.3",
50 | "remark-gfm": "^4.0.0",
51 | "sonner": "^1.4.41",
52 | "tailwind-merge": "^2.2.2",
53 | "tailwindcss-animate": "^1.0.7"
54 | },
55 | "devDependencies": {
56 | "@tailwindcss/typography": "^0.5.10",
57 | "@thoughtbot/tailwindcss-aria-attributes": "^0.2.0",
58 | "@types/json-schema": "^7.0.15",
59 | "@types/node": "^20.11.30",
60 | "@types/react": "^18.2.67",
61 | "@types/react-dom": "^18.2.22",
62 | "@typescript-eslint/eslint-plugin": "^6.21.0",
63 | "@typescript-eslint/parser": "^6.21.0",
64 | "@vitejs/plugin-react": "^4.2.1",
65 | "autoprefixer": "^10.4.19",
66 | "eslint": "^8.57.0",
67 | "eslint-plugin-react-hooks": "^4.6.0",
68 | "eslint-plugin-react-refresh": "^0.4.6",
69 | "postcss": "^8.4.38",
70 | "rollup-plugin-base-url": "^0.0.2",
71 | "tailwindcss": "^3.4.1",
72 | "typescript": "^5.4.3",
73 | "vite": "^5.2.2",
74 | "vite-bundle-analyzer": "^0.8.3",
75 | "vite-plugin-dts": "^3.7.3",
76 | "vite-plugin-lib-inject-css": "^2.0.0"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/npm-flect/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/npm-flect/public/vite.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/npm-flect/src/application.tsx:
--------------------------------------------------------------------------------
1 | import { Router } from "@/components/routing";
2 | import { Toaster } from "@/components/ui/sonner";
3 | import "@/globals.css";
4 | import { ConfigContextState, ConfigProvider } from "@/contexts/config";
5 | import { TooltipProvider } from "@/components/ui/tooltip";
6 |
7 | export function Flect(props: ConfigContextState) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/action-resolver.tsx:
--------------------------------------------------------------------------------
1 | import { ActionResolver } from "@/contexts/action-resolver";
2 | import {
3 | dispatchEventAction,
4 | notifyAction,
5 | redirectAction,
6 | } from "@/lib/actions";
7 | import { AnyActionProps } from "@/types";
8 |
9 | export const FlectActionResolver: ActionResolver = (props: AnyActionProps) => {
10 | switch (props.type) {
11 | case "notify": {
12 | return () => notifyAction(props);
13 | }
14 | case "redirect": {
15 | return () => redirectAction(props);
16 | }
17 | case "dispatch-event": {
18 | return () => dispatchEventAction(props);
19 | }
20 | }
21 |
22 | return null;
23 | };
24 | FlectActionResolver.package = "flect";
25 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/component-resolver.tsx:
--------------------------------------------------------------------------------
1 | import { AnyComponentProps } from "@/types";
2 |
3 | import { Avatar } from "@/components/flect/avatar";
4 | import { Button } from "@/components/flect/button";
5 | import { Container } from "@/components/flect/container";
6 | import { Heading } from "@/components/flect/heading";
7 | import { Link } from "@/components/flect/link";
8 | import { Text } from "@/components/flect/text";
9 | import { Table } from "@/components/flect/table";
10 | import { Outlet } from "@/components/flect/outlet";
11 | import { Form } from "@/components/flect/form";
12 | import { NavLink } from "@/components/flect/nav-link";
13 | import { Paragraph } from "@/components/flect/paragraph";
14 | import { Markdown } from "@/components/flect/markdown";
15 | import { CopyButton } from "@/components/flect/copy-button";
16 | import { CodeBlock } from "@/components/flect/code-block";
17 | import { Custom } from "@/components/flect/custom";
18 | import { ComponentResolver } from "@/contexts/component-resolver";
19 | import { Dialog } from "@/components/flect/dialog";
20 | import { Display } from "@/components/flect/display";
21 | import { DataGrid } from "@/components/flect/data-grid";
22 | import { DeferredFetch } from "@/components/flect/deferred-fetch";
23 |
24 | export const FlectComponentResolver: ComponentResolver = (
25 | props: AnyComponentProps,
26 | ) => {
27 | switch (props.type) {
28 | case "avatar":
29 | return ;
30 | case "button":
31 | return ;
32 | case "code-block":
33 | return ;
34 | case "container":
35 | return ;
36 | case "copy-button":
37 | return ;
38 | case "data-grid":
39 | return ;
40 | case "deferred-fetch":
41 | return ;
42 | case "dialog":
43 | return ;
44 | case "display":
45 | return ;
46 | case "custom":
47 | return ;
48 | case "form":
49 | return ;
50 | case "heading":
51 | return ;
52 | case "link":
53 | return ;
54 | case "markdown":
55 | return ;
56 | case "nav-link":
57 | return ;
58 | case "outlet":
59 | return ;
60 | case "paragraph":
61 | return ;
62 | case "table":
63 | return ;
64 | case "text":
65 | return ;
66 | default:
67 | return null;
68 | }
69 | };
70 | FlectComponentResolver.package = "flect";
71 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/any-component.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentResolverContext } from "@/contexts/component-resolver";
2 | import { AnyComponentProps } from "@/types";
3 | import { useContext } from "react";
4 |
5 | export function AnyComponent(props: AnyComponentProps): JSX.Element {
6 | const context = useContext(ComponentResolverContext);
7 | if (!context) {
8 | return (
9 |
10 | Component resolver context not found.
11 |
12 | );
13 | }
14 |
15 | const { resolvers } = context;
16 | const resolver = resolvers[props.package];
17 | if (resolver) {
18 | const resolvedComponent = resolver(props);
19 | if (resolvedComponent === null) {
20 | return (
21 |
22 | Component of type {props.type} could not be resolved.
23 |
24 | );
25 | }
26 | return resolvedComponent;
27 | }
28 |
29 | return (
30 |
31 | No component resolver found for type {props.type}.
32 |
33 | );
34 | }
35 |
36 | export function AnyComponents({
37 | children,
38 | }: {
39 | children?: AnyComponentProps[];
40 | }) {
41 | return (
42 | <>
43 | {children &&
44 | children.map((props, index) => )}
45 | >
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/avatar.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AvatarFallback,
3 | AvatarImage,
4 | Avatar as AvatarUI,
5 | } from "@/components/ui/avatar";
6 |
7 | export interface AvatarProps {
8 | package: "flect";
9 | type: "avatar";
10 | className?: string;
11 | src?: string;
12 | alt?: string;
13 | fallback: string;
14 | }
15 | export function Avatar(props: AvatarProps) {
16 | return (
17 |
18 |
19 | {props.fallback}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/button.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button as ButtonUI,
3 | ButtonProps as ButtonPropsUI,
4 | } from "@/components/ui/button";
5 | import { AnyComponents } from "@/components/flect/any-component";
6 | import { AnyActionProps, AnyComponentProps } from "@/types";
7 | import { useAction } from "@/hooks/use-action";
8 |
9 | export interface ButtonProps extends Omit {
10 | package: "flect";
11 | type: "button";
12 | onClickAction?: AnyActionProps;
13 | className?: string;
14 | children?: AnyComponentProps[];
15 | }
16 |
17 | export function Button({ children, onClickAction, ...rest }: ButtonProps) {
18 | const resolvedAction = useAction(onClickAction);
19 | return (
20 | resolvedAction()}>
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { CopyButton } from "@/components/flect/copy-button";
2 | import { cn } from "@/lib/utils";
3 |
4 | export type CodeBlockProps = {
5 | package: "flect";
6 | type: "code-block";
7 | className?: string;
8 | text: string;
9 | language?: string;
10 | };
11 |
12 | export function CodeBlock({ text, ...props }: CodeBlockProps) {
13 | return (
14 |
20 |
26 | {text}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/container.tsx:
--------------------------------------------------------------------------------
1 | import { AnyComponents } from "@/components/flect/any-component";
2 | import { AnyComponentProps } from "@/types";
3 |
4 | export interface ContainerProps {
5 | package: "flect";
6 | type: "container";
7 | className?: string;
8 | children?: AnyComponentProps[];
9 | tag: "div" | "section" | "header" | "footer" | "main" | "nav" | "aside";
10 | }
11 |
12 | export function Container(props: ContainerProps) {
13 | const { children, className, tag } = props;
14 | const Tag = tag || "div";
15 | return (
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/copy-button.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button as ButtonUI,
3 | ButtonProps as ButtonPropsUI,
4 | } from "@/components/ui/button";
5 | import { cn } from "@/lib/utils";
6 | import React from "react";
7 | import { Check, Copy } from "lucide-react";
8 |
9 | export interface CopyButtonProps extends Omit {
10 | package: "flect";
11 | type: "copy-button";
12 | className?: string;
13 | }
14 |
15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
16 | export function CopyButton({ type, ...props }: CopyButtonProps) {
17 | const [isCopied, setIsCopied] = React.useState(false);
18 |
19 | return (
20 | {
24 | if (typeof window === "undefined") return;
25 | setIsCopied(true);
26 | void window.navigator.clipboard.writeText(
27 | props.value?.toString() ?? "",
28 | );
29 | setTimeout(() => setIsCopied(false), 2000);
30 | }}
31 | {...props}
32 | >
33 | {isCopied ? (
34 |
35 | ) : (
36 |
37 | )}
38 |
39 | {isCopied ? "Copied" : "Copy to clipboard"}
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/custom.tsx:
--------------------------------------------------------------------------------
1 | export interface CustomProps {
2 | package: "flect";
3 | type: "custom";
4 | className?: string;
5 | subType: string;
6 | }
7 |
8 | export function Custom(props: CustomProps) {
9 | return (
10 |
11 | The custom component {props.subType}
component is not
12 | implemented yet.
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/deferred-fetch.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatchActionListen } from "@/hooks/use-dispatch-action-listen";
2 | import { DispatchEventActionProps } from "@/lib/actions";
3 | import { AnyComponentProps } from "@/types";
4 | import { useState, useEffect } from "react";
5 | import { AnyComponent } from "@/components/flect/any-component";
6 |
7 | export interface DeferredFetchProps {
8 | package: "flect";
9 | type: "deferred-fetch";
10 | className?: string;
11 | path: string;
12 | trigger: DispatchEventActionProps;
13 | }
14 |
15 | export function DeferredFetch(props: DeferredFetchProps) {
16 | const { dispatched } = useDispatchActionListen(props.trigger.event);
17 | const [componentProps, setComponentProps] =
18 | useState(null);
19 |
20 | useEffect(() => {
21 | if (dispatched) {
22 | fetch(props.path)
23 | .then((response) => response.json())
24 | .then((json) => {
25 | setComponentProps(json.body);
26 | });
27 | }
28 | }, [dispatched, props.path]);
29 |
30 | return componentProps && ;
31 | }
32 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/dialog.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog as DialogUI,
3 | DialogContent,
4 | DialogDescription,
5 | DialogHeader,
6 | DialogTitle,
7 | } from "@/components/ui/dialog";
8 | import { AnyComponentProps } from "@/types";
9 | import { AnyComponents } from "@/components/flect/any-component";
10 | import { cn } from "@/lib/utils";
11 | import { DispatchEventActionProps } from "@/lib/actions";
12 | import { useDispatchActionListen } from "@/hooks/use-dispatch-action-listen";
13 | import { useEffect, useState } from "react";
14 |
15 | export interface DialogProps {
16 | package: "flect";
17 | type: "dialog";
18 | className?: string;
19 | title?: string;
20 | description?: string;
21 | children?: AnyComponentProps[];
22 | defaultOpen: boolean;
23 | trigger?: DispatchEventActionProps;
24 | }
25 |
26 | export function Dialog(props: DialogProps) {
27 | const { dispatched, setDispatched } = useDispatchActionListen(
28 | props.trigger?.event || "",
29 | );
30 | const [isOpen, setIsOpen] = useState(props.defaultOpen);
31 |
32 | function setOpen(open: boolean) {
33 | setIsOpen(open);
34 | setDispatched(open);
35 | }
36 |
37 | useEffect(() => {
38 | if (dispatched) {
39 | setIsOpen(dispatched);
40 | }
41 | }, [dispatched]);
42 |
43 | return (
44 |
45 |
46 | {(props.title || props.description) && (
47 |
48 | {props.title && {props.title}}
49 | {props.description && (
50 | {props.description}
51 | )}
52 |
53 | )}
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/display.tsx:
--------------------------------------------------------------------------------
1 | import { Json } from "@/types";
2 |
3 | export type DisplayType = "auto" | "boolean" | "json" | "null" | "text";
4 |
5 | interface ValueProps {
6 | value: Json;
7 | }
8 |
9 | interface DisplayProps extends ValueProps {
10 | displayType: DisplayType;
11 | }
12 |
13 | export function Display({ displayType, value }: DisplayProps) {
14 | switch (displayType) {
15 | case "auto":
16 | return ;
17 | case "boolean":
18 | return ;
19 | case "json":
20 | return ;
21 | case "null":
22 | return ;
23 | case "text":
24 | return ;
25 | default:
26 | return ;
27 | }
28 | }
29 |
30 | export function AutoDisplay({ value }: { value: Json }) {
31 | if (value === null) {
32 | return ;
33 | } else if (typeof value === "boolean") {
34 | return ;
35 | } else if (typeof value === "number") {
36 | return <>{value.toLocaleString()}>;
37 | } else if (typeof value === "string") {
38 | return ;
39 | } else {
40 | return ;
41 | }
42 | }
43 |
44 | export function BooleanDisplay({ value }: ValueProps) {
45 | return <>{value ? "true" : "false"}>;
46 | }
47 |
48 | export function JsonDisplay({ value }: ValueProps) {
49 | return {JSON.stringify(value, null, 2)}
;
50 | }
51 |
52 | export function NullDisplay() {
53 | return <>/>;
54 | }
55 |
56 | export function TextDisplay({ value }: ValueProps) {
57 | return <>{value}>;
58 | }
59 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/heading.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | export interface HeadingProps {
4 | package: "flect";
5 | type: "heading";
6 | className?: string;
7 | level: 1 | 2 | 3 | 4 | 5 | 6;
8 | text: string;
9 | id?: string;
10 | }
11 |
12 | export function Heading(props: HeadingProps) {
13 | const { level, text, id } = props;
14 | const Tag = `h${level}` as keyof JSX.IntrinsicElements;
15 | return (
16 |
17 | {text}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/link.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { AnyComponents } from "@/components/flect/any-component";
3 | import { Link as RemixLink } from "react-router-dom";
4 | import { VariantProps } from "class-variance-authority";
5 | import { linkVariants } from "@/components/flect/link.types";
6 | import { AnyComponentProps } from "@/types";
7 |
8 | export interface LinkProps extends VariantProps {
9 | package: "flect";
10 | type: "link";
11 | className?: string;
12 | href: string;
13 | target?: "_self" | "_blank";
14 | children?: AnyComponentProps[];
15 | }
16 |
17 | export function Link(props: LinkProps) {
18 | const { href, underline, children, className } = props;
19 | return (
20 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/link.types.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const linkVariants = cva("text-primary", {
4 | variants: {
5 | underline: {
6 | none: "",
7 | hover: "underline-offset-4 hover:underline",
8 | always: "underline-offset-4 underline",
9 | },
10 | isActive: {
11 | true: "underline-offset-4 underline font-semibold",
12 | false: "",
13 | },
14 | },
15 | defaultVariants: {
16 | underline: "hover",
17 | isActive: false,
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import ReactMarkdown, { Components } from "react-markdown";
3 | import remarkGfm from "remark-gfm";
4 | import { CodeBlock } from "./code-block";
5 |
6 | export interface MarkdownProps {
7 | package: "flect";
8 | type: "markdown";
9 | className?: string;
10 | text: string;
11 | }
12 |
13 | export function Markdown(props: MarkdownProps) {
14 | const { text, className } = props;
15 | const components: Components = {
16 | code({ children, className }) {
17 | const language = /language-(\w+)/.exec(className || "");
18 | if (!language) {
19 | return {children}
;
20 | }
21 | return (
22 |
28 | );
29 | },
30 | };
31 |
32 | return (
33 |
38 | {text}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/nav-link.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { AnyComponents } from "@/components/flect/any-component";
3 | import { NavLink as RemixNavLink } from "react-router-dom";
4 | import { LinkProps } from "@/components/flect/link";
5 | import { linkVariants } from "@/components/flect/link.types";
6 |
7 | export interface NavLinkProps extends Omit {
8 | package: "flect";
9 | type: "nav-link";
10 | }
11 |
12 | export function NavLink(props: NavLinkProps) {
13 | const { href, underline, className, children } = props;
14 | return (
15 |
18 | cn(linkVariants({ underline, isActive, className }))
19 | }
20 | target={props.target}
21 | >
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/outlet.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet as RemixOutlet } from "react-router-dom";
2 |
3 | export interface OutletProps {
4 | package: "flect";
5 | type: "outlet";
6 | }
7 |
8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
9 | export function Outlet(_: OutletProps) {
10 | return ;
11 | }
12 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/paragraph.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { ReactNode } from "react";
3 | import { Link as RemixLink } from "react-router-dom";
4 | import { linkVariants } from "./link.types";
5 |
6 | export interface ParagraphProps {
7 | package: "flect";
8 | type: "paragraph";
9 | className?: string;
10 | text: string;
11 | }
12 |
13 | export function Paragraph(props: ParagraphProps) {
14 | const { text, className } = props;
15 | const renderText = (): ReactNode[] => {
16 | const pattern = /\[(.*?)\]\((.*?)\)/g;
17 | let lastIndex = 0;
18 | let match: RegExpExecArray | null;
19 | const result: ReactNode[] = [];
20 |
21 | while ((match = pattern.exec(text)) !== null) {
22 | if (match.index > lastIndex) {
23 | result.push(text.substring(lastIndex, match.index));
24 | }
25 |
26 | result.push(
27 |
33 | {match[1]}
34 | ,
35 | );
36 |
37 | lastIndex = pattern.lastIndex;
38 | }
39 |
40 | if (lastIndex < text.length) {
41 | result.push(text.substring(lastIndex));
42 | }
43 |
44 | return result;
45 | };
46 | return {renderText()}
;
47 | }
48 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/table.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Table as TableUI,
3 | TableHead,
4 | TableBody,
5 | TableRow,
6 | TableCell,
7 | TableHeader,
8 | } from "@/components/ui/table";
9 | import { Json } from "@/types";
10 | import { JSONSchema7 } from "json-schema";
11 | import { Display, DisplayType } from "@/components/flect/display";
12 |
13 | export interface Dataset {
14 | [k: string]: Json;
15 | }
16 |
17 | interface Model extends JSONSchema7 {
18 | displayType: DisplayType;
19 | properties: {
20 | [key: string]: Model;
21 | };
22 | }
23 |
24 | export interface TableProps {
25 | package: "flect";
26 | type: "table";
27 | className?: string;
28 |
29 | model: Model;
30 | datasets: Dataset[];
31 | }
32 |
33 | export function Table(props: TableProps) {
34 | return (
35 |
36 |
37 |
38 | {Object.keys(props.model.properties).map((key, index) => {
39 | return (
40 |
41 | {props.model.properties[key].title}
42 |
43 | );
44 | })}
45 |
46 |
47 |
48 | {props.datasets.map((dataset, index) => (
49 |
50 | {Object.keys(props.model.properties).map((key, index) => {
51 | return (
52 |
53 |
57 |
58 | );
59 | })}
60 |
61 | ))}
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/flect/text.tsx:
--------------------------------------------------------------------------------
1 | export interface TextProps {
2 | package: "flect";
3 | type: "text";
4 | className?: string;
5 | text: string;
6 | }
7 |
8 | export function Text(props: TextProps) {
9 | const { text, className } = props;
10 | return (
11 | <>{className ? {text} : <>{text}>}>
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "@/components/routing";
2 | export * from "@/components/action-resolver";
3 | export * from "@/components/component-resolver";
4 | export * from "@/components/flect/any-component.tsx";
5 | export * from "@/components/flect/avatar.tsx";
6 | export * from "@/components/flect/button.tsx";
7 | export * from "@/components/flect/code-block.tsx";
8 | export * from "@/components/flect/container.tsx";
9 | export * from "@/components/flect/copy-button.tsx";
10 | export * from "@/components/flect/custom.tsx";
11 | export * from "@/components/flect/form.tsx";
12 | export * from "@/components/flect/heading.tsx";
13 | export * from "@/components/flect/link.tsx";
14 | export * from "@/components/flect/markdown.tsx";
15 | export * from "@/components/flect/nav-link.tsx";
16 | export * from "@/components/flect/paragraph.tsx";
17 | export * from "@/components/flect/outlet.tsx";
18 | export * from "@/components/flect/table.tsx";
19 | export * from "@/components/flect/text.tsx";
20 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/routing.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createBrowserRouter,
3 | RouteObject,
4 | RouterProvider,
5 | useLoaderData,
6 | useRouteError,
7 | LoaderFunctionArgs,
8 | useNavigate,
9 | RouteProps as RemixRouteProps,
10 | } from "react-router-dom";
11 | import { useCallback, useEffect, useState } from "react";
12 | import React from "react";
13 | import { Button } from "@/components/ui/button";
14 | import { AnyComponentProps } from "@/types";
15 | import { AnyComponent } from "@/components/flect/any-component";
16 |
17 | const ROOT_ROUTE_PREFIX = "/flect";
18 | const NAVIGATION_ROUTE_PATH = "/_route/";
19 |
20 | export type RouteProps = RemixRouteProps & {
21 | children?: RouteProps[];
22 | segment: string;
23 | path: string;
24 | loader_path: string;
25 | index: boolean;
26 | };
27 |
28 | type PageResponse = {
29 | body: AnyComponentProps;
30 | };
31 |
32 | const RouteElement = React.memo(() => {
33 | const response = useLoaderData() as PageResponse;
34 | return ;
35 | });
36 |
37 | const ErrorElement = React.memo(() => {
38 | const error = useRouteError() as { statusText?: string; message?: string };
39 | const navigate = useNavigate();
40 | console.error(error);
41 |
42 | return (
43 |
44 |
45 |
Oops!
46 |
Sorry, an unexpected error has occurred.
47 |
48 | {error.statusText || error.message}
49 |
50 |
53 |
54 |
55 | );
56 | });
57 |
58 | interface DefaultPageResponse {
59 | [key: string]: unknown;
60 | }
61 |
62 | const fetchJson = async (
63 | input: RequestInfo,
64 | init?: RequestInit,
65 | ): Promise => {
66 | const response = await fetch(input, init);
67 | if (!response.ok) {
68 | throw new Error(response.statusText);
69 | }
70 | return await response.json();
71 | };
72 |
73 | const useRoutes = () => {
74 | const [routes, setRoutes] = useState();
75 |
76 | const convertRoute = useCallback((route: RouteProps): RouteObject => {
77 | const path = route.path.replace(/{(.*?)}/g, ":$1");
78 | const loader = async ({ params }: LoaderFunctionArgs) => {
79 | let url = `${route.loader_path.replace(/{(.*?)}/g, ":$1")}`;
80 |
81 | Object.keys(params).forEach((key) => {
82 | const value = params[key];
83 | if (value) {
84 | url = url.replace(`:${key}`, value);
85 | }
86 | });
87 | return fetchJson(url);
88 | };
89 | const routeObj: Partial = {
90 | path: path,
91 | element: ,
92 | errorElement: ,
93 | loader,
94 | };
95 | if (route.index === true) {
96 | routeObj.index = true;
97 | } else {
98 | routeObj.children = route.children?.map(convertRoute);
99 | }
100 | return routeObj;
101 | }, []);
102 |
103 | useEffect(() => {
104 | let isMounted = true;
105 |
106 | fetchJson(`${ROOT_ROUTE_PREFIX}${NAVIGATION_ROUTE_PATH}`)
107 | .then((routeProps) => {
108 | if (isMounted) {
109 | setRoutes(routeProps.map(convertRoute));
110 | }
111 | })
112 | .catch(console.error);
113 |
114 | return () => {
115 | isMounted = false;
116 | };
117 | }, [convertRoute]);
118 |
119 | return routes;
120 | };
121 |
122 | export const Router = () => {
123 | const routeObjects = useRoutes();
124 | return routeObjects ? (
125 |
126 | ) : null;
127 | };
128 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Avatar = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ));
19 | Avatar.displayName = AvatarPrimitive.Root.displayName;
20 |
21 | const AvatarImage = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ));
31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
32 |
33 | const AvatarFallback = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 | ));
46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
47 |
48 | export { Avatar, AvatarImage, AvatarFallback };
49 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | },
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | },
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3 | import { Check } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ));
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
27 |
28 | export { Checkbox };
29 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as DialogPrimitive from "@radix-ui/react-dialog";
3 | import { X } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const Dialog = DialogPrimitive.Root;
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger;
10 |
11 | const DialogPortal = DialogPrimitive.Portal;
12 |
13 | const DialogClose = DialogPrimitive.Close;
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ));
52 | DialogContent.displayName = DialogPrimitive.Content.displayName;
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | );
66 | DialogHeader.displayName = "DialogHeader";
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | );
80 | DialogFooter.displayName = "DialogFooter";
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ));
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ));
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogClose,
114 | DialogTrigger,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | };
121 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as LabelPrimitive from "@radix-ui/react-label";
3 | import { Slot } from "@radix-ui/react-slot";
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form";
12 |
13 | import { cn } from "@/lib/utils";
14 | import { Label } from "@/components/ui/label";
15 |
16 | const Form = FormProvider;
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath,
21 | > = {
22 | name: TName;
23 | };
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue,
27 | );
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath,
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext);
44 | const itemContext = React.useContext(FormItemContext);
45 | const { getFieldState, formState } = useFormContext();
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState);
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ");
51 | }
52 |
53 | const { id } = itemContext;
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | };
63 | };
64 |
65 | type FormItemContextValue = {
66 | id: string;
67 | };
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue,
71 | );
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId();
78 |
79 | return (
80 |
81 |
82 |
83 | );
84 | });
85 | FormItem.displayName = "FormItem";
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField();
92 |
93 | return (
94 |
100 | );
101 | });
102 | FormLabel.displayName = "FormLabel";
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } =
109 | useFormField();
110 |
111 | return (
112 |
123 | );
124 | });
125 | FormControl.displayName = "FormControl";
126 |
127 | const FormDescription = React.forwardRef<
128 | HTMLParagraphElement,
129 | React.HTMLAttributes
130 | >(({ className, ...props }, ref) => {
131 | const { formDescriptionId } = useFormField();
132 |
133 | return (
134 |
140 | );
141 | });
142 | FormDescription.displayName = "FormDescription";
143 |
144 | const FormMessage = React.forwardRef<
145 | HTMLParagraphElement,
146 | React.HTMLAttributes
147 | >(({ className, children, ...props }, ref) => {
148 | const { error, formMessageId } = useFormField();
149 | const body = error ? String(error?.message) : children;
150 |
151 | if (!body) {
152 | return null;
153 | }
154 |
155 | return (
156 |
162 | {body}
163 |
164 | );
165 | });
166 | FormMessage.displayName = "FormMessage";
167 |
168 | export {
169 | useFormField,
170 | Form,
171 | FormItem,
172 | FormLabel,
173 | FormControl,
174 | FormDescription,
175 | FormMessage,
176 | FormField,
177 | };
178 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as LabelPrimitive from "@radix-ui/react-label";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
9 | );
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ));
22 | Label.displayName = LabelPrimitive.Root.displayName;
23 |
24 | export { Label };
25 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PopoverPrimitive from "@radix-ui/react-popover";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Popover = PopoverPrimitive.Root;
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger;
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ));
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
28 |
29 | export { Popover, PopoverTrigger, PopoverContent };
30 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster as Sonner } from "sonner";
2 |
3 | type ToasterProps = React.ComponentProps;
4 |
5 | const Toaster = ({ ...props }: ToasterProps) => {
6 | return (
7 |
22 | );
23 | };
24 |
25 | export { Toaster };
26 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ));
17 | Table.displayName = "Table";
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ));
25 | TableHeader.displayName = "TableHeader";
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ));
37 | TableBody.displayName = "TableBody";
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className,
48 | )}
49 | {...props}
50 | />
51 | ));
52 | TableFooter.displayName = "TableFooter";
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ));
67 | TableRow.displayName = "TableRow";
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ));
82 | TableHead.displayName = "TableHead";
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ));
94 | TableCell.displayName = "TableCell";
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ));
106 | TableCaption.displayName = "TableCaption";
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | };
118 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | },
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/src/npm-flect/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider;
7 |
8 | const Tooltip = TooltipPrimitive.Root;
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger;
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ));
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
29 |
--------------------------------------------------------------------------------
/src/npm-flect/src/contexts/action-resolver.tsx:
--------------------------------------------------------------------------------
1 | import { AnyActionProps } from "@/types";
2 | import { ReactNode, createContext, useEffect, useState } from "react";
3 |
4 | export interface ActionResolver {
5 | package: string;
6 | (props: AnyActionProps): (() => void) | null;
7 | }
8 |
9 | export interface ActionResolverContextState {
10 | resolvers: { [resolverName: string]: ActionResolver };
11 | registerResolver: (resolver: ActionResolver) => void;
12 | }
13 |
14 | const defaultContextValue: ActionResolverContextState = {
15 | resolvers: {},
16 | registerResolver: () => {},
17 | };
18 |
19 | export const ActionResolverContext =
20 | createContext(defaultContextValue);
21 |
22 | export const ActionResolverProvider: React.FC<{
23 | children: ReactNode;
24 | resolver: ActionResolver;
25 | }> = ({ children, resolver }) => {
26 | const [resolvers, setResolvers] = useState<{
27 | [resolverName: string]: ActionResolver;
28 | }>({});
29 |
30 | const registerResolver = (resolver: ActionResolver) => {
31 | const resolverName = resolver.package;
32 | if (!resolverName) {
33 | console.warn(
34 | "Resolver function is anonymous and cannot be registered. Please provide a named function.",
35 | );
36 | return;
37 | }
38 | setResolvers((prevResolvers) => ({
39 | ...prevResolvers,
40 | [resolverName]: resolver,
41 | }));
42 | };
43 |
44 | useEffect(() => {
45 | registerResolver(resolver);
46 | }, [resolver]);
47 |
48 | return (
49 |
50 | {children}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/npm-flect/src/contexts/component-resolver.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useEffect,
4 | useState,
5 | ReactNode,
6 | useContext,
7 | } from "react";
8 | import { AnyComponentProps } from "@/types";
9 |
10 | export interface ComponentResolver {
11 | package: string;
12 | (props: AnyComponentProps): JSX.Element | null;
13 | }
14 |
15 | interface ComponentResolverContextState {
16 | resolvers: { [resolverName: string]: ComponentResolver };
17 | registerResolver: (resolver: ComponentResolver) => void;
18 | }
19 |
20 | export const ComponentResolverContext = createContext<
21 | ComponentResolverContextState | undefined
22 | >(undefined);
23 |
24 | export const ComponentResolverProvider: React.FC<{
25 | children: ReactNode;
26 | resolver: ComponentResolver;
27 | }> = ({ children, resolver }) => {
28 | const context = useContext(ComponentResolverContext);
29 | const initialState = context
30 | ? { ...context.resolvers, [resolver.package]: resolver }
31 | : { [resolver.package]: resolver };
32 |
33 | const [resolvers, setResolvers] = useState<{
34 | [resolverName: string]: ComponentResolver;
35 | }>(initialState);
36 |
37 | const registerResolver = (resolver: ComponentResolver) => {
38 | const resolverName = resolver.package;
39 | if (!resolverName) {
40 | console.warn(
41 | "Resolver function is anonymous and cannot be registered. Please provide a named function.",
42 | );
43 | return;
44 | }
45 | setResolvers((prevResolvers) => ({
46 | ...prevResolvers,
47 | [resolverName]: resolver,
48 | }));
49 | };
50 |
51 | useEffect(() => {
52 | registerResolver(resolver);
53 | }, [resolver]);
54 | return (
55 |
56 | {children}
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/npm-flect/src/contexts/config.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export interface ConfigContextState {
4 | debug?: boolean;
5 | }
6 |
7 | const defaultConfig: ConfigContextState = {
8 | debug: false,
9 | };
10 |
11 | export const ConfigContext = createContext(defaultConfig);
12 |
13 | export const ConfigProvider: React.FC<{
14 | children: React.ReactNode;
15 | config: ConfigContextState;
16 | }> = ({ children, config }) => {
17 | return (
18 | {children}
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/npm-flect/src/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 5.9% 10%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark {
30 | --background: 240 10% 3.9%;
31 | --foreground: 0 0% 98%;
32 | --card: 240 10% 3.9%;
33 | --card-foreground: 0 0% 98%;
34 | --popover: 240 10% 3.9%;
35 | --popover-foreground: 0 0% 98%;
36 | --primary: 0 0% 98%;
37 | --primary-foreground: 240 5.9% 10%;
38 | --secondary: 240 3.7% 15.9%;
39 | --secondary-foreground: 0 0% 98%;
40 | --muted: 240 3.7% 15.9%;
41 | --muted-foreground: 240 5% 64.9%;
42 | --accent: 240 3.7% 15.9%;
43 | --accent-foreground: 0 0% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 0 0% 98%;
46 | --border: 240 3.7% 15.9%;
47 | --input: 240 3.7% 15.9%;
48 | --ring: 240 4.9% 83.9%;
49 | }
50 | }
51 |
52 | @layer base {
53 | * {
54 | @apply border-border;
55 | }
56 | body {
57 | @apply bg-background font-sans text-foreground antialiased;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/npm-flect/src/hooks/use-action.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useContext } from "react";
2 | import { AnyActionProps } from "@/types";
3 | import { resolveAction } from "@/lib/actions";
4 | import { ActionResolverContext } from "@/contexts/action-resolver";
5 |
6 | export function useAction(props: AnyActionProps | undefined) {
7 | const { resolvers } = useContext(ActionResolverContext);
8 | return useCallback(() => {
9 | return resolveAction(resolvers, props)();
10 | }, [resolvers, props]);
11 | }
12 |
--------------------------------------------------------------------------------
/src/npm-flect/src/hooks/use-dispatch-action-listen.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export function useDispatchActionListen(event: string) {
4 | const [dispatched, setDispatched] = useState(false);
5 | useEffect(() => {
6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
7 | function handleEvent(_: Event) {
8 | setDispatched(true);
9 | }
10 |
11 | window.addEventListener(event, handleEvent);
12 | return () => {
13 | window.removeEventListener(event, handleEvent);
14 | };
15 | }, [event]);
16 |
17 | return { dispatched, setDispatched };
18 | }
19 |
--------------------------------------------------------------------------------
/src/npm-flect/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "@/application";
2 | export * from "@/contexts/action-resolver";
3 | export * from "@/contexts/component-resolver";
4 | export type * as types from "@/types";
5 | export * from "@/lib/utils";
6 |
--------------------------------------------------------------------------------
/src/npm-flect/src/lib/actions.ts:
--------------------------------------------------------------------------------
1 | import { toast } from "sonner";
2 | import { AnyActionProps } from "@/types";
3 | import { ActionResolver } from "@/contexts/action-resolver";
4 |
5 | export type NotifyActionProps = {
6 | package: "flect";
7 | type: "notify";
8 | title: string;
9 | description?: string;
10 | position?:
11 | | "top-left"
12 | | "top-right"
13 | | "bottom-left"
14 | | "bottom-right"
15 | | "top-center"
16 | | "bottom-center";
17 | style?:
18 | | "normal"
19 | | "action"
20 | | "success"
21 | | "info"
22 | | "warning"
23 | | "error"
24 | | "loading"
25 | | "default";
26 | };
27 |
28 | export type RedirectActionProps = {
29 | package: "flect";
30 | type: "redirect";
31 | path: string;
32 | };
33 |
34 | export type DispatchEventActionProps = {
35 | package: "flect";
36 | type: "dispatch-event";
37 | event: string;
38 | };
39 |
40 | export function resolveAction(
41 | resolvers: { [resolverName: string]: ActionResolver },
42 | props: AnyActionProps | undefined,
43 | ) {
44 | if (!props) {
45 | return () => {};
46 | }
47 |
48 | const resolver = resolvers[props.package];
49 | if (!resolver) {
50 | throw new Error(
51 | `useAction: No action resolver found for package ${props.package}.`,
52 | );
53 | }
54 |
55 | const resolvedAction = resolver(props);
56 | if (resolvedAction === null) {
57 | throw new Error(
58 | `useAction: Action of type ${props.type} could not be resolved.`,
59 | );
60 | }
61 |
62 | return resolvedAction;
63 | }
64 |
65 | export function notifyAction(props: NotifyActionProps) {
66 | const { title, style, ...rest } = props;
67 | switch (style) {
68 | case "success":
69 | toast.success(title, rest);
70 | break;
71 | case "info":
72 | toast.info(title, rest);
73 | break;
74 | case "warning":
75 | toast.warning(title, rest);
76 | break;
77 | case "error":
78 | toast.error(title, rest);
79 | break;
80 | default:
81 | toast(title, rest);
82 | }
83 | }
84 |
85 | export function redirectAction(props: RedirectActionProps) {
86 | window.location.replace(props.path);
87 | }
88 |
89 | export function dispatchEventAction(props: DispatchEventActionProps) {
90 | window.dispatchEvent(new CustomEvent(props.event));
91 | }
92 |
--------------------------------------------------------------------------------
/src/npm-flect/src/lib/ajv-resolver.ts:
--------------------------------------------------------------------------------
1 | import { Resolver, appendErrors, FieldError } from "react-hook-form";
2 | import Ajv, { DefinedError } from "ajv";
3 | import ajvErrors from "ajv-errors";
4 |
5 | const parseErrorSchema = (
6 | ajvErrors: DefinedError[],
7 | validateAllFieldCriteria: boolean,
8 | ): Record => {
9 | ajvErrors.forEach((error) => {
10 | if (error.keyword === "required") {
11 | error.instancePath += "/" + error.params.missingProperty;
12 | }
13 | });
14 |
15 | return ajvErrors.reduce>((previous, error) => {
16 | const path = error.instancePath.substring(1).replace(/\//g, ".");
17 |
18 | if (!previous[path]) {
19 | previous[path] = {
20 | message: error.message,
21 | type: error.keyword,
22 | };
23 | }
24 |
25 | if (validateAllFieldCriteria) {
26 | const types = previous[path].types;
27 | const messages = types && types[error.keyword];
28 |
29 | previous[path] = appendErrors(
30 | path,
31 | validateAllFieldCriteria,
32 | previous,
33 | error.keyword,
34 | messages
35 | ? ([] as string[]).concat(messages as string[], error.message || "")
36 | : error.message,
37 | ) as FieldError;
38 | }
39 |
40 | return previous;
41 | }, {});
42 | };
43 |
44 | export function ajvResolver(schema: object, addVocabulary: string[]): Resolver {
45 | const ajv = new Ajv({ allErrors: true });
46 | ajv.addVocabulary(addVocabulary);
47 | ajvErrors(ajv);
48 | const validate = ajv.compile(schema);
49 | return async (values, _, options) => {
50 | const valid = validate(values);
51 |
52 | if (valid) {
53 | return { values, errors: {} };
54 | } else {
55 | const errors = parseErrorSchema(
56 | validate.errors as DefinedError[],
57 | options.criteriaMode === "all",
58 | );
59 | return {
60 | values: {},
61 | errors,
62 | };
63 | }
64 | };
65 | }
66 |
--------------------------------------------------------------------------------
/src/npm-flect/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { JSONSchema7 } from "json-schema";
3 | import { FieldValues } from "react-hook-form";
4 | import { twMerge } from "tailwind-merge";
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs));
8 | }
9 |
10 | export function getMetaContent(name: string): string | undefined {
11 | return (
12 | document.querySelector(`meta[name="${name}"]`)?.getAttribute("content") ||
13 | undefined
14 | );
15 | }
16 |
17 | interface Model extends JSONSchema7 {
18 | properties: {
19 | [key: string]: Model;
20 | };
21 | }
22 |
23 | export function getDefaultValues(schema: Model) {
24 | const defaultRow: FieldValues = {};
25 | Object.keys(schema.properties).forEach((key) => {
26 | const prop = schema.properties[key];
27 | if (prop.default !== undefined && prop.default !== null) {
28 | defaultRow[key] = prop.default;
29 | }
30 | });
31 | return defaultRow;
32 | }
33 |
--------------------------------------------------------------------------------
/src/npm-flect/src/types.d.ts:
--------------------------------------------------------------------------------
1 | export type Json =
2 | | string
3 | | number
4 | | boolean
5 | | null
6 | | Json[]
7 | | {
8 | [k: string]: Json;
9 | };
10 |
11 | export type AnyComponentProps =
12 | | AvatarProps
13 | | ButtonProps
14 | | CodeBlockProps
15 | | ContainerProps
16 | | CopyButtonProps
17 | | CustomProps
18 | | DataGridProps
19 | | DeferredFetchProps
20 | | DialogProps
21 | | DisplayProps
22 | | FormProps
23 | | HeadingProps
24 | | LinkProps
25 | | MarkdownProps
26 | | NavLinkProps
27 | | OutletProps
28 | | ParagraphProps
29 | | TableProps
30 | | TextProps;
31 |
32 | export type AnyActionProps =
33 | | NotifyActionProps
34 | | RedirectActionProps
35 | | DispatchEventActionProps;
36 |
37 | export type ActionResponse = {
38 | action: AnyActionProps;
39 | };
40 |
--------------------------------------------------------------------------------
/src/npm-flect/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/npm-flect/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require("tailwindcss/defaultTheme");
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ["class"],
6 | content: ["./src/**/*.{ts,tsx}"],
7 | prefix: "",
8 | safelist: [
9 | "sr-only",
10 | {
11 | pattern: /^(col-span)-/,
12 | },
13 | ],
14 | theme: {
15 | container: {
16 | center: true,
17 | padding: "2rem",
18 | screens: {
19 | "2xl": "1400px",
20 | },
21 | },
22 | extend: {
23 | colors: {
24 | border: "hsl(var(--border))",
25 | input: "hsl(var(--input))",
26 | ring: "hsl(var(--ring))",
27 | background: "hsl(var(--background))",
28 | foreground: "hsl(var(--foreground))",
29 | primary: {
30 | DEFAULT: "hsl(var(--primary))",
31 | foreground: "hsl(var(--primary-foreground))",
32 | },
33 | secondary: {
34 | DEFAULT: "hsl(var(--secondary))",
35 | foreground: "hsl(var(--secondary-foreground))",
36 | },
37 | destructive: {
38 | DEFAULT: "hsl(var(--destructive))",
39 | foreground: "hsl(var(--destructive-foreground))",
40 | },
41 | muted: {
42 | DEFAULT: "hsl(var(--muted))",
43 | foreground: "hsl(var(--muted-foreground))",
44 | },
45 | accent: {
46 | DEFAULT: "hsl(var(--accent))",
47 | foreground: "hsl(var(--accent-foreground))",
48 | },
49 | popover: {
50 | DEFAULT: "hsl(var(--popover))",
51 | foreground: "hsl(var(--popover-foreground))",
52 | },
53 | card: {
54 | DEFAULT: "hsl(var(--card))",
55 | foreground: "hsl(var(--card-foreground))",
56 | },
57 | },
58 | borderRadius: {
59 | lg: "var(--radius)",
60 | md: "calc(var(--radius) - 2px)",
61 | sm: "calc(var(--radius) - 4px)",
62 | },
63 | keyframes: {
64 | "accordion-down": {
65 | from: { height: "0" },
66 | to: { height: "var(--radix-accordion-content-height)" },
67 | },
68 | "accordion-up": {
69 | from: { height: "var(--radix-accordion-content-height)" },
70 | to: { height: "0" },
71 | },
72 | },
73 | animation: {
74 | "accordion-down": "accordion-down 0.2s ease-out",
75 | "accordion-up": "accordion-up 0.2s ease-out",
76 | },
77 | fontFamily: {
78 | Inter: ["Inter"],
79 | sans: ['"Inter"', ...defaultTheme.fontFamily.sans],
80 | },
81 | },
82 | },
83 | plugins: [
84 | require("tailwindcss-animate"),
85 | require("@tailwindcss/typography"),
86 | require("@thoughtbot/tailwindcss-aria-attributes"),
87 | ],
88 | };
89 |
--------------------------------------------------------------------------------
/src/npm-flect/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | }
8 | },
9 | "include": ["src"],
10 | "references": [{ "path": "./tsconfig.node.json" }]
11 | }
12 |
--------------------------------------------------------------------------------
/src/npm-flect/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/npm-flect/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig } from "vite";
3 | import dts from "vite-plugin-dts";
4 | import { libInjectCss } from "vite-plugin-lib-inject-css";
5 |
6 | export default defineConfig(({ mode }) => {
7 | return {
8 | plugins: [dts(), libInjectCss()],
9 | define: {
10 | "process.env.NODE_ENV": JSON.stringify(mode),
11 | },
12 | resolve: {
13 | alias: {
14 | "@": path.resolve(__dirname, "./src"),
15 | },
16 | },
17 | build: {
18 | lib: {
19 | entry: {
20 | index: path.resolve(__dirname, "src/index.ts"),
21 | components: path.resolve(__dirname, "src/components/index.ts"),
22 | },
23 | name: "flect",
24 | },
25 | rollupOptions: {
26 | external: ["react", "react-dom"],
27 | output: {
28 | globals: {
29 | react: "React",
30 | "react-dom": "ReactDOM",
31 | },
32 | },
33 | },
34 | },
35 | };
36 | });
37 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/__init__.py:
--------------------------------------------------------------------------------
1 | from flect.application import flect
2 | from flect.component import components, data_grid, display, form
3 | from flect.response import ActionResponse, PageResponse
4 | from flect.version import VERSION
5 |
6 | __version__ = VERSION
7 |
8 | __all__ = [
9 | "flect",
10 | "PageResponse",
11 | "ActionResponse",
12 | "components",
13 | "data_grid",
14 | "display",
15 | "form",
16 | ]
17 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/actions.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated, Literal, Optional, Union
2 |
3 | from pydantic import AliasGenerator, BaseModel, ConfigDict, Field
4 | from pydantic.alias_generators import to_camel
5 |
6 |
7 | class BaseAction(BaseModel):
8 | package: Literal["flect"] = "flect"
9 | model_config = ConfigDict(extra="forbid", alias_generator=AliasGenerator(serialization_alias=to_camel))
10 |
11 |
12 | class DispatchEvent(BaseAction):
13 | type: Literal["dispatch-event"] = "dispatch-event"
14 | event: str = Field(..., min_length=1, description="The event to dispatch.")
15 |
16 |
17 | class Notify(BaseAction):
18 | type: Literal["notify"] = "notify"
19 |
20 | title: str = Field(..., description="The title of the notification.")
21 | description: Optional[str] = Field(default=None, description="The description of the notification.")
22 | position: Optional[
23 | Literal["top-left", "top-right", "bottom-left", "bottom-right", "top-center", "bottom-center"]
24 | ] = Field(default=None, description="Position of the toast.")
25 | style: Optional[Literal["normal", "action", "success", "info", "warning", "error", "loading", "default"]] = Field(
26 | default=None, description="Type of the toast."
27 | )
28 |
29 |
30 | class Redirect(BaseAction):
31 | type: Literal["redirect"] = "redirect"
32 | path: str = Field(..., description="The path to redirect to.")
33 |
34 |
35 | AnyAction = Annotated[
36 | Union[DispatchEvent, Notify, Redirect],
37 | Field(discriminator="type"),
38 | ]
39 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/application.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import pathlib
3 | from types import ModuleType
4 | from typing import Any, Optional
5 |
6 | from fastapi import FastAPI
7 | from fastapi.staticfiles import StaticFiles
8 |
9 | from flect.routing.server import get_app_router
10 |
11 |
12 | class flect(FastAPI):
13 | def __init__(
14 | self,
15 | app: ModuleType,
16 | prebuilt_uri: Optional[str] = None,
17 | **kwargs: Any,
18 | ) -> None:
19 | super().__init__(**kwargs)
20 | self.app_module = app
21 | self.prebuilt_uri = self.validate_prebuilt_uri(prebuilt_uri)
22 | self.reload_event = asyncio.Event()
23 | self.setup_flect()
24 |
25 | def setup_flect(self) -> None:
26 | self.include_router(get_app_router(self.app_module, self.prebuilt_uri), tags=["flect"])
27 |
28 | def validate_prebuilt_uri(self, prebuilt_uri: Optional[str]) -> Optional[str]:
29 | if prebuilt_uri is not None:
30 | if not prebuilt_uri.startswith("http"):
31 | self.mount("/static", StaticFiles(directory=pathlib.Path(prebuilt_uri)), name="static")
32 | prebuilt_uri = "/static"
33 | return prebuilt_uri
34 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/component/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/src/flect/component/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/src/flect/component/data_grid.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Literal, Optional
2 |
3 | from pydantic import BaseModel, Field
4 | from typing_extensions import Unpack
5 |
6 | from flect.types import FieldKwargs
7 |
8 | __all__ = "Combobox", "ComboboxOption", "Input", "Select"
9 |
10 |
11 | class ComboboxOption(BaseModel):
12 | label: str
13 | value: Any
14 |
15 |
16 | def Combobox(
17 | editable: bool = True,
18 | placeholder: Optional[str] = None,
19 | options: Optional[list[ComboboxOption]] = None,
20 | *,
21 | class_name: Optional[str] = None,
22 | **kwargs: Unpack[FieldKwargs],
23 | ) -> Any:
24 | return Field(
25 | json_schema_extra={
26 | "fieldType": "combobox",
27 | "editable": editable,
28 | "className": class_name,
29 | "attrs": {
30 | "placeholder": placeholder,
31 | "options": [option.model_dump() for option in options] if options else None,
32 | },
33 | },
34 | **kwargs,
35 | )
36 |
37 |
38 | def Input(
39 | editable: bool = True,
40 | type: Literal["text", "email"] = "text",
41 | placeholder: Optional[str] = None,
42 | *,
43 | class_name: Optional[str] = None,
44 | **kwargs: Unpack[FieldKwargs],
45 | ) -> Any:
46 | return Field(
47 | json_schema_extra={
48 | "fieldType": "input",
49 | "editable": editable,
50 | "className": class_name,
51 | "attrs": {
52 | "type": type,
53 | "placeholder": placeholder,
54 | },
55 | },
56 | **kwargs,
57 | )
58 |
59 |
60 | def Select(
61 | editable: bool = True,
62 | placeholder: Optional[str] = None,
63 | *,
64 | class_name: Optional[str] = None,
65 | **kwargs: Unpack[FieldKwargs],
66 | ) -> Any:
67 | return Field(
68 | json_schema_extra={
69 | "fieldType": "select",
70 | "editable": editable,
71 | "className": class_name,
72 | "attrs": {
73 | "placeholder": placeholder,
74 | },
75 | },
76 | **kwargs,
77 | )
78 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/component/display.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from pydantic import Field
4 | from typing_extensions import Unpack
5 |
6 | from flect.types import FieldKwargs
7 |
8 | __all__ = "TextDisplay", "BooleanDisplay"
9 |
10 |
11 | def TextDisplay(
12 | **kwargs: Unpack[FieldKwargs],
13 | ) -> Any:
14 | return Field(
15 | json_schema_extra={
16 | "displayType": "text",
17 | },
18 | **kwargs,
19 | )
20 |
21 |
22 | def BooleanDisplay(
23 | **kwargs: Unpack[FieldKwargs],
24 | ) -> Any:
25 | return Field(
26 | json_schema_extra={
27 | "displayType": "boolean",
28 | },
29 | **kwargs,
30 | )
31 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/component/form.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Literal, Optional
2 |
3 | from pydantic import Field
4 | from typing_extensions import Unpack
5 |
6 | from flect.types import FieldKwargs
7 |
8 | __all__ = "Checkbox", "Input", "Select", "Textarea"
9 |
10 |
11 | def Checkbox(
12 | *,
13 | class_name: Optional[str] = None,
14 | **kwargs: Unpack[FieldKwargs],
15 | ) -> Any:
16 | return Field(
17 | json_schema_extra={
18 | "fieldType": "checkbox",
19 | "className": class_name,
20 | },
21 | **kwargs,
22 | )
23 |
24 |
25 | def Input(
26 | type: Literal["text", "password", "email"] = "text",
27 | placeholder: Optional[str] = None,
28 | *,
29 | class_name: Optional[str] = None,
30 | **kwargs: Unpack[FieldKwargs],
31 | ) -> Any:
32 | return Field(
33 | json_schema_extra={
34 | "fieldType": "input",
35 | "className": class_name,
36 | "attrs": {
37 | "type": type,
38 | "placeholder": placeholder,
39 | },
40 | },
41 | **kwargs,
42 | )
43 |
44 |
45 | def Select(
46 | *,
47 | class_name: Optional[str] = None,
48 | placeholder: Optional[str] = None,
49 | **kwargs: Unpack[FieldKwargs],
50 | ) -> Any:
51 | return Field(
52 | json_schema_extra={
53 | "fieldType": "select",
54 | "className": class_name,
55 | "attrs": {
56 | "placeholder": placeholder,
57 | },
58 | },
59 | **kwargs,
60 | )
61 |
62 |
63 | def Textarea(
64 | rows: Optional[int] = None,
65 | cols: Optional[int] = None,
66 | placeholder: Optional[str] = None,
67 | *,
68 | class_name: Optional[str] = None,
69 | **kwargs: Unpack[FieldKwargs],
70 | ) -> Any:
71 | return Field(
72 | json_schema_extra={
73 | "fieldType": "textarea",
74 | "className": class_name,
75 | "attrs": {
76 | "rows": rows,
77 | "cols": cols,
78 | "placeholder": placeholder,
79 | },
80 | },
81 | **kwargs,
82 | )
83 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/config.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class Config(BaseModel):
5 | debug: bool = False
6 |
7 | def render_to_html(self) -> str:
8 | html = ""
9 |
10 | debug_string = "true" if self.debug else ""
11 | html += f''
12 | return html
13 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/constants.py:
--------------------------------------------------------------------------------
1 | EXCLUDED_FOLDER_NAMES = {"__pycache__"}
2 |
3 | PAGE_ROUTE_FILENAME = "page.py"
4 | LAYOUT_ROUTE_FILENAME = "layout.py"
5 | API_ROUTE_FILENAME = "route.py"
6 |
7 | DYNAMIC_ROUTE_PREFIX = "dynamic__"
8 | GROUP_ROUTE_PREFIX = "group__"
9 | ROOT_ROUTE_PREFIX = "/flect"
10 |
11 | LAYOUT_ROUTE_SUFFIX = "_layout/"
12 |
13 | NAVIGATION_ROUTE_PATH = "/_route/"
14 |
15 | API_ROUTE_METHODS = {"GET", "POST"}
16 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/src/flect/py.typed
--------------------------------------------------------------------------------
/src/python-flect/src/flect/response.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from flect.actions import AnyAction
6 | from flect.component.components import AnyComponent
7 | from flect.head import Head
8 |
9 |
10 | class PageResponse(BaseModel):
11 | head: Optional[Head] = Field(default=None, description="The head of the response.")
12 | body: Optional[AnyComponent] = Field(default=None, description="The components of the response.")
13 |
14 |
15 | class ActionResponse(BaseModel):
16 | action: AnyAction
17 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/routing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/src/flect/routing/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/src/flect/routing/sort.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from flect.constants import GROUP_ROUTE_PREFIX, LAYOUT_ROUTE_SUFFIX
4 |
5 |
6 | def path_priority(path: str) -> tuple:
7 | """
8 | Calculate the priority of a route path based on several weighted factors.
9 |
10 | Parameters:
11 | - path (str): The route path for which to calculate the priority.
12 |
13 | Returns:
14 | - tuple: A tuple representing the path's priority. Lower values indicate higher priority. The factors are:
15 | 1. catch_all_weight: Infinity if the path includes a catch-all pattern, indicating it should be matched last. Otherwise, 0.
16 | 2. depth_weight: The depth of the path, with deeper paths considered more specific and thus lower priority.
17 | 3. segment_weight: The last valid segment of the path, used for sorting when depths are equal.
18 | 4. layout_weight: 1 if the path is for a layout route, 0 otherwise. Layout routes typically have lower priority.
19 | 5. dynamic_weight: 1 if the path is for a dynamic route, 0 otherwise. Dynamic routes typically have lower priority.
20 | """
21 |
22 | # Normalize path for processing
23 | normalized_path = re.sub(rf"{GROUP_ROUTE_PREFIX}[^/]+/", "", path)
24 | if normalized_path.endswith(LAYOUT_ROUTE_SUFFIX):
25 | normalized_path = normalized_path[: -len(LAYOUT_ROUTE_SUFFIX)]
26 |
27 | parts = normalized_path.strip("/").split("/")
28 |
29 | # Calculate weights
30 | catch_all_weight = float("inf") if "{path:path}" in normalized_path else 0
31 | depth_weight = len(parts)
32 | segment_weight = parts[-1] if parts else ""
33 | layout_weight = 1 if path.endswith(LAYOUT_ROUTE_SUFFIX) else 0
34 | dynamic_weight = 1 if path.endswith("}/") else 0
35 |
36 | return catch_all_weight, depth_weight, segment_weight, layout_weight, dynamic_weight
37 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/sitemap.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Literal, Optional
2 | from xml.dom.minidom import parseString
3 | from xml.etree.ElementTree import Element, SubElement, tostring
4 |
5 | from pydantic import BaseModel, Field
6 |
7 | if TYPE_CHECKING:
8 | from flect.routing.client import ClientRoute
9 |
10 |
11 | class Sitemap(BaseModel):
12 | url: str
13 | last_modified: Optional[str]
14 | change_frequency: Optional[Literal["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]]
15 | priority: Optional[float] = Field(default=None, ge=0.0, le=1.0)
16 |
17 |
18 | async def get_sitemap_objs(routes: list["ClientRoute"]) -> list[Sitemap]:
19 | sitemaps = []
20 | for route in routes:
21 | if route.is_page:
22 | if route.sitemap:
23 | sitemaps.extend(await route.sitemap(route.path))
24 | elif not route.is_dynamic:
25 | sitemaps.append(
26 | Sitemap(url=route.loader_path, last_modified=None, change_frequency=None, priority=None)
27 | )
28 | sitemaps.extend(await get_sitemap_objs(route.children))
29 | return sorted(sitemaps, key=lambda sitemap: sitemap.url)
30 |
31 |
32 | async def generate_sitemap_xml(
33 | routes: list["ClientRoute"],
34 | base_url: str,
35 | ) -> str:
36 | sitemap_objs = await get_sitemap_objs(routes)
37 | sitemap = Element("urlset")
38 | sitemap.set("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")
39 |
40 | for sitemap_obj in sitemap_objs:
41 | url_element = SubElement(sitemap, "url")
42 |
43 | loc = SubElement(url_element, "loc")
44 | loc.text = f"{base_url}{sitemap_obj.url}"
45 |
46 | if sitemap_obj.last_modified:
47 | lastmod = SubElement(url_element, "lastmod")
48 | lastmod.text = sitemap_obj.last_modified
49 |
50 | if sitemap_obj.change_frequency:
51 | change_freq = SubElement(url_element, "changefreq")
52 | change_freq.text = sitemap_obj.change_frequency
53 |
54 | if sitemap_obj.priority is not None:
55 | priority = SubElement(url_element, "priority")
56 | priority.text = str(sitemap_obj.priority)
57 |
58 | return parseString(tostring(sitemap, encoding="utf-8")).toprettyxml()
59 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/types.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated, Any, Callable, Literal, Optional, TypedDict, Union
2 |
3 | import pydantic
4 | from pydantic import AliasChoices, AliasPath, types
5 | from pydantic_core import core_schema
6 |
7 |
8 | class JsonDataSchema:
9 | @staticmethod
10 | def __get_pydantic_json_schema__(
11 | _core_schema: core_schema.CoreSchema, handler: pydantic.GetJsonSchemaHandler
12 | ) -> Any:
13 | json_data_schema = core_schema.union_schema(
14 | [
15 | core_schema.str_schema(),
16 | core_schema.float_schema(),
17 | core_schema.bool_schema(),
18 | core_schema.none_schema(),
19 | core_schema.list_schema(core_schema.definition_reference_schema("JsonData")),
20 | core_schema.dict_schema(core_schema.str_schema(), core_schema.definition_reference_schema("JsonData")),
21 | ],
22 | ref="JsonData",
23 | )
24 | return handler(json_data_schema)
25 |
26 |
27 | JsonData = Annotated[Any, JsonDataSchema()]
28 |
29 |
30 | class FieldKwargs(TypedDict, total=False):
31 | default_factory: Optional[Callable[[], Any]]
32 | alias: Optional[str]
33 | alias_priority: Optional[int]
34 | validation_alias: Optional[Union[str, AliasPath, AliasChoices]]
35 | serialization_alias: Optional[str]
36 | title: Optional[str]
37 | description: Optional[str]
38 | examples: Optional[list[Any]]
39 | exclude: Optional[bool]
40 | discriminator: Optional[Union[str, types.Discriminator]]
41 | frozen: Optional[bool]
42 | validate_default: Optional[bool]
43 | repr: bool
44 | init: Optional[bool]
45 | init_var: Optional[bool]
46 | kw_only: Optional[bool]
47 | pattern: Optional[str]
48 | strict: Optional[bool]
49 | gt: Optional[float]
50 | ge: Optional[float]
51 | lt: Optional[float]
52 | le: Optional[float]
53 | multiple_of: Optional[float]
54 | allow_inf_nan: Optional[bool]
55 | max_digits: Optional[int]
56 | decimal_places: Optional[int]
57 | min_length: Optional[int]
58 | max_length: Optional[int]
59 | union_mode: Literal["smart", "left_to_right"]
60 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/utils.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 | import pathlib
3 | from functools import lru_cache
4 | from types import ModuleType
5 |
6 |
7 | @lru_cache
8 | def load_module(file_path: pathlib.Path) -> ModuleType:
9 | """
10 | Loads a Python module from a given file path.
11 |
12 | Parameters
13 | ----------
14 | file_path : pathlib.Path
15 | The file path of the module to load.
16 |
17 | Returns
18 | -------
19 | ModuleType
20 | The loaded module.
21 | """
22 | spec = importlib.util.spec_from_file_location(file_path.stem, str(file_path))
23 | if spec and spec.loader:
24 | module = importlib.util.module_from_spec(spec)
25 | spec.loader.exec_module(module)
26 | return module
27 | raise ImportError(f"Could not load module from {file_path}")
28 |
--------------------------------------------------------------------------------
/src/python-flect/src/flect/version.py:
--------------------------------------------------------------------------------
1 | import importlib.metadata
2 |
3 | VERSION = importlib.metadata.version("flect")
4 |
--------------------------------------------------------------------------------
/src/python-flect/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/tests/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/tests/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/tests/app/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/tests/app/layout.py:
--------------------------------------------------------------------------------
1 | from flect import PageResponse
2 | from flect import components as c
3 |
4 | from tests.app.utils import get_relative_path_text
5 |
6 |
7 | async def layout(outlet: c.AnyComponent = c.Outlet()) -> PageResponse:
8 | return PageResponse(body=c.Container(children=[c.Text(text=get_relative_path_text(__file__)), outlet]))
9 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/page.py:
--------------------------------------------------------------------------------
1 | from flect import PageResponse
2 | from flect import components as c
3 |
4 | from tests.app.utils import get_relative_path_text
5 |
6 |
7 | async def page() -> PageResponse:
8 | return PageResponse(
9 | body=c.Button(children=[c.Text(text=get_relative_path_text(__file__))]),
10 | )
11 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/tests/app/segment1/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/dynamic__segment_id/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/tests/app/segment1/dynamic__segment_id/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/dynamic__segment_id/page.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 |
3 | from fastapi import Path
4 | from flect import PageResponse
5 | from flect import components as c
6 |
7 | from tests.app.utils import get_relative_path_text
8 |
9 |
10 | async def page(segment_id: Annotated[int, Path(..., description="The ID of the dynamic segment")]) -> PageResponse:
11 | return PageResponse(body=c.Button(children=[c.Text(text=get_relative_path_text(__file__))]))
12 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/dynamic__segment_id/route.py:
--------------------------------------------------------------------------------
1 | from flect import ActionResponse, actions
2 |
3 | from tests.app.utils import get_relative_path_text
4 |
5 |
6 | async def post() -> ActionResponse:
7 | return ActionResponse(action=actions.Redirect(path=get_relative_path_text(__file__)))
8 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/group__segment2/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/tests/app/segment1/group__segment2/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/group__segment2/layout.py:
--------------------------------------------------------------------------------
1 | from flect import PageResponse
2 | from flect import components as c
3 |
4 | from tests.app.utils import get_relative_path_text
5 |
6 |
7 | async def layout(outlet: c.AnyComponent = c.Outlet()) -> PageResponse:
8 | return PageResponse(body=c.Container(children=[c.Text(text=get_relative_path_text(__file__)), outlet]))
9 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/group__segment2/segment3/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/tests/app/segment1/group__segment2/segment3/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/group__segment2/segment3/page.py:
--------------------------------------------------------------------------------
1 | from flect import PageResponse
2 | from flect import components as c
3 |
4 | from tests.app.utils import get_relative_path_text
5 |
6 |
7 | async def page() -> PageResponse:
8 | return PageResponse(body=c.Button(children=[c.Text(text=get_relative_path_text(__file__))]))
9 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/group__segment2/segment3/route.py:
--------------------------------------------------------------------------------
1 | from flect import ActionResponse, actions
2 |
3 | from tests.app.utils import get_relative_path_text
4 |
5 |
6 | async def post() -> ActionResponse:
7 | return ActionResponse(action=actions.Redirect(path=get_relative_path_text(__file__)))
8 |
9 |
10 | async def get() -> ActionResponse:
11 | return ActionResponse(action=actions.Redirect(path=get_relative_path_text(__file__)))
12 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/layout.py:
--------------------------------------------------------------------------------
1 | from flect import PageResponse
2 | from flect import components as c
3 |
4 | from tests.app.utils import get_relative_path_text
5 |
6 |
7 | async def layout(outlet: c.AnyComponent = c.Outlet()) -> PageResponse:
8 | return PageResponse(body=c.Container(children=[c.Text(text=get_relative_path_text(__file__)), outlet]))
9 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/page.py:
--------------------------------------------------------------------------------
1 | from flect import PageResponse
2 | from flect import components as c
3 |
4 | from tests.app.utils import get_relative_path_text
5 |
6 |
7 | async def page() -> PageResponse:
8 | return PageResponse(body=c.Button(children=[c.Text(text=get_relative_path_text(__file__))]))
9 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/segment2/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/tests/app/segment1/segment2/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/segment2/page.py:
--------------------------------------------------------------------------------
1 | from flect import PageResponse
2 | from flect import components as c
3 |
4 | from tests.app.utils import get_relative_path_text
5 |
6 |
7 | async def page() -> PageResponse:
8 | return PageResponse(body=c.Button(children=[c.Text(text=get_relative_path_text(__file__))]))
9 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/segment1/segment2/route.py:
--------------------------------------------------------------------------------
1 | from flect import ActionResponse
2 | from flect import actions as a
3 |
4 | from tests.app.utils import get_relative_path_text
5 |
6 |
7 | async def post() -> ActionResponse:
8 | return ActionResponse(action=a.Redirect(path=get_relative_path_text(__file__)))
9 |
--------------------------------------------------------------------------------
/src/python-flect/tests/app/utils.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 | APP_PATH = pathlib.Path(__file__).parent
4 |
5 |
6 | def get_relative_path_text(file: str) -> str:
7 | file_path = pathlib.Path(file)
8 | relative_path = file_path.relative_to(APP_PATH)
9 | return convert_path_to_regex_str(str(relative_path))
10 |
11 |
12 | def convert_path_to_regex_str(path: str) -> str:
13 | return f"^{path}$"
14 |
--------------------------------------------------------------------------------
/src/python-flect/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 | import pytest
4 | from fastapi import FastAPI
5 | from flect.routing.client import get_client_routes
6 | from flect.routing.server import generate_loader_routes
7 | from flect.routing.sort import path_priority
8 | from starlette.testclient import TestClient
9 |
10 | # from flect.routing import get_client_loader_router, get_client_routes
11 | from tests import app as test_app_module
12 |
13 | APP_FOLDER = pathlib.Path(test_app_module.__file__).parent
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def app_module():
18 | return test_app_module
19 |
20 |
21 | @pytest.fixture(scope="session")
22 | def app_folder():
23 | return APP_FOLDER
24 |
25 |
26 | @pytest.fixture(scope="session")
27 | def client_routes(app_folder):
28 | routes = get_client_routes(app_folder)
29 | return routes
30 |
31 |
32 | @pytest.fixture(scope="session")
33 | def loader_routes(client_routes):
34 | loader_routes = generate_loader_routes(client_routes)
35 | return sorted(loader_routes, key=lambda r: path_priority(r.path))
36 |
37 |
38 | @pytest.fixture
39 | def app():
40 | return FastAPI()
41 |
42 |
43 | @pytest.fixture
44 | def client(app):
45 | return TestClient(app)
46 |
--------------------------------------------------------------------------------
/src/python-flect/tests/test_head.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from flect.head import TitleTemplate, merge_parent_title
3 |
4 |
5 | @pytest.mark.parametrize(
6 | ("parent_title", "child_title", "expected"),
7 | [
8 | (None, TitleTemplate(default="title", absolute=False), TitleTemplate(default="title", absolute=False)),
9 | (
10 | TitleTemplate(default="title", absolute=False),
11 | TitleTemplate(default="title", absolute=False),
12 | TitleTemplate(default="title - title", absolute=False),
13 | ),
14 | (
15 | TitleTemplate(template="{title}-title", default="title", absolute=False),
16 | TitleTemplate(default="title", absolute=False),
17 | TitleTemplate(default="title-title", absolute=False),
18 | ),
19 | (
20 | TitleTemplate(template="{title}-title", default="title", absolute=False),
21 | TitleTemplate(default="title", absolute=True),
22 | TitleTemplate(default="title", absolute=True),
23 | ),
24 | ],
25 | )
26 | def test_merge_parent_title(parent_title, child_title, expected):
27 | title = merge_parent_title(
28 | parent_title,
29 | child_title,
30 | )
31 |
32 | assert title == expected
33 |
--------------------------------------------------------------------------------
/src/python-flect/tests/test_render.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import pytest
4 | from fastapi import FastAPI, Request
5 | from flect import PageResponse
6 | from flect import components as c
7 | from flect.render import generate_html, get_route_response, render_server_side_html
8 |
9 | from tests.app.utils import convert_path_to_regex_str
10 |
11 |
12 | def test_generate_html():
13 | test_html = "Test HTML"
14 | result = generate_html(test_html)
15 | assert test_html in result
16 | assert "" in result
17 |
18 |
19 | @pytest.fixture()
20 | async def endpoint():
21 | async def route_endpoint() -> PageResponse:
22 | return PageResponse(body=c.Button(children=[c.Text(text="Hello flect!")]))
23 |
24 | return route_endpoint
25 |
26 |
27 | @pytest.fixture()
28 | def app(endpoint):
29 | app = FastAPI()
30 | app.add_api_route("/test", endpoint, methods=["GET"])
31 | return app
32 |
33 |
34 | @pytest.fixture()
35 | def route_request():
36 | return Request(scope={"type": "http", "method": "GET", "path": "/test", "query_string": "", "headers": {}})
37 |
38 |
39 | async def test_get_route_response(app, route_request):
40 | response = await get_route_response(route_request, app.routes[-1])
41 | assert response is not None
42 |
43 |
44 | async def test_resolve_route_response(route_request, client_routes, loader_routes):
45 | response = await render_server_side_html(route_request, client_routes, loader_routes)
46 | assert response is not None
47 |
48 |
49 | @pytest.mark.parametrize(
50 | "request_path, except_response",
51 | [
52 | ("/", {"page.py", "layout.py"}),
53 | ("/segment1/", {"layout.py", "segment1/layout.py", "segment1/page.py"}),
54 | ("/segment1/segment2/", {"layout.py", "segment1/layout.py", "segment1/segment2/page.py"}),
55 | (
56 | "/segment1/segment3/",
57 | {
58 | "layout.py",
59 | "segment1/layout.py",
60 | "segment1/group__segment2/layout.py",
61 | "segment1/group__segment2/segment3/page.py",
62 | },
63 | ),
64 | ("/segment1/0/", {"layout.py", "segment1/layout.py", "segment1/dynamic__segment_id/page.py"}),
65 | ],
66 | )
67 | async def test_render_server_side_html(app_module, client_routes, loader_routes, request_path, except_response):
68 | request = Request(
69 | scope={
70 | "type": "http",
71 | "method": "GET",
72 | "path": request_path,
73 | "query_string": "",
74 | "headers": {},
75 | "path_params": {"path": request_path},
76 | }
77 | )
78 | _, body = await render_server_side_html(
79 | request,
80 | client_routes,
81 | loader_routes,
82 | )
83 | pattern = re.compile(r"(\^.*?\$)", re.MULTILINE)
84 | matches = pattern.findall(body)
85 | except_response = set((convert_path_to_regex_str(r)) for r in except_response)
86 | assert except_response == set(matches)
87 |
--------------------------------------------------------------------------------
/src/python-flect/tests/test_routing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chaoyingz/flect/57970b59f98e5ce729494e8ce2986b4eecac2138/src/python-flect/tests/test_routing/__init__.py
--------------------------------------------------------------------------------
/src/python-flect/tests/test_routing/test_server.py:
--------------------------------------------------------------------------------
1 | from flect.constants import NAVIGATION_ROUTE_PATH, ROOT_ROUTE_PREFIX
2 | from flect.routing.server import (
3 | generate_api_routes,
4 | generate_loader_routes,
5 | generate_navigation_routes,
6 | generate_server_side_render_routes,
7 | generate_sitemap_routes,
8 | get_app_router,
9 | )
10 |
11 |
12 | def test_generate_navigation_routes(client_routes, client):
13 | routes = generate_navigation_routes(client_routes)
14 | route = next(routes, None)
15 | client.app.routes.append(route)
16 | response = client.get(f"{ROOT_ROUTE_PREFIX}{NAVIGATION_ROUTE_PATH}")
17 | assert response.status_code == 200
18 | assert response.json() == [route.model_dump() for route in client_routes]
19 |
20 |
21 | def test_generate_loader_routes(client_routes):
22 | routes = generate_loader_routes(client_routes)
23 | assert list(routes)
24 |
25 |
26 | def test_generate_api_routes(app_folder):
27 | routes = generate_api_routes(app_folder)
28 | routes_list = list(routes)
29 | assert len(routes_list) == 4
30 |
31 |
32 | def test_generate_server_side_render_routes(client_routes, loader_routes, client):
33 | routes = generate_server_side_render_routes(client_routes, loader_routes)
34 | route = next(routes, None)
35 | client.app.routes.append(route)
36 | response = client.get("/")
37 | assert response.status_code == 200
38 | assert response.text
39 |
40 |
41 | def test_generate_sitemap_routes(client_routes, client):
42 | route = generate_sitemap_routes(client_routes)
43 | client.app.routes.append(next(route))
44 | response = client.get("/sitemap.xml")
45 | assert response.status_code == 200
46 | assert response.text
47 |
48 |
49 | def test_get_app_router(app_module):
50 | router = get_app_router(app_module)
51 | assert router.routes
52 |
--------------------------------------------------------------------------------
/src/python-flect/tests/test_routing/test_sort.py:
--------------------------------------------------------------------------------
1 | from flect.routing.sort import path_priority
2 |
3 |
4 | def test_sort_path():
5 | paths = [
6 | "/flect/_layout/",
7 | "/flect/segment1/_layout/",
8 | "/flect/segment1/segment3/",
9 | "/flect/segment1/{segment_id}/",
10 | "/flect/segment1/segment2/",
11 | "/flect/segment1/",
12 | "/flect/",
13 | "{path:path}",
14 | "/flect/segment1/group__segment_id/",
15 | ]
16 |
17 | assert sorted(paths, key=path_priority) == [
18 | "/flect/",
19 | "/flect/_layout/",
20 | "/flect/segment1/",
21 | "/flect/segment1/group__segment_id/",
22 | "/flect/segment1/_layout/",
23 | "/flect/segment1/segment2/",
24 | "/flect/segment1/segment3/",
25 | "/flect/segment1/{segment_id}/",
26 | "{path:path}",
27 | ]
28 |
--------------------------------------------------------------------------------
/src/python-flect/tests/test_sitemap.py:
--------------------------------------------------------------------------------
1 | from flect.sitemap import generate_sitemap_xml, get_sitemap_objs
2 |
3 |
4 | async def test_get_sitemap_objs(client_routes):
5 | sitemaps = await get_sitemap_objs(client_routes)
6 | assert sitemaps
7 |
8 |
9 | async def test_generate_sitemap_xml(client_routes):
10 | sitemap = await generate_sitemap_xml(
11 | client_routes,
12 | "http://localhost:8000",
13 | )
14 | assert sitemap
15 |
--------------------------------------------------------------------------------
/src/python-flect/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from flect.utils import load_module
3 |
4 |
5 | def test_load_module_success(app_folder):
6 | module = load_module(app_folder / "page.py")
7 | assert module is not None
8 | assert hasattr(module, "page")
9 |
10 |
11 | def test_load_module_failure(tmp_path):
12 | with pytest.raises(ImportError):
13 | load_module(tmp_path)
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "forceConsistentCasingInFileNames": true
23 | },
24 | "include": ["src"]
25 | }
26 |
--------------------------------------------------------------------------------