diff --git a/README.md b/README.md index c9eb6d51..31a9d418 100644 --- a/README.md +++ b/README.md @@ -417,7 +417,7 @@ Begin by reading the [Quickstart Guide](https://opensource.respawn.pro/FlowMVI/q you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -426,30 +426,30 @@ Begin by reading the [Quickstart Guide](https://opensource.respawn.pro/FlowMVI/q limitations under the License. ``` -[badge-android]: http://img.shields.io/badge/-android-6EDB8D.svg?style=flat +[badge-android]: https://img.shields.io/badge/-android-6EDB8D.svg?style=flat -[badge-android-native]: http://img.shields.io/badge/support-[AndroidNative]-6EDB8D.svg?style=flat +[badge-android-native]: https://img.shields.io/badge/support-[AndroidNative]-6EDB8D.svg?style=flat -[badge-jvm]: http://img.shields.io/badge/-jvm-DB413D.svg?style=flat +[badge-jvm]: https://img.shields.io/badge/-jvm-DB413D.svg?style=flat -[badge-js]: http://img.shields.io/badge/-js-F8DB5D.svg?style=flat +[badge-js]: https://img.shields.io/badge/-js-F8DB5D.svg?style=flat [badge-js-ir]: https://img.shields.io/badge/support-[IR]-AAC4E0.svg?style=flat [badge-nodejs]: https://img.shields.io/badge/-nodejs-68a063.svg?style=flat -[badge-linux]: http://img.shields.io/badge/-linux-2D3F6C.svg?style=flat +[badge-linux]: https://img.shields.io/badge/-linux-2D3F6C.svg?style=flat -[badge-windows]: http://img.shields.io/badge/-windows-4D76CD.svg?style=flat +[badge-windows]: https://img.shields.io/badge/-windows-4D76CD.svg?style=flat [badge-wasm]: https://img.shields.io/badge/-wasm-624FE8.svg?style=flat -[badge-apple-silicon]: http://img.shields.io/badge/support-[AppleSilicon]-43BBFF.svg?style=flat +[badge-apple-silicon]: https://img.shields.io/badge/support-[AppleSilicon]-43BBFF.svg?style=flat -[badge-ios]: http://img.shields.io/badge/-ios-CDCDCD.svg?style=flat +[badge-ios]: https://img.shields.io/badge/-ios-CDCDCD.svg?style=flat -[badge-mac]: http://img.shields.io/badge/-macos-111111.svg?style=flat +[badge-mac]: https://img.shields.io/badge/-macos-111111.svg?style=flat -[badge-watchos]: http://img.shields.io/badge/-watchos-C0C0C0.svg?style=flat +[badge-watchos]: https://img.shields.io/badge/-watchos-C0C0C0.svg?style=flat -[badge-tvos]: http://img.shields.io/badge/-tvos-808080.svg?style=flat +[badge-tvos]: https://img.shields.io/badge/-tvos-808080.svg?style=flat diff --git a/docs/docs/integrations/essenty.md b/docs/docs/integrations/essenty.md index aaf76159..08022dd4 100644 --- a/docs/docs/integrations/essenty.md +++ b/docs/docs/integrations/essenty.md @@ -1,5 +1,6 @@ --- sidebar_position: 3 +sidebar_label: Essenty --- # Essenty Integration diff --git a/docs/docs/plugins/prebuilt.md b/docs/docs/plugins/prebuilt.md index 53bdce07..25cd439b 100644 --- a/docs/docs/plugins/prebuilt.md +++ b/docs/docs/plugins/prebuilt.md @@ -11,10 +11,6 @@ execute _in the order they were installed_ into the Store. This allows you to assemble business logic like a lego by placing the "bricks" in the order you want, and transparently inject some logic into any store at any point. -Here's how the Plugin chain works: - -![](/chart.png) - ## Plugin Ordering :::danger[The order of plugins matters! ] diff --git a/docs/docs/quickstart.md b/docs/docs/quickstart.md index 4a7fea5a..e83c27b1 100644 --- a/docs/docs/quickstart.md +++ b/docs/docs/quickstart.md @@ -5,6 +5,8 @@ sidebar_position: 1 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import ThemedImage from '@theme/ThemedImage'; # Get Started with FlowMVI @@ -20,6 +22,19 @@ First of all, here's how the library works: - Clients _subscribe_ to Stores to _render_ their **State** and _consume_ side-effects, called **Actions**. - States, Intents, and Actions together form a **Contract**. +
+Show the diagram + + + +
+ ## Step 1: Configure the library ### 1.1: Add dependencies ![Maven Central](https://img.shields.io/maven-central/v/pro.respawn.flowmvi/core?label=Maven%20Central) @@ -166,8 +181,6 @@ sealed interface CounterAction : MVIAction { } ``` -- All Contract classes _must_ be **immutable** and **comparable**. If you don't define `equals`, your IDE will - complain. - If your store does not have a `State`, you can use an `EmptyState` object provided by the library. - If your store does not have side effects, use `Nothing` in place of the side-effect type. diff --git a/docs/docs/state/statemanagement.md b/docs/docs/state/statemanagement.md index ef30231f..b6aab032 100644 --- a/docs/docs/state/statemanagement.md +++ b/docs/docs/state/statemanagement.md @@ -6,17 +6,11 @@ sidebar_label: Updating State # Managing State State management in FlowMVI is slightly different from what we are used to in MVVM. -This guide will teach you everything about how to manage and update application state in a safe, fast, and extensible manner. +This guide will teach you everything about how to manage application state in a fast and durable manner. ## Understanding State -In FlowMVI, state is represented by classes implementing the `MVIState` marker interface. States must be: - -- **Immutable** - State object must never change after it is created -- **Comparable** - State object must implement a stable and valid `hashCode`/`equals` contract - -To adhere to the requirements above, the only thing you need to do in most cases is to use `data class`es or `data object`s that will generate `equals` and `hashCode` for you. - +In FlowMVI, state is represented by classes implementing the `MVIState` marker interface. The simplest state looks like this: ```kotlin @@ -26,28 +20,37 @@ data class CounterState( ) : MVIState ``` -The marker interface `MVIState` is needed to **enforce** the requirements above at compilation time. If you do not define `equals`, your IDE will complain. -It also prevents you from using unintended objects (like 3rd party interfaces or network response objects) as your state. +States must be: +- **Immutable** - State object must never change after it is created +- **Comparable** - State object must implement a stable and valid `hashCode`/`equals` contract +- **Scoped** - Unintended objects (like 3rd party interfaces or network responses) should not be used as a State. + +The marker interface `MVIState` is needed to **enforce** the requirements above at compilation time. +To adhere to the requirements above, the only thing you need to do in most cases is to use `data class`es or `data object`s that will generate `equals` and `hashCode` for you. :::warning[Do not mutate the state directly!] -Do not use mutable properties like `var` or mutable collections. +Avoid mutable properties like `var` or mutable collections. Always create new instances using `copy()` and ensure the collections you pass are **new** ones, not just `Mutable` collections upcasted to their parent type, otherwise your state updates will **not be reflected**. ::: +:::tip[Empty state] + +If your store does not have a `State`, you can use an `EmptyState` object provided by the library. + +::: + ## State Families -The state in the example above can be used by itself, but most apps can be in more than one state at a time. The best example is LCE (loading-content-error) state family. +The state in the example above can be used by itself, but most apps can have more than one state. The best example is LCE (loading-content-error) state family. The key things we want from such state are that: - There are no unused/junk/placeholder values for each state. - For example, during an `Error` state, there is no data because it failed loading, and vice versa, when the state is `Content`, we don't have an error to report. - Having these properties unnecessarily pollutes the code and fosters many bugs. - State clients who use it cannot gain access to unwanted data. - - This means that if we pass our state to the UI to render it, we want to avoid accidentally showing **both** an error message **and** the list of items. - The only way to reliably prevent that is to **enforce** a compile-time error on clients, making them handle all different types of states. + - For example, if we pass our state to the UI to render it, we want to avoid accidentally showing **both** an error message **and** the list of items. To achieve our goals above, FlowMVI supports **State Families**, represented as sealed interfaces: @@ -66,29 +69,30 @@ Using `sealed interface` instead of `class` improves performance by reducing all ::: -Now not only must the client handle all states using an exhaustive `when` block, but we no longer expose any properties that shouldn't be present, such as having `items: List? = null` or `e: Exception? = null`. - However, the code above introduces some complexity to handling state types, such as needing to cast or check the state's type before updating it: ```kotlin val current = state.value as? Content ?: return -current.items // use the property + +// use the property +val items = current.items ``` -For that, the library provides a nice and simple DSL consisting of two functions: +For that, the library provides a DSL consisting of two functions: ```kotlin +// capture and update updateState { // this: LCEState.Content copy(items = items + loadMoreItems()) } +// capture but do not change withState { // this: LCEState.Error action(ShowErrorMessage(exception = this.e)) } ``` These functions first check the type of the state, and if it is not of the first type parameter, they skip the operation inside the `block` entirely. -The latter one only checks the state but does not change it. :::tip[Fail Fast] @@ -101,8 +105,8 @@ an animation, leading to, for example, the app retrying failed data loading mult ### Nested State Families -Of course, you can mix and match the approaches above or introduce multiple nesting levels to your states. For example, to implement progressive content loading, you can create -a common state data class with multiple state families nested inside: +Of course, you can mix and match the approaches above or introduce multiple nesting levels to your states. +For example, to implement progressive content loading, you can create a common state `data class` with multiple families nested inside: ```kotlin sealed interface FeedState: MVIState { @@ -125,10 +129,10 @@ In that case, you will not need the typed versions of `updateState`, but rather - `value.typed()` to cast the `value` to type `T` or return `null` otherwise (just like the operator `as?`) - `value.withType { }` to operate on `value` only if it's of type `T`, or just return it otherwise -If you represent the state this way, you will never have to write ugly `null`-ridden code again to manage states with placeholders, +If you represent the state this way, you will never have to write `null`-ridden code again to manage states with placeholders, and your stores' subscribers will never have the problem of rendering an inconsistent or invalid state. -But next, let's talk about thread safety. +Next, let's talk about state updates. ## Serialized State Transactions (SSTs) @@ -154,15 +158,19 @@ This code contains multiple race conditions: 1. The code obtains the index to the last item to load more items, then executes a long operation, during which **the state may have already changed**. When trying to add new items, the item list could have already been modified, and we'll get duplicate or stale values appended. This is a **data race**. -2. When mutating the state using `state.value`, while the right-hand side of the expression is being executed, the left-hand side +2. When mutating the state using `state.value`, while the right-hand side of the expression is being evaluated, the left-hand side (`state.value`) could have already been changed by another thread, leading to the right-hand side overwriting the state with stale data. This is a **thread race**. -The first problem arises only when we need to **read the current state** to make a decision on which logic to execute next. +These problems arise only when we need to **read the current state** to make a decision on what to do next. In the example above, we need the current items to load the next page, but this is apparent in many other cases as well, -such as form validation, conditional logic, or LCE-based logic. +such as form validation or any conditional logic. -You may have never encountered these issues before, but this is only due to luck - because the state is modified on the main thread and sequentially. +You may have never encountered these issues before, but this is only due to luck - because the state was modified on the main thread and sequentially. FlowMVI, unlike many other libraries, allows the state to be modified on **any thread** to enable much better performance and reactiveness of your apps. +You **will** run into these issues as soon as you override `coroutineContext` or enable the `parallelIntents` property of the Store. + +
+Regarding the `StateFlow.update { }` operator To address a common objection to this argument that sounds like: @@ -175,11 +183,14 @@ Study the [documentation](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx- This means that if the starting and ending states do not match, your `function` block will be executed **multiple times**. This not only wastes resources but can be detrimental if you call a function that is not _idempotent_ (has side effects) inside the `function` block. For example, if you are making a banking app and perform a transaction, then the code above can lead to the user's credit card being charged multiple times. -Pretty awful, don't you agree? Even if you always modify the state on the main thread, or use `update { }`, the parallel nature of coroutines does not prevent you from having data races. -To combat this, the library uses SSTs by default when you gain any access to the `state`. +
+ +--- + +To combat the problems above, the library uses SSTs by default when you use the state. This is also why you can't just access `state` wherever you wish. Instead, you only have 2 functions to gain access to the state: `updateState` and `withState`. @@ -187,9 +198,8 @@ The typed functions we discussed in the previous section are just aliases to the **no one else can read or change the state**. Whenever you call `updateState`, the transaction is synchronized using a Mutex, and all other code that tries to also call `updateState` will wait in a FIFO queue until the `block` finishes execution and the state is updated. -This means that when you have access to the state, you can be absolutely sure it will not change to something unexpected inside the lambda. +This means that when you have access to the state, you can be sure it will not change to something unexpected inside the lambda. You can execute any suspending and long-running code inside the block and it will only be executed **once** and on the most up-to-date value. -So even in systems with hundreds of parallel state updates, it will always stay consistent. :::info @@ -198,7 +208,7 @@ This does not happen when `withState` is called as the state does not change. ::: -### Reentrant vs Non-reentrant +### Reentrant vs Non-Reentrant By default, SSTs are **reentrant**, which means that you are allowed to write code like this: @@ -211,25 +221,24 @@ updateState { ``` In that case, if you are already inside a state transaction, a new one will **not be created** for nested update blocks. -Otherwise, you would get a **deadlock** as the inner transaction waits for the outer state update to finish, which waits for the inner update to finish. +Otherwise, you would get a **deadlock** as the inner block waits for the outer update to finish, which waits for the inner update to finish. -This, unfortunately, has a performance penalty due to context switching, which makes the reentrant transactions ~1600% slower. -For most apps, this is negligible as the time is still measured in microseconds, so as of 3.x SSTs are reentrant by default. +This, unfortunately, has a performance penalty due to context switching, which makes reentrant transactions ~1600% slower. +For most apps, however, this is negligible as the time is still measured in microseconds. -When configuring a store, it is possible to change the `stateStrategy` property to make the transactions non-reentrant: +When configuring a Store, it is possible to change the `stateStrategy` property to make the transactions non-reentrant: ```kotlin val store = store(initial = Loading) { configure { stateStrategy = Atomic(reentrant = false) - debuggable = true } } ``` In this case, while the store is `debuggable`, the library will check the transaction for you so that instead of a deadlock you at least get a crash. -:::info[New Default in 4.0] +:::info[New default in 4.0] In the future, non-reentrant transactions may become the default for the simple reason of redundancy. Since you are in the transaction, can just use `this` property, which is already the most up-to-date state. @@ -240,8 +249,8 @@ Since you are in the transaction, can just use `this` property, which is already Although non-reentrant transactions are already very fast, they are still ~2x slower due to locking overhead. -Only if you absolutely **must** squeeze the last drop of performance from the store, and you are **sure** you can avoid the pitfalls discussed above manually, -you can use one of two ways to override the synchronization routines: +Only if you absolutely **must** squeeze the last drop of performance from the Store, and you are **sure** you handle the problems discussed above, +you can use one of two ways to override the synchronization: - `updateStateImmediate` function, which avoids all locks, **plugins** and thread safety, or - `StateStrategy.Immediate` which disables SSTs entirely. @@ -252,18 +261,23 @@ you can use one of two ways to override the synchronization routines: ::: -One example where that may be needed is Compose's text fields: +
+Where can bypassing be needed? + +One example where overriding is needed is Compose's text fields: ```kotlin data class State(val input: String = "") : MVIState -// composable: -TextField( - value = state.input, - onValueChange = { intent(ChangedInput(it)) }, -) +@Composable +fun IntentReceiver.ScreenContent(state: State) { + + TextField( + value = state.input, + onValueChange = { intent(ChangedInput(it)) }, + ) +} -// container: val store = store(State()) { reduce { intent -> when(intent) { @@ -278,11 +292,12 @@ val store = store(State()) { Due to flaws in Compose's `TextField` implementation, if you do not update the state immediately, the UI will have jank. This will be addressed in future Compose releases with `BasicTextField2`. +
+ :::warning[Do not leak the state] -Despite all the effort put into safety, it's still possible to leak the state. -For example, assigning it to external variables or launching coroutines while in an SST will **leak** the current state outside the transaction. -If you do leak the state, always assume that **any** state outside the transaction is **invalid and outdated**. +It's still possible to leak the state by assigning it to external variables or launching coroutines while in an SST. +This can be necessary, but if you do leak the state, always assume that **any** state outside the transaction is **invalid and outdated**. ::: @@ -290,8 +305,8 @@ If you do leak the state, always assume that **any** state outside the transacti With MVVM, a best practice is to produce the state from several upstream flows using `combine`, then the `stateIn` operator to make the flow hot. -A key distinction of MVI compared to MVVM is that a BLoC always has a single, hot, mutable state. -To avoid resource leaks and redundant work, the state must only be updated **while the subscribers are present** (otherwise who is there to render the state?). +A key distinction of MVI compared to MVVM is that a Store always has a single, hot, mutable state. +To avoid resource leaks and redundant work, the state should only be updated **while the subscribers are present**. FlowMVI provides the API for that in the form of the `whileSubscribed` plugin: ```kotlin @@ -315,8 +330,7 @@ val store = store(ProgressiveState()) { // initial value just like stateIn } ``` -So make sure that instead of collecting long-running or infinite flows in the `init` plugin, you observe data streams only when subscribers are present, unless your -internal store logic depends on the state being up-to-date in the background. +### Persisting data Additionally, whenever you produce your state, such as in the `combine` lambda, you must **consider the current state**. For example, if your state has an in-memory value, such as a text input, and you use a State Family, you must "persist" the previous value so that it is not overridden: diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index f60ca540..2ac4ef11 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -1,43 +1,74 @@ -import { themes as prismThemes } from 'prism-react-renderer'; -import type { Config } from '@docusaurus/types'; -import type * as Preset from '@docusaurus/preset-classic'; +import { themes as prismThemes } from "prism-react-renderer"; +import type { Config } from "@docusaurus/types"; +import type * as Preset from "@docusaurus/preset-classic"; -const description = "Architecture Framework for Kotlin. Reuse every line of code. Handle all errors automatically. No boilerplate. Analytics, metrics, debugging in 3 lines. 50+ features." +const description = + "Architecture Framework for Kotlin. Reuse every line of code. Handle all errors automatically. No boilerplate. Analytics, metrics, debugging in 3 lines. 50+ features."; const config: Config = { - title: 'FlowMVI', - tagline: 'Simplify Complexity.', - favicon: '/favicon.ico', - url: 'https://opensource.respawn.pro', - baseUrl: '/FlowMVI/', - organizationName: 'respawn-app', - projectName: 'FlowMVI', - onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'throw', - onDuplicateRoutes: 'throw', + title: "FlowMVI", + tagline: "Simplify Complexity.", + favicon: "/favicon.ico", + url: "https://opensource.respawn.pro", + baseUrl: "/FlowMVI/", + organizationName: "respawn-app", + projectName: "FlowMVI", + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "throw", + onDuplicateRoutes: "throw", trailingSlash: false, markdown: { mermaid: true, }, i18n: { - defaultLocale: 'en', - locales: ['en'], + defaultLocale: "en", + locales: ["en"], }, + stylesheets: [ + "https://fonts.googleapis.com/css2?family=Comfortaa:wght@400;500;600;700&family=Montserrat+Alternates:wght@500;600;700&display=swap", + ], + headTags: [ + { + tagName: "link", + attributes: { + rel: "preconnect", + href: "https://fonts.googleapis.com", + }, + }, + { + tagName: "link", + attributes: { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossorigin: "anonymous", + }, + }, + { + tagName: "link", + attributes: { + rel: "preload", + href: "https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Regular.woff", + as: "font", + type: "font/woff", + crossorigin: "anonymous", + }, + }, + ], presets: [ [ - 'classic', + "classic", { docs: { breadcrumbs: false, sidebarCollapsed: false, - sidebarPath: './sidebars.ts', - routeBasePath: '/', - editUrl: 'https://github.com/respawn-app/flowmvi/tree/main/docs', + sidebarPath: "./sidebars.ts", + routeBasePath: "/", + editUrl: "https://github.com/respawn-app/FlowMVI/blob/master/docs/", }, blog: false, pages: false, theme: { - customCss: './src/css/custom.css', + customCss: "./src/css/custom.css", }, gtag: { trackingID: "G-NRB9ZFKNGN", @@ -47,7 +78,7 @@ const config: Config = { ], themeConfig: { colorMode: { - defaultMode: 'dark', + defaultMode: "dark", respectPrefersColorScheme: true, }, metadata: [ @@ -60,7 +91,7 @@ const config: Config = { { name: "og:description", content: description }, { name: "description", content: description }, ], - image: '/banner.png', + image: "/banner.png", algolia: { contextualSearch: false, appId: "YFIMJHUME7", @@ -75,29 +106,29 @@ const config: Config = { }, }, navbar: { - title: 'FlowMVI', + title: "FlowMVI", hideOnScroll: true, style: "dark", logo: { - alt: 'Logo', - src: '/icon.svg', + alt: "Logo", + src: "/icon.svg", }, items: [ { href: "/", label: `© ${new Date().getFullYear()} Respawn OSS`, - position: 'right', + position: "right", }, { - href: 'https://opensource.respawn.pro/FlowMVI/javadocs/index.html', - label: 'API Docs', - position: 'right', + href: "https://opensource.respawn.pro/FlowMVI/javadocs/index.html", + label: "API Docs", + position: "right", }, { - href: 'https://github.com/respawn-app/FlowMVI', + href: "https://github.com/respawn-app/FlowMVI", label: undefined, - className: 'header-github-link', - position: 'right', + className: "header-github-link", + position: "right", }, ], }, @@ -106,107 +137,60 @@ const config: Config = { theme: prismThemes.oneLight, darkTheme: prismThemes.oneDark, additionalLanguages: [ - 'java', - 'kotlin', - 'bash', - 'diff', - 'json', - 'toml', - 'yaml', - 'gradle', - 'groovy', + "java", + "kotlin", + "bash", + "diff", + "json", + "toml", + "yaml", + "gradle", + "groovy", `properties`, ], magicComments: [ { - className: 'theme-code-block-highlighted-line', - line: 'highlight-next-line', - block: { start: 'highlight-start', end: 'highlight-end' }, + className: "theme-code-block-highlighted-line", + line: "highlight-next-line", + block: { start: "highlight-start", end: "highlight-end" }, }, { - className: 'code-block-error-line', - line: 'This will error', + className: "code-block-error-line", + line: "This will error", }, ], }, - headTags: [ - { - tagName: 'link', - attributes: { - rel: 'preconnect', - href: 'https://fonts.googleapis.com', - }, - }, - { - tagName: 'link', - attributes: { - rel: 'preconnect', - href: 'https://fonts.gstatic.com', - crossorigin: 'anonymous', - }, - }, - { - tagName: 'link', - attributes: { - rel: 'preload', - href: 'https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Regular.woff2', - as: 'font', - type: 'font/woff2', - crossorigin: 'anonymous', - }, - }, - { - tagName: 'link', - attributes: { - rel: 'preload', - href: 'https://fonts.googleapis.com/css2?family=Montserrat+Alternates:wght@500;600;700&display=swap', - as: 'style', - }, - }, - { - tagName: 'link', - attributes: { - rel: 'stylesheet', - href: 'https://fonts.googleapis.com/css2?family=Comfortaa:wght@400;500;600;700&display=swap', - }, - } - ], } satisfies Preset.ThemeConfig, plugins: [ [ - '@docusaurus/plugin-pwa', + "@docusaurus/plugin-pwa", { - offlineModeActivationStrategies: [ - 'appInstalled', - 'standalone', - 'queryString', - 'saveData', - ], + offlineModeActivationStrategies: ["appInstalled", "standalone", "queryString", "saveData"], pwaHead: [ { - tagName: 'link', - rel: 'icon', - href: 'icon.svg', + tagName: "link", + rel: "icon", + href: "icon.svg", }, { - tagName: 'link', - rel: 'manifest', - href: 'manifest.json', + tagName: "link", + rel: "manifest", + href: "manifest.json", }, { - tagName: 'meta', - name: 'theme-color', - content: '#00d46a', + tagName: "meta", + name: "theme-color", + content: "#00d46a", }, { - tagName: 'link', - rel: 'apple-touch-icon', - href: 'apple-touch-icon.png', + tagName: "link", + rel: "apple-touch-icon", + href: "apple-touch-icon.png", }, { - tagName: 'meta', - name: 'apple-mobile-web-app-capable', - content: 'yes', + tagName: "meta", + name: "apple-mobile-web-app-capable", + content: "yes", }, ], }, diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index c12acb56..46f1742a 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -6,20 +6,12 @@ @font-face { font-family: "Monaspace Neon"; - src: url(https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Regular.woff2) - format("woff2"), - url(https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Regular.woff) - format("woff"); + src: url(https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Regular.woff) + format("woff"); font-display: swap; font-feature-settings: "calt", "liga", "ss01", "ss02", "ss03", "ss04", "ss05", "ss06", "ss07", "ss08", "ss09"; } -@font-face { - font-family: "Montserrat Alternates"; - src: url("https://fonts.googleapis.com/css2?family=Montserrat+Alternates:wght@500;600;700&display=swap"); - font-display: swap; -} - :root { --ifm-color-primary: #2e8555; --ifm-color-primary-dark: #29784c; @@ -28,8 +20,8 @@ --ifm-color-primary-light: #33925d; --ifm-color-primary-lighter: #359962; --ifm-color-primary-lightest: #3cad6e; - --ifm-font-family-base: Comfortaa, Montserrat, system-ui, -apple-system, sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, - Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --ifm-font-family-base: "Comfortaa", "Montserrat", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Helvetica, Arial, sans-serif; --ifm-font-family-monospace: "Monaspace Neon", "JetBrains Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --ifm-font-size-base: 100%; @@ -48,7 +40,7 @@ --ifm-color-primary-light: #00e975; --ifm-color-primary-lighter: #00f47a; --ifm-color-primary-lightest: #15ff8a; - --docusaurus-highlighted-code-line-bg: #1E2227; + --docusaurus-highlighted-code-line-bg: #1e2227; } [data-theme="light"] .DocSearch { @@ -116,7 +108,7 @@ h3, h4, h5, h6 { - font-family: Comfortaa; + font-family: var(--ifm-font-family-base); letter-spacing: normal; } @@ -143,3 +135,29 @@ h1::first-letter, h2::first-letter { color: var(--ifm-color-primary); } + +/* Details styling */ +details { + --docusaurus-details-decoration-color: var(--ifm-color-primary) !important; + background-color: var(--ifm-color-emphasis-100) !important; + border: 1px solid var(--ifm-color-primary) !important; + border-radius: var(--ifm-global-radius); +} + +[data-theme="dark"] details { + --docusaurus-details-decoration-color: var(--ifm-color-primary) !important; + background-color: var(--ifm-color-emphasis-100) !important; + border: 1px solid var(--ifm-color-primary) !important; +} + +details > summary { + border-bottom-color: var(--docusaurus-details-decoration-color) !important; +} + +details > summary::before { + border-color: transparent transparent transparent var(--ifm-color-primary) !important; +} + +details > summary:hover { + background-color: var(--ifm-color-emphasis-200) !important; +} diff --git a/docs/static/chart.png b/docs/static/chart.png deleted file mode 100644 index fb4ce25b..00000000 Binary files a/docs/static/chart.png and /dev/null differ diff --git a/docs/static/diagram-dark.webp b/docs/static/diagram-dark.webp new file mode 100644 index 00000000..fa136df4 Binary files /dev/null and b/docs/static/diagram-dark.webp differ diff --git a/docs/static/diagram-light.webp b/docs/static/diagram-light.webp new file mode 100644 index 00000000..d4b6debb Binary files /dev/null and b/docs/static/diagram-light.webp differ