@@ -90,59 +90,55 @@ const fields = inject(injectionKeys.evaluatedFields);
```
-The code above will make _Bubble Message_ available in the Builder.
+The code above will make Bubble Message available in the Builder.
-![Custom Components - Bubble Message](./images/custom-components.bubble-message.png)
+![Custom Components - Bubble Message](/framework/images/custom-components.bubble-message.png)
## Developing templates
### Run a local server
-To get started, clone the [Framework repository](https://github.com/streamsync-cloud/streamsync) from GitHub.
+
+
+ To get started, clone the [Framework repository](https://github.com/streamsync-cloud/streamsync) from GitHub.
+
+
+ To develop custom templates in a developer-friendly way, ensure you have a front-end development server with instant reload capabilities. The front-end code for Framework is located in the `ui` folder. With Node and npm installed on your system, run `npm install` to install dependencies. Then, start the server with support for custom component templates using `npm run custom.dev`.
+ ```sh
+ cd ui
+ npm install
+ # "custom.dev" links templates in "custom_components/"
+ # "dev" runs the server without them
+ npm run custom.dev
+ ```
+
+
+ The command `npm run custom.dev` starts a front-end server, which requires a back-end to function fully. Start Framework via command line, specifying the option `--port 5000`, to provide a back-end on that port. It's recommended to create a new app for testing the template you're developing.
+ ```sh
+ writer create customtester
+ writer edit customtester --port 5000
+ ```
+
+
+ You should now be able to access Framework via the URL provided by Vite, e.g. `http://localhost:5174`. In the Builder's _Toolkit_, you should see the sample component, _Balloon Message_. Add it to your tester application.
+
+
-To develop custom templates, at least in a developer-friendly way, you'll need a front-end development server with instant reloads.
-
-The front-end code for Framework can be found in the folder `ui`. With Node and npm in your system, run `npm install` to install dependencies, and start the server with support for custom component templates using `npm run custom.dev`.
-
-```sh
-cd ui
-npm install
-
-# "custom.dev" links templates in "custom_components/"
-# "dev" runs the server without them
-
-npm run custom.dev
-```
-
-The command above will start a front-end server, but won't be of much use by itself —a back-end is needed. The front-end development server proxies back-end requests to port 5000.
-
-Start Framework via command line, specifying the option `--port 5000`, to provide a back-end in that port. It's recommended to create a new app for testing the template you're developing.
-
-```sh
-writer create customtester
-writer edit customtester --port 5000
-```
-
-You should now be able to access Framework via the URL provided by Vite, e.g. `http://localhost:5174`. In the Builder's _Toolkit_, you should see the sample component, _Balloon Message_. Add it to your tester application.
### Create a new component
-Go to `ui/src/custom_components/` and open the Vue single-file components, i.e. the `.vue` files. These files contain comments that will help you get started. Try editing the provided templates, you should see changes reflected.
-
-
+
You can also have a look at the built-in component templates, since their syntax is equivalent. They can be found in the `ui/src/core_components/` folder.
-
+
+Go to `ui/src/custom_components/` and open the Vue single-file components, i.e. the `.vue` files. These files contain comments that will help you get started. Try editing the provided templates, you should see changes reflected.
You can get started by duplicating one of these examples. Make sure you add the new template to the entrypoint, as discussed below.
### Define entrypoint
-For custom component templates to be taken into account, they need to be accessible from the entrypoint.
-
-Edit `ui/src/custom_components/index.ts` to define which templates you wish to export and under which identifiers.
+For custom component templates to be taken into account, they need to be accessible from the entrypoint. Edit `ui/src/custom_components/index.ts` to define which templates you wish to export and under which identifiers.
```ts
-
// Import the templates
import BubbleMessage from './BubbleMessage.vue';
@@ -160,8 +156,6 @@ A single or multiple templates can be specified. Take into account that they wil
## Bundling templates
-### Pack and collect
-
Execute `npm run custom.build`, this will generate the output `.js` and `.css` files into `ui/custom_components_dist`.
```sh
@@ -171,8 +165,4 @@ Execute `npm run custom.build`, this will generate the output `.js` and `.css` f
npm run custom.build
```
-Collect the files from `ui/custom_components_dist` and pack them in a folder such as `my_custom_bubbles`.
-
-### Try them
-
-The folder containing the generated files, e.g. `my_custom_bubbles`, can now be placed in the `extensions/` folder of any Framework project. It'll be automatically detected during server startup.
\ No newline at end of file
+Collect the files from `ui/custom_components_dist` and pack them in a folder such as `my_custom_bubbles`. The folder containing the generated files, e.g. `my_custom_bubbles`, can now be placed in the `extensions/` folder of any Framework project. It'll be automatically detected during server startup.
\ No newline at end of file
diff --git a/docs/framework/custom-server.mdx b/docs/framework/custom-server.mdx
index 742ea73d..5b2c241a 100644
--- a/docs/framework/custom-server.mdx
+++ b/docs/framework/custom-server.mdx
@@ -28,11 +28,9 @@ asgi_app: FastAPI = writer.serve.app
def hello():
return "1"
```
-
-::: warning Use `server_setup.py` in `edit` mode
-If you want to use in `edit` mode,
-you can launch `writer edit --enable-server-setup
`.
-:::
+
+Use `server_setup.py` in `edit` mode. If you want to use in `edit` mode, you can launch `writer edit --enable-server-setup `.
+
## Implement custom server
@@ -60,11 +58,10 @@ Note the inclusion of the imported `ws_max_size` setting. This is important for
Fine-tuning Uvicorn allows you to set up SSL, configure proxy headers, etc, which can prove vital in complex deployments.
-::: tip Use server setup hook
+Use server setup hook
```python
asgi_app = writer.serve.get_asgi_app(app_path, mode, enable_server_setup=True)
```
-:::
## Multiple apps at once
diff --git a/docs/framework/deploy-with-docker.mdx b/docs/framework/deploy-with-docker.mdx
index 0a1be549..79e8595e 100644
--- a/docs/framework/deploy-with-docker.mdx
+++ b/docs/framework/deploy-with-docker.mdx
@@ -2,21 +2,27 @@
title: "Deploy with Docker"
---
+
+To deploy on the Writer cloud see instructions [here](/framework/quickstart#deploying-on-writer-cloud).
+
+
You can use Docker to deploy Framework anywhere. If you're an experienced Docker user, you may want to go straight to the provided Dockerfile.
## Creating a Docker image
-### Setting up
-
-- Make sure you have Docker installed.
-- Open a terminal and navigate to your app's folder.
-- Create a `pyproject.toml` using `poetry init` and install `writer` using `poetry add writer`
-
-### Creating a Dockerfile
-
-A Dockerfile is a file with instructions that tell Docker how to build your image. It must be named `Dockerfile`.
+
+
+ Make sure you have Docker installed on your system.
+
+
+ Open a terminal and navigate to your app’s folder.
+
+
+ Create a `pyproject.toml` file using `poetry init` and install `writer` using `poetry add writer`.
+
+
-You can use the following as-is, or as a starting point. It should be saved in your app's folder, together with `main.py` and `ui.json`.
+A Dockerfile is a file with instructions that tell Docker how to build your image. It must be named `Dockerfile`. You can use the following as-is, or as a starting point. It should be saved in your app's folder, together with `main.py` and `ui.json`.
```docker
FROM python:3.10-bullseye
@@ -38,17 +44,13 @@ It uses an official Python slim base image with a multistage build to reduce the
If you're a Docker expert, feel free to work on your own `Dockerfile`. Framework is, after all, a standard Python package.
-### Building the Docker image
-
-To build the image, write `docker build . -t ` followed by an image tag, which you're free to choose and will locally identify your image.
+To build the image, use `docker build . -t ` followed by an image tag, which you're free to choose and will locally identify your image.
```sh
docker build . -t my_streamsync_app
```
-Platform considerations
-
By default, Docker builds images in the architecture it's being run on. If you're working with an ARM computer, such as a Mac M2 or a Raspberry Pi, Docker will build an ARM image. Most cloud services will only accept x86 images. You can use another computer (or virtual machine) to build the image, or you can use [Docker buildx](https://docs.docker.com/build/building/multi-platform/).
@@ -83,11 +85,20 @@ Go on your browser to [http://localhost:8080](http://localhost:8080) to check ev
As mentioned earlier, once the image is a registry, it can be spun up by others. After trying a few options, we recommend using Google Cloud Run. Its free tier is generous and SSL works out of the box.
-![Run and Share - Google Cloud Run](./images/deploy-with-docker.google-cloud-run.png)
+![Run and Share - Google Cloud Run](/framework/images/deploy-with-docker.google-cloud-run.png)
Cloud Run can be configured in just one page. It takes the image from a registry and makes it available via a URL, with SSL enabled by default. We recommend the following settings:
-- Minimum 0 instances, maximum 4 instances. Unless your app needs to serve several thousands of users.
-- Request timeout to the maximum allowed and _Session Affinity_ enabled. This ensures that WebSockets connections are not unnecessarily dropped.
-- 2GB of memory and 2 vCPUs. This will likely be enough to comfortably run a simple app. You can probably get away with much less (512MB of memory and 1vCPU), if your app isn't too demanding and you don't expect much traffic.
+
+
+ Minimum 0 instances, maximum 4 instances. This range is suitable unless your app needs to serve several thousands of users.
+
+
+ Set the request timeout to the maximum allowed and enable Session Affinity. This ensures that WebSocket connections are not unnecessarily dropped.
+
+
+ Allocate 2GB of memory and 2 vCPUs. This configuration will likely be enough to comfortably run a simple app. However, you can probably manage with much less—512MB of memory and 1 vCPU—if your app isn’t too demanding and you don’t expect much traffic.
+
+
+
diff --git a/docs/framework/frontend-scripts.mdx b/docs/framework/frontend-scripts.mdx
index b26c5099..bf103190 100644
--- a/docs/framework/frontend-scripts.mdx
+++ b/docs/framework/frontend-scripts.mdx
@@ -28,16 +28,12 @@ initial_state.import_frontend_module("my_script", "/static/mymodule.js")
```
-Use versions to avoid caching
-
-Similarly to stylesheets, your browser may cache modules, preventing updates from being reflected. Append a querystring to invalidate the cache, e.g. use `/static/script.js?3`.
+Use versions to avoid caching. Similarly to stylesheets, your browser may cache modules, preventing updates from being reflected. Append a querystring to invalidate the cache, e.g. use `/static/script.js?3`.
## Writing a module
-The module should be a standard ES6 module and export at least one function, enabling it to be triggered from the back-end. As per JavaScript development best practices, modules should have no side effects.
-
-An example of a module is shown below.
+The module should be a standard ES6 module and export at least one function, enabling it to be triggered from the back-end. As per JavaScript development best practices, modules should have no side effects. An example of a module is shown below.
```js
let i = 0;
@@ -59,7 +55,7 @@ def handle_click(state):
state.call_frontend_function("mymodule", "sendAlert", ["Bob"])
```
-## Import a JavaScript script
+## Import a JS script
Framework can also import and run JavaScript scripts directly, for their side effects. These are imported via the report's `import_script` method. This method takes two arguments. The first, `script_key` is the identifier used to import the script. The second, `path` is the path to the file. The specified path must be available to the front-end, so storing it in your application's `./static` folder is recommended.
@@ -72,12 +68,10 @@ initial_state.import_script("my_script", "/static/script.js")
```
-Prefer ES6 modules
-
-Importing scripts is useful to import libraries that don't support ES6 modules. When possible, use ES6 modules. The `import_script` syntax is only used for side effects; you'll only be able to call functions from the back-end using modules that have been previously imported via `import_frontend_module`.
+Prefer ES6 modules: importing scripts is useful to import libraries that don't support ES6 modules. When possible, use ES6 modules. The `import_script` syntax is only used for side effects; you'll only be able to call functions from the back-end using modules that have been previously imported via `import_frontend_module`.
-## Importing a script or stylesheet from a URL
+## Importing a script from a URL
Framework can also import scripts and stylesheets from URLs. This is useful for importing libraries from CDNs. The `import_script` and `import_stylesheet` methods take a `url` argument, which is the URL to the script or stylesheet.
@@ -93,6 +87,10 @@ initial_state.import_script("lodash", "https://cdnjs.cloudflare.com/ajax/libs/lo
## Frontend core
+
+Effectively using Framework's core can be challenging and will likely entail reading its [source code](https://github.com/streamsync-cloud/streamsync/blob/master/ui/src/core/index.ts). Furthermore, it's considered an internal capability rather than a public API, so it may unexpectedly change between releases.
+
+
You can access Framework's front-end core via `globalThis.core`, unlocking all sorts of functionality. Notably, you can use `getUserState()` to get values from state.
```js
@@ -102,8 +100,3 @@ export function alertHueRotationValue() {
}
```
-
-Here be dragons
-
-Effectively using Framework's core can be challenging and will likely entail reading its [source code](https://github.com/streamsync-cloud/streamsync/blob/master/ui/src/core/index.ts). Furthermore, it's considered an internal capability rather than a public API, so it may unexpectedly change between releases.
-
\ No newline at end of file
diff --git a/docs/framework/handling-inputs.mdx b/docs/framework/handling-inputs.mdx
index 2297973a..5d85797b 100644
--- a/docs/framework/handling-inputs.mdx
+++ b/docs/framework/handling-inputs.mdx
@@ -8,7 +8,7 @@ There are two, complementary, ways to handle inputs in Framework: via event hand
Input components have _change_ events that are dispatched when the value changes. The new value is provided as a payload in the event handler. Change events have slightly different names across components, reflecting the payloads they provide. For example, _Number Input_ and _Slider Input_ use the event `wf-number-change` while _Text Input_ and _Text Area Input_ use the generic `wf-change`.
-As discussed in the [Event handlers](event-handlers.html) section, the payload can be accessed via the `payload` argument in the event handler.
+As discussed in the [Event handlers](/framework/event-handlers) section, the payload can be accessed via the `payload` argument in the event handler.
```py
# This event handler takes the payload and assigns it
@@ -23,7 +23,7 @@ Writing event handlers for every input component can be tedious. In most cases,
Bindings automatically handle the _change_ event for the component and set the value of the state element to the payload. Furthermore, bindings are two-way. If the state element is updated from the back-end, the front-end component is updated to reflect the new value.
-As mentioned in the [Builder basics](builder-basics.html) section of the guide, bindings can be configured in the component settings.
+As mentioned in the [Builder basics](/framework/builder-basics) section of the guide, bindings can be configured in the component settings.
![Repeater example](/framework/images/handling-inputs.binding.png)
diff --git a/docs/framework/images/auth_unauthorized_default.png b/docs/framework/images/auth_unauthorized_default.png
index 280ebeb6..cf703877 100644
Binary files a/docs/framework/images/auth_unauthorized_default.png and b/docs/framework/images/auth_unauthorized_default.png differ
diff --git a/docs/framework/images/backend-initiated-actions.notifications.png b/docs/framework/images/backend-initiated-actions.notifications.png
index 4098cc5a..670cd5eb 100644
Binary files a/docs/framework/images/backend-initiated-actions.notifications.png and b/docs/framework/images/backend-initiated-actions.notifications.png differ
diff --git a/docs/framework/images/custom-components.architecture.png b/docs/framework/images/custom-components.architecture.png
index d3571879..e1463c31 100644
Binary files a/docs/framework/images/custom-components.architecture.png and b/docs/framework/images/custom-components.architecture.png differ
diff --git a/docs/framework/images/custom-components.bubble-message.png b/docs/framework/images/custom-components.bubble-message.png
index 7a378f80..d5f2aeb3 100644
Binary files a/docs/framework/images/custom-components.bubble-message.png and b/docs/framework/images/custom-components.bubble-message.png differ
diff --git a/docs/framework/images/custom-components.external.png b/docs/framework/images/custom-components.external.png
index 586a23d1..0f41ece9 100644
Binary files a/docs/framework/images/custom-components.external.png and b/docs/framework/images/custom-components.external.png differ
diff --git a/docs/framework/images/deploy-with-docker.google-cloud-run.png b/docs/framework/images/deploy-with-docker.google-cloud-run.png
index b6909cf1..c7452417 100644
Binary files a/docs/framework/images/deploy-with-docker.google-cloud-run.png and b/docs/framework/images/deploy-with-docker.google-cloud-run.png differ
diff --git a/docs/framework/images/repeater.example.png b/docs/framework/images/repeater.example.png
index 58973b09..5fe5c312 100644
Binary files a/docs/framework/images/repeater.example.png and b/docs/framework/images/repeater.example.png differ
diff --git a/docs/framework/introduction.mdx b/docs/framework/introduction.mdx
index 05f78b83..70fc2a4c 100644
--- a/docs/framework/introduction.mdx
+++ b/docs/framework/introduction.mdx
@@ -2,7 +2,7 @@
title: "Introduction"
---
-The Writer Framework lets you build feature-rich apps by using a drag-and-drop visual editor and writing the back-end code in Python. It’s fast and flexible, with clean, easily testable syntax. It provides separation of concerns between UI and business logic, enabling more complex apps.
+The Writer Framework lets you build feature-rich apps by using a drag-and-drop visual editor called **the Builder** and writing the back-end code in Python. It's fast and flexible, with clean, easy-to-test syntax. It provides separation of concerns between UI and business logic, enabling more complex apps.
![Framework Builder screenshot](/framework/public/builder.png#framework)
@@ -64,6 +64,19 @@ The Writer Framework offers:
-To get started, head to the Quickstart or one of the tutorials, such as the chat assistant tutorial.
+To get started, head to [Quickstart](/framework/quickstart) or our tutorials:
+
+
+
+ Generate multiple social media posts in a click of button using our social media generator.
+
+
+ Using Knowledge Graph, our graph-based RAG solution, you can build chat assistants to quickly ask questions using your data sources.
+
+
+ Build real-time digital shelves for hundreds of products that are automatically customized for different e-retailers.
+
+
+
diff --git a/docs/framework/quickstart.mdx b/docs/framework/quickstart.mdx
index 3b51154a..b3776ed9 100644
--- a/docs/framework/quickstart.mdx
+++ b/docs/framework/quickstart.mdx
@@ -38,9 +38,9 @@ writer create testapp
A Framework app is a folder with the following items.
-1. `main.py`. The entry point for the app. You can import anything you need from here.
-2. `ui.json`. Contains the UI component declarations. Maintained by the , the framework's visual editor.
-3. `static/`. This folder contains front-end static files which you might want to distribute with your app. For example, images and stylesheets.
+1. `main.py` - The entry point for the app. You can import anything you need from here.
+2. `ui.json` - Contains the UI component declarations. Maintained by the , the framework's visual editor.
+3. `static/` - This folder contains front-end static files which you might want to distribute with your app. For example, images and stylesheets.
## Start the editor
@@ -83,11 +83,11 @@ If you need to run Framework as a module, you can use the `writer.command_line`
python -m writer.command_line run my_app
```
-## Writer cloud
+## Deploying on Writer cloud
Writer provides a quick and fast way to deploy your apps via the Writer cloud.
```sh
writer deploy
```
-You’ll be asked to enter your API key. To find your key, log in to your[App Studio account](https://app.writer.com/aistudio) account and either create a new framework app by going through the create app workflow or choose an existing framework app from your home screen. For other deployment options, see[Deploy with Docker](/framework/deploy-with-docker).
\ No newline at end of file
+You’ll be asked to enter your API key. To find your key, log in to your [App Studio account](https://app.writer.com/aistudio) account and either create a new framework app by going through the create app workflow or choose an existing framework app from your home screen. For other deployment options, see [Deploy with Docker](/framework/deploy-with-docker).
\ No newline at end of file
diff --git a/docs/framework/repeater.mdx b/docs/framework/repeater.mdx
index a849914d..261618cf 100644
--- a/docs/framework/repeater.mdx
+++ b/docs/framework/repeater.mdx
@@ -12,7 +12,7 @@ Each iteration is rendered with a different **context**, a dictionary containing
### Food Selector example
-![Repeater example](./images/repeater.example.png)
+![Repeater example](/framework/images/repeater.example.png)
Given the state below, the contents of _Repeater_ will be repeated 3 times. For the first iteration, `itemId` will equal `Banana`, and `item` will equal `{"type": "fruit", "colour": "yellow"}`. Components inside _Repeater_ will be able to access this data using references such as `@{itemId}` and `@{item.type}`.
diff --git a/docs/framework/social-post-generator.mdx b/docs/framework/social-post-generator.mdx
index f9c73b57..ac05dead 100644
--- a/docs/framework/social-post-generator.mdx
+++ b/docs/framework/social-post-generator.mdx
@@ -4,7 +4,7 @@ title: "Social post generator"
In this tutorial, you will use Writer Framework to build an application that generates social media posts and tags based on user input.
-![Finished social post generator application](/framework/images/tutorial/social-finished-app.png)
+![Finished social post generator application](/framework/images/tutorial/social-finished-app.png#framework)
## Setting up your project
@@ -14,7 +14,7 @@ In this tutorial, you will use Writer Framework to build an application that gen
From the Home screen, click on Build an app.
-![Writer home screen](/framework/images/tutorial/home.png)
+![Writer home screen](/framework/images/tutorial/home.png#framework)
Select Framework as the app type you’d like to create, enabling you to generate keys and build your app with the Writer Framework.
@@ -108,7 +108,7 @@ Then, drag a Separator component underneath the Tags component.
Finally, drag a Text component underneath the button. This is where the text generated by the LLM will appear.
-![Initial UI layout](/framework/images/tutorial/social-initial-ui-layout.png)
+![Initial UI layout](/framework/images/tutorial/social-initial-ui-layout.png#framework)
## Updating the code
@@ -173,27 +173,27 @@ The last task is to bind all of the state elements and the click handler to the
First, click on the Text Input component to bring up the Component Settings panel. In the Bindings section, type `topic`.
-
+
Next, click on the Button component to bring up the Component Settings panel. In the Events section, use the dropdown for `wf-click` to choose the `handle_button_click` handler.
-
+
Click on the Message component and set the Text to `@{message}`. Under Visibility, choose “Custom” and set it to `message`.
Next, click on the Tags component. Under General, click “JSON” for Tags and set the object to `@{tags}`.
-
+
Then, click on the Text component to bring up the Component Settings panel. In General Properties, bind this text box to a variable called `posts` by entering `@{posts}` in the text property field. You can also set this text box to accept Markdown input if needed by toggling the “Use Markdown” property.
-
+
Finally, click on the Section that contains the Tags and Text components. Under Visibility, choose “Custom” and set it to `posts`.
When you save and run this code, your social media posts should will generate in the text section you created earlier.
-![Finished social post generator application](/framework/images/tutorial/social-finished-app.png)
+![Finished social post generator application](/framework/images/tutorial/social-finished-app.png#framework)
## Deploying the application
diff --git a/docs/framework/stylesheets.mdx b/docs/framework/stylesheets.mdx
index acf06c37..133a0960 100644
--- a/docs/framework/stylesheets.mdx
+++ b/docs/framework/stylesheets.mdx
@@ -6,7 +6,7 @@ The appearance of your application can be fully customised via CSS stylesheets.
## Importing a stylesheet
-Stylesheet imports are triggered via Framework's `mail`, similarly to other features discussed in [Backend-initiated actions](/backend-initiated-actions). When the import is triggered, the front-end downloads the specified stylesheet and creates a `style` element with its contents.
+Stylesheet imports are triggered via Framework's `mail`, similarly to other features discussed in [Backend-initiated actions](/framework/backend-initiated-actions). When the import is triggered, the front-end downloads the specified stylesheet and creates a `style` element with its contents.
The `import_stylesheet` method takes the `stylesheet_key` and `path` arguments. The first works as an identifier that will let you override the stylesheet later if needed. The second is the path to the CSS file.The path specified needs to be available to the front-end, so storing it in the `/static` folder of your app is recommended.
@@ -27,11 +27,9 @@ initial_state = wf.init_state({
initial_state.import_stylesheet("theme", "/static/custom.css")
```
-
-Use versions to avoid caching
-
-During development time, stylesheets may be cached by your browser, preventing updates from being reflected. Append a querystring to bust the cache, e.g. use `/static/custom.css?3`.
-
+
+Use versions to avoid caching. During development time, stylesheets may be cached by your browser, preventing updates from being reflected. Append a querystring to bust the cache, e.g. use `/static/custom.css?3`.
+
## Applying CSS classes
@@ -58,9 +56,7 @@ The CSS code for the class used earlier, `bubblegum`, can be found below. Note h
```
-Component structure may change
-
-When targeting specific HTML elements inside components, take into account that the internal structure of components may change across Framework versions.
+Component structure may change. When targeting specific HTML elements inside components, take into account that the internal structure of components may change across Framework versions.
Alternatively, you can override Framework's style variables. This behaves slightly differently though; style variables are inherited by children components. For example, if a *Section* has been assigned the `bubblegum` class, its children will also have a pink background by default.
diff --git a/docs/framework/testing.mdx b/docs/framework/testing.mdx
index 7fb49a13..53478231 100644
--- a/docs/framework/testing.mdx
+++ b/docs/framework/testing.mdx
@@ -27,8 +27,8 @@ artificial_state = StreamsyncState({
The code of a Framework application basically consists of two things:
-- Initial state
-- Event handlers
+1. Initial state
+2. Event handlers
It's straightforward to test both, as shown below.