diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..7e72d31e5219d37568c30a58ae0d80a1043c5f77 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,18 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +assets/htmx-meme.png filter=lfs diff=lfs merge=lfs -text +assets/og-image.png filter=lfs diff=lfs merge=lfs -text +assets/og-sq.png filter=lfs diff=lfs merge=lfs -text +assets/thumb.png filter=lfs diff=lfs merge=lfs -text +assets/webdev.jpg filter=lfs diff=lfs merge=lfs -text +docs/explains/CreateWebhook.png filter=lfs diff=lfs merge=lfs -text +docs/explains/CreateWebhook2.png filter=lfs diff=lfs merge=lfs -text +docs/explains/imgs/gh-oauth.png filter=lfs diff=lfs merge=lfs -text +docs/explains/refund.png filter=lfs diff=lfs merge=lfs -text +docs/explains/StripeDashboard_API_Key.png filter=lfs diff=lfs merge=lfs -text +docs/explains/StripePaymentPage.jpg filter=lfs diff=lfs merge=lfs -text +docs/site_libs/bootstrap/bootstrap-icons.woff filter=lfs diff=lfs merge=lfs -text +docs/tutorials/by_example_files/figure-commonmark/cell-58-1-image.png filter=lfs diff=lfs merge=lfs -text +docs/tutorials/by_example_files/figure-html/cell-58-1-image.png filter=lfs diff=lfs merge=lfs -text +texts.db filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6da0c20f56ebb8c937277b2cdae72bb6fe3cd77c --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.swp +*.db +*.db-* +*.db_* +tags +index_files/ +data/ +.DS_Store +.sesskey +__pycache__/ diff --git a/README.md b/README.md index c49c7bfb61e3b469ae9512533fab3389f8a8ffe8..e735c3046975e5d31ca6377ddebca44cd63a7321 100644 --- a/README.md +++ b/README.md @@ -1,20 +1 @@ ---- -title: Home Fasthtml -emoji: 🚀 -colorFrom: red -colorTo: red -sdk: docker -app_port: 8501 -tags: -- streamlit -pinned: false -short_description: Jeremy Howard FastHTML exploration -license: apache-2.0 ---- - -# Welcome to Streamlit! - -Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart: - -If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community -forums](https://discuss.streamlit.io). +fasthtml home page diff --git a/about/LICENSE b/about/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 --- /dev/null +++ b/about/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/about/README.md b/about/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a3b0d9ba67ddf2f900d9a5687a8e4c37027cdf31 --- /dev/null +++ b/about/README.md @@ -0,0 +1,2 @@ +# fh-about +fastht.ml/about source diff --git a/about/app.py b/about/app.py new file mode 100644 index 0000000000000000000000000000000000000000..a6863dfa5dff1f182a0d826fb8ae10a5532fe93b --- /dev/null +++ b/about/app.py @@ -0,0 +1,77 @@ +from itertools import chain +from fasthtml.common import * +from functools import partial +from monsterui.all import * +import fasthtml.common as fh +from monsterui.foundations import * + +_A = partial(A, target='_blank') +def Markdown(s, **kw): return Div(render_md(s, class_map_mods={'ul':'uk-list uk-list-bullet space-y-2 mb-6 ml-6 text-lg'}), **kw) + +ghurl = 'https://github.com/AnswerDotAI/fasthtml' +fhurl = 'https://fastht.ml' +docs = '/docs' + +def NavBar(*c, # Component for right side of navbar (Often A tag links) + brand=None, # Brand/logo component for left side + right_items=None, + right_cls='items-center space-x-4', # Spacing for desktop links + mobile_cls='space-y-4', # Spacing for mobile links + cls='p-4', # Classes for navbar + )->FT: # Responsive NavBar + "Creates a responsive navigation bar with mobile menu support" + menu_id = fh.unqid() + mobile_icon = A(UkIcon("menu", width=30, height=30), cls="md:hidden", data_uk_toggle=f"target: #{menu_id}; cls: hidden") + return Div( + Div(DivFullySpaced( + DivLAligned(brand, Div(*c, cls='hidden md:flex space-x-4')), # Brand/logo component for left side + mobile_icon, # Hamburger menu icon + Div(*right_items,cls=(stringify(right_cls),'hidden md:flex'))), # Desktop Navbar + cls=('monster-navbar', stringify(cls))), + DivCentered(*c, *right_items, + cls=(stringify(mobile_cls),stringify(cls),'hidden md:hidden monster-navbar'), + id=menu_id)) + +def NavLink(*args, cls='text-gray-800', target='_blank', **kw): + return fh.A(*args, cls=(cls, TextT.lg, 'hover:text-yellow-400/60 transition-colors text-sm'), target=target, **kw) + +def BstPage(selidx, title, h2s, *c): + navitems = [('About', '/about'), ('Vision', '/about/vision'), ('Foundations', '/about/foundation'), + ('Technology', '/about/tech'), ('Components', '/about/components'), ('Limits', '#', {'disabled':True})] + logo = 'assets/logo.svg' + return ( + Title(title), + Container( + NavBar( + *[NavLink(k, href=v, target=None, cls=TextT.bold if selidx==i else '') for i, (k,v) in enumerate(navitems[:-1])], + NavLink('Limits', href='javascript:void(0)', disabled=True, uk_tooltip='No limits!', cls=TextT.gray, target=None), + brand=A(Img(src=logo), href=fhurl), + right_items=[ + NavLink('Docs', href=docs), + NavLink(Img(src='assets/github-mark/github-mark.svg', width=20, height=20), href=ghurl), + ], + cls='bg-[#3cdd8c] rounded-t-3xl p-2 py-5 px-4'), + + Container( + Grid( + Div(NavContainer(*[Li(A(h2, href=f'#sec{i+1}')) for i,h2 in enumerate(h2s)], + sticky=True, uk_scrollspy_nav=True), cls='hidden md:block col-span-1'), + Div(H1(title, cls='mb-12'), *c, cls='col-span-12 md:col-span-5'), + cols=6)), + + # Footer + DividerLine(), + Grid( + P("© 2024 onwards AnswerDotAI, Inc", cls=TextPresets.muted_lg), + DivCentered(A(Img(src=logo, height=24), href=fhurl)), + DivRAligned(A("Home"), + _A("Docs",href='/docs'), + _A("Company", href='https://www.answer.ai/'), cls=TextPresets.muted_lg+'space-x-4'), + ), + cls='py-8 mb-4' + ContainerT.xl)) + +def Sections(h2s, texts): + colors = 'yellow', 'pink', 'teal', 'blue' + div_cls = 'py-2 px-3 mt-4 mb-2 bg-{}-400/70 rounded-tl-3xl' #bg-{}-100 + return chain([Div(H2(h2, id=f'sec{i+1}', cls=div_cls.format(colors[i%4])), Div(txt, cls='px-2')) + for i,(h2,txt) in enumerate(zip(h2s, texts))]) diff --git a/about/components.py b/about/components.py new file mode 100644 index 0000000000000000000000000000000000000000..e6bbb74b41a9f422f7b02461247b3bb8e31b2cda --- /dev/null +++ b/about/components.py @@ -0,0 +1,53 @@ +from about.app import * + +def page(): + h2s = 'Why', 'How', 'The future' + txts = [Markdown(s1), Markdown(s2), Markdown(s3)] + secs = Sections(h2s, txts) + return BstPage(4, "Python HTML components", h2s, *secs) + +s1 = """ +FastHTML embeds HTML generation inside Python code. The idea of embedding an HTML generator inside a programming language is not new. It is a particularly popular approach in functional languages, and includes libraries like: Elm-html (Elm), hiccl (Common Lisp), hiccup (Clojure), Falco.Markup (F#), Lucid (Haskell), and dream-html (OCaml). But the idea has now gone far beyond the functional programming world--- JSX, an embedded HTML generator for React, is one of the most popular approaches for creating web apps today. + +However most Python programmers are probably more familiar with template-based approaches, such as Jinja2 or Mako. Templates were originally created for web development in the 1990s, back when web design required complex browser-specific HTML. By using templates, designers were able to work in a familiar language, and programmers could "fill in the blanks" with the data they needed. Today this is not needed, since we can create simple semantic HTML, and use CSS to style it. + +Templates have a number of disadvantages, for instance: + +- They require a separate language to write the templates, which is an additional learning curve +- Template languages are generally less concise and powerful than Python +- Refactoring a template into sub-components is harder than refactoring Python code +- Templates generally require separate files +- Templates generally do not support the Python debugger. + +By using Python as the HTML-generation language, we can avoid these disadvantages. More importantly, we can create a rich ecosystem of tools and frameworks available as pip-installable Python modules, which can be used to build web applications. +""" + +s2 = """ +FastHTML's underlying component data structure is called `FT` ("FastTag"). To learn how this works in detail, see the [Explaining FT Components](/docs/explains/explaining_xt_components.html) page. `FT` objects can be created with functions with the Capitalized name of each HTML tag, such as `Div`, `P`, and `Img`. The functions generally take positional and keyword arguments: + +- Positional arguments represent a list of children, which can be strings (in which case they are text nodes), FT child components, or other Python objects (which are stringified). +- Keyword arguments represent a dictionary of attributes, which can be used to set the properties of the HTML tag +- Keyword arguments starting with `hx_` are used for HTMX attributes. + +Some functions, such as `File`, have special syntax for their arguments. For instance, `File` takes a single filename argument, and creates a DOM subtree representing the contents of the file. + +Any FastHTML handler can return a tree of `FT` components, or a tuple of FT component trees, which will be rendered as HTML partials and sent to the client for processing by HTMX. If a user goes directly to a URL rather than using HTMX, the server will automatically return a full HTML page with the partials embedded in the body. + +Much of the time you'll probably be using pre-written FastHTML components that package up HTML, CSS, and JS. Often, these will in turn hand off much of the work to some general web framework; for instance the site you're reading now uses Bootstrap (and the `fh-bootstrap` FastHTML wrapper). At first, moving from HTML to FT components, can seem odd, but it soon becomes natural -- as Audrey Roy Greenfeld, a hugely experienced Python web programmer author, and educator, told us: + +> "*In my head I had resistance and initial scepticism to converting all my HTML to FT. When I realised that working with the tags in Python is like the elegance of working in the frequency domain after Fourier transform vs. working with time series data in the time domain, I finally gave in, let go, started enjoying the FT tags. The first few times I thought the approach of conversion and then copy pasting was crazy. It was only when I started to understand how to organise the tags into components that it suddenly felt elegant and templates felt crazy.*" + +One good approach to creating components is to find things you like on the web and convert them to FastHTML. There's a simple trick to doing this: + +1. Right-click on the part of a web page that you want to use in your app, and choose 'Inspect' +1. In the elements window that pops up, right-click on the element you want, choose 'Copy', and then 'Outer HTML' +1. Now you've got HTML in your clipboard, you can automatically convert it to FastHTML: go to [h2f.answer.ai](https://h2x.answer.ai/), paste the HTML into the text area at the top, then the FastHTML code will appear at the bottom. Click the Copy icon at the top right of that code and then paste it into your Python app. + +BTW, the h2f app mentioned above is written in around a dozen lines of code! You can see the [source code here](https://github.com/AnswerDotAI/fasthtml-example/blob/main/h2f/main.py). +""" + +s3 = """ +We want your help! FastHTML is very new, so the ecosystem at this stage is still small. We hope to see FastHTML Python versions of style libraries like Bootstrap, DaisyUI, and Shoelace, as well as versions of all the most popular JavaScript libraries. If you are a Python developer, we would love your help in creating these libraries! If you do create something for FastHTML users, let us know, so we can link to your work (or if you think it would be a useful part of the FastHTML library itself, or one of our extension libraries, feel free to send us a pull request). + +We would also like to see Python modules that hook into FastHTML's and Starlette's extensibility points, such as for authentication, database access, deployment, multi-host support, and so forth. Thanks to Python's flexibility and the power of ASGI, it should be possible for a single FastHTML server to replace a whole stack of separate web servers, proxies, and other components. +""" diff --git a/about/favicon.ico b/about/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..82ef3bab5996bc30a77e3fe0901081eed5eb8c61 Binary files /dev/null and b/about/favicon.ico differ diff --git a/about/foundations.py b/about/foundations.py new file mode 100644 index 0000000000000000000000000000000000000000..40aee46ddc3663091dad61e3130a9d3ad2ed9904 --- /dev/null +++ b/about/foundations.py @@ -0,0 +1,81 @@ +from about.app import * + +def page(): + h2s = 'ASGI', 'HTMX', 'HTTP', 'HTML/CSS/JS' + secs = Sections(h2s, map(Markdown, [s1, s2, s3, s4])) + return BstPage(2, 'The foundations of FastHTML', h2s, *secs) + +s1 = """ +FastHTML brings together and builds on top of two well-established, astonishingly flexible, performant technology frameworks: *ASGI* (implemented in Uvicorn and Starlette), and *HTMX*. + +[ASGI](https://asgi.readthedocs.io/en/latest/) is a small but incredibly clever approach to simplifying how HTTP, the foundation of web communication, works. It converts all the different parts of an HTTP transaction into a basic, well-defined Python API: a single function, which takes three parameters, which provides access to the full HTTP specification. + +Uvicorn is the ASGI server used by FastHTML---that is, it is responsible for listening for HTTP messages, and converting them into the Python ASGI API. Then Starlette is responsible for taking this powerful single-function ASGI foundation and making it more convenient for programmers, by adding a small number of functions and classes that remove the boilerplate you would otherwise need to support ASGI. As a FastHTML user you very rarely need to know anything about the ASGI/Uvicorn/Starlette trio, other than that it is there in the background doing a lot of work for you! + +To learn more about how Uvicorn and Starlette work in FastHTML, see the relevant [technology section](/tech#sec3). +""" + +s2 = """ +HTML on its own provides only the most basic interaction mechanisms: you can click on a link to “get” an HTML page, or you can click a button on a form to “post” form data. In either case, the HTML result from the server replaces the current page (known as a “full page refresh”). These limitations have been there since the earlier days of the web. [HTMX](https://htmx.org/) is a library that removes them, by removing four key constraints: + +1. Any element on a page can call the server, not only links and forms +2. Any event can call the server (e.g. mouseover, key-down, or scroll), not only clicks +3. Any HTTP method can be used to call the server, not only “get” and “post” methods +4. The server response can be used to modify the existing page in any way, deleting elements, adding elements, or changing elements, instead of only replacing the whole page. + +HTMX meme + +HTMX was previously known as [Intercooler](https://intercoolerjs.org/). It is now over 10 years old---so it's a mature technology. HTMX/Intercooler is responsible for the idea that we can build on top of the fundamentals of the web, without sacrificing the ability to create modern, interactive web applications. Without it, FastHTML would not exist. HTMX is famous for its [memes](https://v1.htmx.org/essays/#memes), including the image above, which highlights how HTMX's approach returns us to the simplicity of the early days of the web (although perhaps now we should update that meme to FastHTML 2024, where we would have just 3 parts: browser, DOM, and a python file!) + +To learn more about how HTMX works and how to use it, see the [HTMX technology section](/tech#sec2). To understand the benefits of using HTMX in practice, watch [this talk](https://www.youtube.com/watch?v=3GObi93tjZI), which goes through a real case study of using HTMX to replace React in a complex large application. It shows how HTMX allowed the amount of code to be drastically reduced, the speed of the site got faster, and they were able to simplify their team structure by removing the need for frontend specialists. +""" + +s3 = """ +All web page requests are made by your browser, and returned by the web server, using *HTTP*. Many web programming systems attempt to hide this from the developer, but FastHTML (and the underlying technologies Uvicorn, Starlette, and HTMX) does not. By surfacing this, it means you are working directly with the foundations of the web, not through frequently-changing leaky abstractions. HTTP is, at its heart, a simple text protocol that underlies all web communication. It starts with a request, e.g: + +``` +GET / HTTP/1.1 +Host: www.example.com +User-Agent: Mozilla/5.0 +Accept-Language: en-GB,en;q=0.5 +Accept-Encoding: gzip, deflate, br +Connection: keep-alive +``` + +The first line shows it is a *GET* request for the root URL (`/`). The next lines are *headers*, which provide additional information about the request. + +The server then responds with a status code (here *200*, which represents success), headers, and the content, e.g: + +``` +HTTP/1.1 200 OK +Date: Wed, 08 Jan 2024 23:1:05 GMT +Content-Type: text/html; charset=UTF-8 +Content-Length: 5 +Server: Apache/2.4.51 (Unix) +Connection: close + +hello +``` + +When you understand that all web applications communicate like this, and your programming framework lets you easily interact with this, you will have no limitations on what you can build. Having said that, working directly with HTTP's text protocol is not easy, which is why the higher-level ASGI protocol exists. It makes all of HTTP available to the Python programmer in a simpler form. In addition, HTMX allows the browser to more fully utilise HTTP. +""" + +s4 = """ +In the previous section, the server responded with the body "`hello`". But in practice, web server responses today generally are either HTML or JSON. With FastHTML (as we'll see in the [HTMX technology section](/tech#sec2)), our responses are nearly always HTML. Here's an example of a basic HTML page with a header and a body containing a paragraph (`

` tag). + +```html + + Example +

Hello World!

+ +``` + +HTML creates structure, and the browser converts the HTML internally into a Document Object Model (DOM) element tree. To add styling to the browser's representation of a document, we add styles using *CSS*. One approach is to manually define styles in a CSS file---for instance [here's the stylesheet](https://github.com/AnswerDotAI/fh-bootstrap/blob/main/fh_bootstrap/assets/fh-bootstrap.css#L1) we're using for the site you're reading now, with the CSS rule which gives the above HTML block a light grey border and background. + +Most styles in most FastHTML applications won't be manually defined, but instead will come from a CSS framework like Bootstrap, DaisyUI, or Shoelace. FastHTML makes these easily available as [FT components](/components). + +Although most of the logic of your application will generally be written in Python and made available over HTTP using FastHTML, you might well want some self-contained UI updates to happen directly in the browser. For this, you can write JavaScript and add it to the web page using FastHTML. This is not often strictly required, but can make some parts of your app faster, more concise, or add some convenient functionality from the browser's DOM API. For instance, we often add a "Copy" button with sample code in our apps, which requires using the DOM API, and therefore requires adding a little JavaScript. JavaScript was originally designed for this purpose, so it's a particularly good fit for adding client-side behaviours to applications. + +To learn how to add JS libraries to FastHTML, it can help to look at examples. FastHTML includes modules for a number of popular JS libraries, such as [Marked.js](https://marked.js.org/). To see how this is implemented, have a look at the [seven lines of source code](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/js.py#L7) for `MarkdownJS` in Python. +""" + diff --git a/about/main.py b/about/main.py new file mode 100644 index 0000000000000000000000000000000000000000..d116b35b5c648e5e07f07c8facf11945bb0f9cc1 --- /dev/null +++ b/about/main.py @@ -0,0 +1,19 @@ +import about.vision, about.overview, about.foundations, about.tech, about.components +from fasthtml.common import * +from monsterui.all import * + +hdrs = ( + *Theme.blue.headers(highlightjs=True, mode='light'), + Script(src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/html.min.js"), + *Socials(title='About FastHTML', description='Learn the foundations of FastHTML', site_name='fastht.ml/about', + twitter_site='@answerdotai', image=f'/assets/og-sq.png', url=''), +) + +app,rt = fast_app(hdrs=hdrs) +app.get('/')(about.overview.page) +app.get('/components')(about.components.page) +app.get('/foundation')(about.foundations.page) +app.get('/tech')(about.tech.page) +app.get('/vision')(about.vision.page) + +serve() diff --git a/about/overview.py b/about/overview.py new file mode 100644 index 0000000000000000000000000000000000000000..5028f4bd526fefa8881822e1ce69242e510a7731 --- /dev/null +++ b/about/overview.py @@ -0,0 +1,60 @@ +from about.app import * + +def page(): + caption = "'Real' web development shouldn't be this hard..." + fig = Figure(DivCentered(Img(src='/assets/webdev.jpg', alt='Web dev'), Caption(caption, cls=TextT.lg+'pt-4')), cls='float-right w-1/2 m-4') + h2s = 'Getting started', 'Background', 'Current Status' + txts = [Markdown(s1), Div(fig, Markdown(s2)), Markdown(s3)] + secs = Sections(h2s, txts) + return BstPage(0, 'About FastHTML', h2s, *secs) + +s1 = """ +FastHTML is a new way to create modern interactive web apps. It scales down to a 6-line python file, and scales up to complex production apps. Auth, DBs, caching, styling, etc are all built-in, and replaceable and extensible. 1-click deploy is available to Railway, Vercel, Huggingface, and more---or deploy to any Python server or VPS, including Azure, GCP, and AWS. + +You're using a FastHTML app right now. We didn't create a separate blog system for this site, because building apps with FastHTML is so easy there's no need for it! Here is the [source code](https://github.com/AnswerDotAI/fh-about/blob/main/overview.py) for the current page, for instance. You'll see that the code is very simple, relying on Python components like `Markdown` to build the page. The components are simple Python functions---here is the [source code for `Markdown`](https://github.com/AnswerDotAI/fh-about/blob/main/app.py#L6), taking just one line of code! Out of the box FastHTML provides authentication, [database](/tech#sec5) access, styles (via [PicoCSS](https://picocss.com/)), and more. Every part of the system is extensible and replacable using pip-installable Python modules. + +The site you're reading right now provides background information about the key concepts and ideas behind FastHTML. The [documentation](/docs) focuses on the code. Because FastHTML brings together many different web technologies, it's worth investing some time to understand how it all fits together. Have a look through the five sections in the green navbar (or hamburger menu if you're on mobile) above to deepen your understanding. As legendary Python coder and "Two Scoops of Django" co-author Audrey Roy Greenfeld told us: + +> "*I think the fact that an experienced web dev can get productive in 1 hour accidentally undersells FastHTML a bit. For me it is like a fractal where the more I explore, the more interesting is and the more I learn. I'm about 40 hours in, enough to realise I know nothing compared with what I can learn.*" + +If you're an experienced web dev, then you can use all your knowledge of CSS, HTML, JS, etc. to build web applications with FastHTML right away. We've heard from expert coders that they have successfully built complete web apps within an hour of getting started with FastHTML. We've got a [Quickstart for Web Developers](/docs/tutorials/quickstart_for_web_devs.html) tutorial that will get you up and running quickly. (Read the rest of the docs while you're there!) Next, read through the heavily-commented source of this [idiomatic fasthtml app](https://github.com/AnswerDotAI/fasthtml/blob/main/examples/adv_app.py). Then study some of the [fasthtml-example applications](https://github.com/AnswerDotAI/fasthtml-example), particularly the first four listed. + +If you haven't done much (or any) web development, try following through each step of the [FastHTML By Example](/docs/tutorials/by_example.html) tutorial. We don't yet have a self-contained guide explaining all the web foundations you'll need to know (HTML, HTTP, CSS, etc.), so you'll probably need to do some self-learning through other resources. But watch this space---we're planning a complete web programming from scratch course soon! In the meantime, here's a 1-hour video lesson to help you get going: + +
+ +
+""" + +s2 = """ +FastHTML is a system for writing web applications in Python. It is designed to be simple, powerful, and flexible. It is also designed to be easy to learn and use. The project is inspired by technologies such as React JSX, Hotwire, Astro, FastAPI, and Phoenix LiveView. FastHTML is small and simple---at the time of writing, it's under 1000 lines of code. That's because it's built on top of powerful and flexible foundations: Python, Starlette, Uvicorn, and HTMX. If you're a FastAPI user, much of FastHTML will look very familar; FastAPI was a major inspiration. + +FastHTML was originally started by Jeremy Howard at Answer.AI for a number of reasons: + +- Over 25 years of web development, Jeremy realized that web programming could be easier and more powerful. He was particularly concerned that recent trends had moved away from the power of the web's foundations, resulting in a fractured ecosystem of over-complex frameworks and tools +- He saw that two small but ingenious developments had made the web's foundations more powerful and more accessible: **ASGI** and **HTMX**. But the tools available for using them were still too complex, and the barriers to entry were still too high +- Jeremy and his wife Rachel had spent the last 8 years working to make artificial intelligence accessible to more people. They saw that the most widely used web development tools were too complex for people who aren't full time coders. This meant that Jeremy and Rachel's students struggled to turn their AI project ideas into working applications. +- Jeremy's goal for Answer.AI is to help society benefit from AI, which means creating lots of useful products and services that use AI effectively---so creating those products and services needs to be made as fast and easy as possible. + +FastHTML is a framework that deals with all these issues: it returns to the roots of the web, leveraging ASGI and HTMX, and is usable by both experienced developers and new coders. + +#### A new generation of coders + +Coding is the key to turning the ideas in your head into products and services that can help people. AI has recently made it easier to get started with coding, which means there are more people than ever before who can create useful stuff. + +But this new generation of coders do not generally have the same background as full-time software engineers. They may have been trained in a different field, or they may have learned to code on their own. We hope that FastHTML will make it easier for this new generation of coders to turn their ideas into reality. To create maintainable and scalable solutions. +""" + +s3 = """ +FastHTML works well right now, but it is still young. We are using it for nearly every part of the FastHTML project itself ("almost" because the docs are using [Quarto](https://quarto.org/) for now; we plan to port them to a FastHTML-based documentation system soon). For instance, we worked with a design team to create the [fastht.ml home page](https://www.fastht.ml/), which is implemented in FastHTML---here is the [home page source](https://github.com/AnswerDotAI/home-fasthtml/blob/main/main.py). + +We're working on a number of things to make FastHTML even better. Not everything is ready "out of the box" yet. If you see something missing that you need, please let us know by [creating an issue](https://github.com/AnswerDotAI/fasthtml/issues). Or feel free to add it yourself and send in a pull request! + +The plan is for FastHTML to do just about everything that frameworks like Django, NextJS, and Ruby on Rails do, but it'll take a while to get there! For experienced developers, adding bindings to CSS frameworks, pypi Python modules, and JS libraries is straightforward---if you add one, please put your binding module on pypi so that the community can use it, and let us know so we can link to your project. We invite you to use the "`fh-`" prefix on PyPI to make it easy to identify FastHTML packages there. + +Here's a few of the things on our short to medium term agenda: + +- OAuth support +- Support for more databases +- Support for more CSS frameworks, including DaisyUI, Bootstrap, Shoelace, and Flowbite (we've already made a start at all of these). +""" diff --git a/about/requirements.txt b/about/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..db485d257f81b7edc04105ff82ea5ac2cc16233d --- /dev/null +++ b/about/requirements.txt @@ -0,0 +1,2 @@ +python-fasthtml +monsterui \ No newline at end of file diff --git a/about/tech.py b/about/tech.py new file mode 100644 index 0000000000000000000000000000000000000000..f7fc315ed5ffd447c92201549216e683099f9816 --- /dev/null +++ b/about/tech.py @@ -0,0 +1,88 @@ +from about.app import * + +def page(): + h2s = 'Python', 'HTMX', 'Uvicorn', 'Starlette', 'SQLite' + txts = [Markdown(s1), Markdown(s2), Markdown(s3), Markdown(s4), Markdown(s5)] + secs = Sections(h2s, txts) + return BstPage(3, "FastHTML's tech stack", h2s, *secs) + +s1 = """ +Many of the largest software systems in the world are built using Python, such as much of the code for YouTube, Instagram, Dropbox, and many others. In 2019, Dropbox announced that python was their "most widely used language both for backend services and the desktop client app", with 4 million lines of code. + +If you're already a Python programmer, then you'll know how easy it is to turn your ideas into code using this language. As well as being used for large-scale systems, Python is also popular for the day-to-day work of scientists, engineers, data analysts, and so forth. + +One particular challenge for Python programmers has been that to create a modern web application, they have had to also learn JavaScript, along with a framework like React, Angular, or Vue. Even after learning all this, they still have to deal with the complexity of writing, debugging, and maintaining a multi-language system with complex interactions between the two languages and across the client-server boundary. + +With FastHTML, you'll often find you never have to write any JavaScript at all. Not only does development and debugging become much easier, but many features suddenly become easier to implement. For instance, when we wanted to add caching to speed up our [home page](https://fastht.ml), we simply added a standard decorator to the function that creates it. No need for special infrastructure, because the implementation is all in one place. ASGI makes this particularly powerful---it can handle caching, sessions, authentication, host-based redirects, sub-routing, and more, all in one place. All of this is directly accessible from FastHTML. +""" + +s2 = """ +Nowadays most web applications are built using backend systems that return a combination of JSON and HTML data over HTTP. Javascript, normally using frameworks such as React, Angular, or Vue, is used to combine the JSON and HTML together for display in the browser. This is an *"API based"* approach to web development. + +An alternative "hypermedia-based" approach, used by [HTMX](https://htmx.org/), simplifies things greatly by just returning HTML. FastHTML is designed to create hypermedia applications. Nearly all of the complexity of client-server programming vanishes when using this approach. When going to a page directly, the server will respond with a standard HTML web page: + +```html + + FastHTML Page + +

Hello World!

+ + +``` + +This can be generated using this FastHTML code: + +```python +@rt('/') +def get(): return Div(P('Hello World!'), hx_get="/change") +``` + +When clicking on this link, the server will respond with an "*HTML partial*"---that is, just a snippet of HTML which will be inserted into the existing page: + +```html +

Nice to be here!

+``` + +In this case, the returned element will replace the original `P` element (since that's the default behavior of HTMX). Our code to create this `/change` handler is: + +```python +@rt('/change') +def get(): return P('Nice to be here!') +``` + +As we discussed in the [HTMX foundations](/foundation#sec2) section, HTMX removes four critical constraints of HTML. It allows any event on any DOM element to call any HTTP method on any path and place the response anywhere in the DOM. If you haven't written a hypermedia-based application before, then we strongly recommend reading the [Hypermedia Systems book](https://hypermedia.systems/). It explains how to build hypermedia applications using HTMX; the techniques you learn there will be directly applicable to FastHTML. +""" + +s3 = """ +[Uvicorn](https://www.uvicorn.org/) is, according to its website, "an ASGI web server". What does that even mean? As [we've discussed](/foundation#sec1), ASGI is a Python API that converts HTTP requests and responses into Python function calls. Uvicorn is a web server which a web browser can talk to, and it in turn talks to an ASGI application, returning its results back to the browser. + +Most of the time you'll run your FastHTML application by simply adding one line of code to the end of your `main.py` file: `serve()`. When you do, a message will be printed letting you know that you now have a web server running on your computer, and if you click on the provided link you'll see your application running. If you look at the source code for `main.py`, you'll see that the line of code that actually runs the server is calling Uvicorn to do the work: + +```python +uvicorn.run(f"{fname}:{app}", host=host, port=port, reload=reload) +``` + +When you deploy your application, you'll often use a service provider like Railway or Vercel. The one-click deployment we provide simply calls `python main.py` for you, and the provider is responsible for connecting the port that Uvicorn is running on to a public IP address. You can also run your application on a server such as a VPS, and either set the `PORT` environment variable to `80` to make it available directly, or add a frontend server like nginx or caddy to forward requests to the port that Uvicorn is running on. +""" + +s4 = """ +Because ASGI is such a simple API (it's literally a single Python function that takes three arguments), counter-intuively that actually makes it quite complex to use. It doesn't do that much for you, so there's quite a lot of boilerplate to write in order to create an ASGI application directly to use with Uvicorn. + +[Starlette](https://www.starlette.io/) makes it much easier to create ASGI applications. It removes a lot of the boilerplate by providing a few simple abstractions, such as `Request`, `Response`, and `Route`. Reading the source code to Starlette is very informative, because you realise how little code is actually involved; it's just converting the minimal ASGI API into a more convenient set of classes and functions. + +Starlette isn't at all opinionated about how you create your web application. Therefore, other libraries have stepped in to provide more specific functionality. For instance, [FastAPI](https://fastapi.tiangolo.com/) provides a framework built on top of Starlette that adds a lot of functionality for creating JSON APIs. + +When Jeremy Howard decided he wanted to create a library to make it easier to build hypermedia applications, he used FastAPI as a role model. In fact, he went through each page of the FastAPI tutorial and attempted to replicate as much as he could, but for hypermedia applications instead of JSON APIs. The creator of FastAPI, Sebastián Ramírez, was extremely generous with his time and advice to Jeremy and helped to explain the thinking behind FastAPI's design. + +The main `FastHTML` class is actually implemented as a subclass of Starlette's `Application` class. That means that you can use any middleware, routing, and other features that are compatible with Starlette. (However, you'll often find that FastHTML provides a more convenient way to do things.) + +Although FastAPI and FastHTML are both built on top of Starlette, and FastHTML is inspired by FastAPI, there are plenty of differences, since they have different purposes. So if you've used FastAPI before, don't assume that everything will be identical! +""" + +s5 = """ +Out of the box, FastHTML provides support for SQLite, via the [Fastlite](https://answerdotai.github.io/fastlite/) library. SQLite is built in to Python, so you don't need to install anything extra. Because it uses a file to store and access the database directly from Python, it's extremely fast to access, and it's very easy to use. Fastlite provides an extremely simple API for database access, and lets you use standard Python builtin functionality such as dataclasses and dicts to read and write data. + +Older versions of SQLite were not scalable, because they didn't support concurrent reads with writes. That limitation however was resolved some years ago through the addition of [write-ahead logging](https://www.sqlite.org/wal.html) (WAL), which FastHTML uses by default. With WAL and a modern multi-core computer and fast SSD, SQLite can support large and popular websites. Systems such as [Litestream](https://litestream.io/) can be used to replicate the database to a remote server. + +Instead of Fastlite and SQLite, you can also use SQLModel, SQLAlchemy, Redis, or any other database server or data storage system. We (and the FastHTML community) will be continually adding more data storage options to FastHTML. +""" diff --git a/about/vision.py b/about/vision.py new file mode 100644 index 0000000000000000000000000000000000000000..197f44119ea5a23688047e9734b840ddf4be929d --- /dev/null +++ b/about/vision.py @@ -0,0 +1,34 @@ +from about.app import * + +def page(): + caption = "A minimal FastHTML app really is minimal." + fig = Figure(DivCentered(Img(src='assets/hello.png', alt='Web dev', cls='h-60'), Caption(caption, cls=TextT.lg+'pt-2')), cls='float-right m-2') + h2s = 'No compromise', 'Scaling down', 'Scaling up' + txts = [Markdown(s1), Div(fig, Markdown(s2)), Markdown(s3)] + secs = Sections(h2s, txts) + return BstPage(1, 'The FastHTML Vision', h2s, *secs) + +s1 = """FastHTML is a general-purpose full-stack web programming system, in the same vein as Django, NextJS, and Ruby on Rails. The vision is to make it the easiest way to create quick prototypes, and **also** the easiest way to create scalable, powerful, rich applications. + +It is important to have a system that can scale down, as well as up. That's because the best way to create a big complex application is to first create a small simple application, and then add to it in small steps. If we don't make it easy to create small, simple applications, then fewer people get started and fewer ideas get tried. + +#### Two types of tools + +Most software development platforms that make it easy to get started make it hard to scale in size and complexity. As a result, the development landscape gets segmented into "domain expert tools" like Streamlit, Gradio, and Wordpress, vs "serious programmer tools" like React and Django. + +This means that picking one of those domain expert tools is a compromise: if what you're building is really successful, then at some point you'll have to throw it away and start again---possibly in a whole different programming language. The domain expert tools generally use very high-level abstractions specific to a single tool, which means learning a new set of foundations too. And the serious programmer tools add unnecessary complexity, slowing down both learning and development, and making maintenance harder. +""" + +s2 = """ +FastHTML scales down by picking the most widely used language for "getting stuff done": Python. And then it throws away everything that makes Python web programming complicated. No templates with quirky template languages. No multi-folder multi-file project skeletons. No complex type systems. No separate JavaScript frontend. No single-framework reactive abstractions. No build step. No tree shaking. No nonsense. + +A FastHTML application can start as a single Python file. In fact, it can stay as a single Python file! You only need to break things into multiple files if _you_ decide that will help you build or maintain your software. + +FastHTML applications don't require learning about and installing separate CSS and JavaScript frameworks. You can pip install a complete style library, such as a UI toolkit or template, and use it entirely from Python. We're building FastHTML libraries for DaisyUI, Bootstrap, Shoelace, Flowbite, and more. You can use these, or create your own, and customise them all with Python. You can pip install additional functionality provided by JavaScript and Python libraries, both controllable entirely from Python. +""" + +s3 = """ +FastHTML scales up by taking advantage of the foundations of the web. Because a FastHTML application directly uses HTTP, HTML, JavaScript, and CSS, there's nothing standing between your application and the power of the web. FastHTML comes with powerful yet simple tools for function-level and handler-level caching, async, threading, HTML partials, and much more. + +The most important thing is that the fundamentals you started out with when you scaled down are identical to those you will use when you scale up! Same language, same libraries, same abstractions. As you continue on your web programming journey, all your new skills become more and more powerful! +""" diff --git a/app.css b/app.css new file mode 100644 index 0000000000000000000000000000000000000000..038121835ef65e901d954841c6beccc902f31140 --- /dev/null +++ b/app.css @@ -0,0 +1,5 @@ +/* app.css */ +@tailwind base; +@tailwind components; +@tailwind utilities; + diff --git a/assets/accordion-minus-icon.svg b/assets/accordion-minus-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..5ec5d07399d09c1de377f507a7bb94aeee4f7a11 --- /dev/null +++ b/assets/accordion-minus-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/accordion-plus-icon.svg b/assets/accordion-plus-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..dd3a769da17d7a2afb7a9c63367a2e0352320dd1 --- /dev/null +++ b/assets/accordion-plus-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/chat-bot.svg b/assets/chat-bot.svg new file mode 100644 index 0000000000000000000000000000000000000000..a2fe4a9a9c905916191c7cfc7bf0f71b53677e20 --- /dev/null +++ b/assets/chat-bot.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/fonts/geist-mono/GeistMonoVF.woff b/assets/fonts/geist-mono/GeistMonoVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..f2ae185cbfd16946a534d819e9eb03924abbcc49 Binary files /dev/null and b/assets/fonts/geist-mono/GeistMonoVF.woff differ diff --git a/assets/fonts/geist-mono/GeistMonoVF.woff2 b/assets/fonts/geist-mono/GeistMonoVF.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..fb2f024aca0a7bfd14a88b7fa74129f107a7d7ee Binary files /dev/null and b/assets/fonts/geist-mono/GeistMonoVF.woff2 differ diff --git a/assets/fonts/geist/Geist-Medium.woff b/assets/fonts/geist/Geist-Medium.woff new file mode 100644 index 0000000000000000000000000000000000000000..8cb9825865291b2e3dc1b2bbb2b92d5827be818b Binary files /dev/null and b/assets/fonts/geist/Geist-Medium.woff differ diff --git a/assets/fonts/geist/Geist-Medium.woff2 b/assets/fonts/geist/Geist-Medium.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a4a98d94d5835a2210908ef6e48d21dc4980ef9c Binary files /dev/null and b/assets/fonts/geist/Geist-Medium.woff2 differ diff --git a/assets/fonts/geist/GeistVF.woff b/assets/fonts/geist/GeistVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..1b62daacff96dad6584e71cd962051b82957c313 Binary files /dev/null and b/assets/fonts/geist/GeistVF.woff differ diff --git a/assets/fonts/geist/GeistVF.woff2 b/assets/fonts/geist/GeistVF.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9983e92a03de482b5708201694101c330b6ae9b8 Binary files /dev/null and b/assets/fonts/geist/GeistVF.woff2 differ diff --git a/assets/footer-path.svg b/assets/footer-path.svg new file mode 100644 index 0000000000000000000000000000000000000000..e99ce871ecc85a3a0a14cc27b3ffabd8c8603ee4 --- /dev/null +++ b/assets/footer-path.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/footer-shapes.svg b/assets/footer-shapes.svg new file mode 100644 index 0000000000000000000000000000000000000000..b5df47429643dbffde35a2e466b3a54887ca2642 --- /dev/null +++ b/assets/footer-shapes.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/game-of-life.svg b/assets/game-of-life.svg new file mode 100644 index 0000000000000000000000000000000000000000..bf598051bfcf8f0261abb7a01ecb8fdeb0b6abbb --- /dev/null +++ b/assets/game-of-life.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/assets/github-mark/github-mark-white.png b/assets/github-mark/github-mark-white.png new file mode 100644 index 0000000000000000000000000000000000000000..50b81752278d084ba9d449fff25f4051df162b0f Binary files /dev/null and b/assets/github-mark/github-mark-white.png differ diff --git a/assets/github-mark/github-mark-white.svg b/assets/github-mark/github-mark-white.svg new file mode 100644 index 0000000000000000000000000000000000000000..d5e64918546d9dd87c9742239deae2397e634343 --- /dev/null +++ b/assets/github-mark/github-mark-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/github-mark/github-mark.png b/assets/github-mark/github-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..6cb3b705d018006a2bd4200ea94c9d5fb98b6f76 Binary files /dev/null and b/assets/github-mark/github-mark.png differ diff --git a/assets/github-mark/github-mark.svg b/assets/github-mark/github-mark.svg new file mode 100644 index 0000000000000000000000000000000000000000..37fa923df33faeccc4b55228046b5b079a82926d --- /dev/null +++ b/assets/github-mark/github-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/hello.png b/assets/hello.png new file mode 100644 index 0000000000000000000000000000000000000000..63e9d0726080e9d69271635a2dd546e7805d4d7d Binary files /dev/null and b/assets/hello.png differ diff --git a/assets/hero-shapes.svg b/assets/hero-shapes.svg new file mode 100644 index 0000000000000000000000000000000000000000..51af39c739ee1dbd04ce9662e561363de497cce0 --- /dev/null +++ b/assets/hero-shapes.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/htmx-meme.png b/assets/htmx-meme.png new file mode 100644 index 0000000000000000000000000000000000000000..85124dbcdb97e8be068d9aa6bc40d40636881b70 --- /dev/null +++ b/assets/htmx-meme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e95b9a8e0c127e30c0ece4919a524560f7a0418a75b7ebc360bddde9a3511134 +size 247658 diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg new file mode 100644 index 0000000000000000000000000000000000000000..b05123b333621e8e6ee695b8bc0810c6fcf76c5d --- /dev/null +++ b/assets/icons/arrow-left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg new file mode 100644 index 0000000000000000000000000000000000000000..37c3772a2a921fe73be7e9974ae0a2cff50d97a9 --- /dev/null +++ b/assets/icons/arrow-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/arrow-up-right-white.svg b/assets/icons/arrow-up-right-white.svg new file mode 100644 index 0000000000000000000000000000000000000000..5a911d0c360c9c5e21b3725d6cc8162a08537d38 --- /dev/null +++ b/assets/icons/arrow-up-right-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/arrow-up-right.svg b/assets/icons/arrow-up-right.svg new file mode 100644 index 0000000000000000000000000000000000000000..415527a565fd00d9ed53c267b20e965ac431ff36 --- /dev/null +++ b/assets/icons/arrow-up-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/copy-icon.svg b/assets/icons/copy-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..0148a9a23c1ec49a1eb64d126ce8eef91c38ed4d --- /dev/null +++ b/assets/icons/copy-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/dot.svg b/assets/icons/dot.svg new file mode 100644 index 0000000000000000000000000000000000000000..07d55fce4e04c6665cb29a251380f2edcc77786f --- /dev/null +++ b/assets/icons/dot.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/minus-icon.svg b/assets/icons/minus-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..056e05f2e8e78fbd6997679acf253ebe806f874a --- /dev/null +++ b/assets/icons/minus-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/plus-icon.svg b/assets/icons/plus-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..47b691ee54481778ff947235a28e614d5bd3cf28 --- /dev/null +++ b/assets/icons/plus-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/stack/asgi_logo-color.svg b/assets/icons/stack/asgi_logo-color.svg new file mode 100644 index 0000000000000000000000000000000000000000..6f23210907f382fe4bbff65ff02671aa9022fabb --- /dev/null +++ b/assets/icons/stack/asgi_logo-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/stack/html-1.svg b/assets/icons/stack/html-1.svg new file mode 100644 index 0000000000000000000000000000000000000000..c62fabbfe156384373311e78e9252d14418419db --- /dev/null +++ b/assets/icons/stack/html-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/stack/html.svg b/assets/icons/stack/html.svg new file mode 100644 index 0000000000000000000000000000000000000000..802ce34c3021e8906608ed1dc73bef9dd41bf8b1 --- /dev/null +++ b/assets/icons/stack/html.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/stack/htmx.svg b/assets/icons/stack/htmx.svg new file mode 100644 index 0000000000000000000000000000000000000000..fe74b103fa810710c0752835039c98bafc31c764 --- /dev/null +++ b/assets/icons/stack/htmx.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/stack/http-domain-svgrepo-com.svg b/assets/icons/stack/http-domain-svgrepo-com.svg new file mode 100644 index 0000000000000000000000000000000000000000..1a1658a0fc072750017ce50b7ceb96ff0ae4b17b --- /dev/null +++ b/assets/icons/stack/http-domain-svgrepo-com.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/stack/hugging-face.svg b/assets/icons/stack/hugging-face.svg new file mode 100644 index 0000000000000000000000000000000000000000..b74a169e27b4a29946761dc510bc43ab3a6dadc0 --- /dev/null +++ b/assets/icons/stack/hugging-face.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/stack/python-anywhere.svg b/assets/icons/stack/python-anywhere.svg new file mode 100644 index 0000000000000000000000000000000000000000..05416b4dd8c59c81c96bdb525d7d28631ff2eebb --- /dev/null +++ b/assets/icons/stack/python-anywhere.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/stack/python.svg b/assets/icons/stack/python.svg new file mode 100644 index 0000000000000000000000000000000000000000..ddc0b81bc4a0777a359d3d6911a4f0aaa6070fe0 --- /dev/null +++ b/assets/icons/stack/python.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/stack/railway.svg b/assets/icons/stack/railway.svg new file mode 100644 index 0000000000000000000000000000000000000000..d6e3d9b3337dc93905740ad77ae04a9e5d00b0de --- /dev/null +++ b/assets/icons/stack/railway.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/stack/sqlite370_banner.gif b/assets/icons/stack/sqlite370_banner.gif new file mode 100644 index 0000000000000000000000000000000000000000..c63fdc4556093ceb0ba41287faeaab0e1690304e Binary files /dev/null and b/assets/icons/stack/sqlite370_banner.gif differ diff --git a/assets/icons/stack/starlette.svg b/assets/icons/stack/starlette.svg new file mode 100644 index 0000000000000000000000000000000000000000..3694718bfbd666eea878a641cb5dcbf4d5a3b2b7 --- /dev/null +++ b/assets/icons/stack/starlette.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/stack/uvicorn.png b/assets/icons/stack/uvicorn.png new file mode 100644 index 0000000000000000000000000000000000000000..cf6a400072c7be2e14aa8329d6a8ee8277871242 Binary files /dev/null and b/assets/icons/stack/uvicorn.png differ diff --git a/assets/icons/stack/vercel.svg b/assets/icons/stack/vercel.svg new file mode 100644 index 0000000000000000000000000000000000000000..97232d5cea5786231b874fbcf9027a7f4af3d796 --- /dev/null +++ b/assets/icons/stack/vercel.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/youtube-icon.svg b/assets/icons/youtube-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..260e60057c70dad9c7b2a0ea7264b6b6003ba992 --- /dev/null +++ b/assets/icons/youtube-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/youtube.svg b/assets/icons/youtube.svg new file mode 100644 index 0000000000000000000000000000000000000000..38d1e2376323c67459f1e8f1ec2101037367520c --- /dev/null +++ b/assets/icons/youtube.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/intro-poster.png b/assets/intro-poster.png new file mode 100644 index 0000000000000000000000000000000000000000..6601e80456cb8e6ad73f19a808f090e1792ebef9 Binary files /dev/null and b/assets/intro-poster.png differ diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..894ade8760968eba94089d83723962ef18660060 --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/og-image.png b/assets/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..1bc01ccee8e875ee84cb76cf0d127bd21520f752 --- /dev/null +++ b/assets/og-image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce67e4709097c59f347b6c047f2314d799595f08fa98808575ea8e183bdfcd94 +size 196818 diff --git a/assets/og-sq.png b/assets/og-sq.png new file mode 100644 index 0000000000000000000000000000000000000000..ada61dfe2eafd8d67ed4f5b87b14db27a0dfa8c2 --- /dev/null +++ b/assets/og-sq.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:397327f6f0054ad72cc8d15a6250b0dcb9d9a36a79a2c833fa2f93bf7ac2d52d +size 122987 diff --git a/assets/pictionary-ai.svg b/assets/pictionary-ai.svg new file mode 100644 index 0000000000000000000000000000000000000000..09f9eada9474ca4cf867f2372ba2b098aa06f799 --- /dev/null +++ b/assets/pictionary-ai.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/assets/testimonials/daniel-roy-greenfield.png b/assets/testimonials/daniel-roy-greenfield.png new file mode 100644 index 0000000000000000000000000000000000000000..9187ed16ac350052db5719ac1d02614b61808ad5 Binary files /dev/null and b/assets/testimonials/daniel-roy-greenfield.png differ diff --git a/assets/testimonials/daniel-roy.png b/assets/testimonials/daniel-roy.png new file mode 100644 index 0000000000000000000000000000000000000000..9187ed16ac350052db5719ac1d02614b61808ad5 Binary files /dev/null and b/assets/testimonials/daniel-roy.png differ diff --git a/assets/testimonials/giles-thomas.png b/assets/testimonials/giles-thomas.png new file mode 100644 index 0000000000000000000000000000000000000000..1186c1f63f49d570e0c98321684c2ce428bb8515 Binary files /dev/null and b/assets/testimonials/giles-thomas.png differ diff --git a/assets/testimonials/guillermo-rauch.jpg b/assets/testimonials/guillermo-rauch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fbfdcf8d8479290997cb3c7376ab87d13702ece0 Binary files /dev/null and b/assets/testimonials/guillermo-rauch.jpg differ diff --git a/assets/testimonials/jake-cooper.png b/assets/testimonials/jake-cooper.png new file mode 100644 index 0000000000000000000000000000000000000000..93902acae0ccd3287380cb21d5df2795ac3b85e1 Binary files /dev/null and b/assets/testimonials/jake-cooper.png differ diff --git a/assets/thumb.png b/assets/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..7edfd6f9aceb6c9f7d13adb4eb58782c124fdc6c --- /dev/null +++ b/assets/thumb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe3c529ce0bcdc272b110a969f753e7309294aa3a3da6e170c50dfb356584ad8 +size 120371 diff --git a/assets/todo.svg b/assets/todo.svg new file mode 100644 index 0000000000000000000000000000000000000000..387cd7d050ae4d0d404f1947ca8a33e5c27b5fa9 --- /dev/null +++ b/assets/todo.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/webdev.jpg b/assets/webdev.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d6ff90829848a5361c1284830a04113f2888942e --- /dev/null +++ b/assets/webdev.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:663e7f5313daf80897ffa7262338e1622a397322ec45f5a67ef48586349121a4 +size 183300 diff --git a/card3d.css b/card3d.css new file mode 100644 index 0000000000000000000000000000000000000000..566ec280314afe15fca0a893c13e58bd3e43a10b --- /dev/null +++ b/card3d.css @@ -0,0 +1,15 @@ +me { + position: relative; width: 225px; height: 300px; padding: 1em; display: flex; + flex-direction: column; justify-content: flex-end; + font-weight: bold; text-align: var(--tpl-align); text-shadow: 0 0 4px #000; color: #ddd; + -webkit-text-stroke: 1px black; margin: 1em; font-size: 1.25em; + box-shadow: 0 1px 5px #00000099; border-radius: 10px; background: var(--tpl-background) center/cover; + transition: .3s ease-out; transition-property: transform, box-shadow; + touch-action: none; /* Prevents scrolling on touch devices */ +} +me:hover { transition-duration: .15s; box-shadow: 0 5px 20px 5px #00000044; } +me > div { + position: absolute; inset: 0; border-radius: 10px; + background-image: radial-gradient(circle at 90% -20%, #ffffff55, #0000000f); + pointer-events: none; /* Allows touch events to pass through */ +} \ No newline at end of file diff --git a/card3d.js b/card3d.js new file mode 100644 index 0000000000000000000000000000000000000000..3ca9a840234c3c154f7a6dfe2c6090e4104ac315 --- /dev/null +++ b/card3d.js @@ -0,0 +1,44 @@ +me().on('mouseenter', ev => { + let e = me(ev) + e.bounds = e.getBoundingClientRect() + e.on('mousemove', e.rotateToPointer) +}).on('mouseleave', ev => { + let e = me(ev) + e.off('mousemove', e.rotateToPointer) + e.style.transform = e.style.background = '' +}) + +// Use a separate function for touch events to allow passive listener +function handleTouchStart(ev) { + let e = me(ev) + e.bounds = e.getBoundingClientRect() + e.addEventListener('touchmove', e.rotateToPointer, { passive: true }) + e.addEventListener('touchend', handleTouchEnd, { passive: true }) +} + +function handleTouchEnd(ev) { + let e = me(ev.target) + e.removeEventListener('touchmove', e.rotateToPointer) + e.removeEventListener('touchend', handleTouchEnd) + e.style.transform = e.style.background = '' +} + +me().addEventListener('touchstart', handleTouchStart, { passive: true }) + +me().rotateToPointer = ev => { + let e = me(ev.target), b = e.bounds + let x, y + if (ev.type === 'touchmove') { + x = ev.touches[0].clientX - b.x - b.width / 2 + y = ev.touches[0].clientY - b.y - b.height / 2 + } else { + x = ev.clientX - b.x - b.width / 2 + y = ev.clientY - b.y - b.height / 2 + } + let d = Math.hypot(x,y) + let amt = {amt} + e.style.transform = `scale3d(${ 1 + 0.07 * amt }, ${ 1 + 0.07 * amt }, 1.0) + rotate3d(${ y/100*amt }, ${ -x/100*amt }, 0, ${ Math.log(d)*2*amt }deg)` + me('div', e).style.backgroundImage = `radial-gradient( + circle at ${ x*2 + b.width/2 }px ${ y*2 + b.height/2 }px, #ffffff77, #0000000f)` +} \ No newline at end of file diff --git a/content.py b/content.py new file mode 100644 index 0000000000000000000000000000000000000000..13446554c208a3c8443c1105926acb4b44f5999d --- /dev/null +++ b/content.py @@ -0,0 +1,145 @@ +from fasthtml.common import * +from inspect import getsource +from home_components import accordion,col,inset,bnset + +eg_url = 'https://github.com/AnswerDotAI/fasthtml-example/tree/main' +samples = [ + ("Game of life", "game-of-life.svg", f"{eg_url}/00_game_of_life"), + ("To-do", "todo.svg", f"{eg_url}/01_todo_app"), + ("Chat bot", "chat-bot.svg", f"{eg_url}/02_chatbot"), + ("Pictionary AI", "pictionary-ai.svg", f"{eg_url}/03_pictionary") +] + +from weather import all_weather + +async def weather_table(): + """Dynamically generated python content + directly incorporated into the HTML""" + # These are actual real-time weather.gov observations + try: results = await all_weather() + except: return P('Weather not available') + rows = [Tr(Td(city), *map(Td, d.values()), cls="even:bg-purple/5") + for city,d in results.items()] + flds = 'City', 'Temp (C)', 'Wind (kmh)', 'Humidity' + head = Thead(*map(Th, flds), cls="bg-purple/10") + return Table(head, *rows, cls="w-full") + +bgurl = "https://ucarecdn.com/35a0e8a7-fcc5-48af-8a3f-70bb96ff5c48/-/preview/750x1000/" +cardcss = "font-family: 'Arial Black', 'Arial Bold', Gadget, sans-serif; perspective: 1500px;" +def card_3d_demo(): + """This is a standalone isolated Python component. + Behavior and styling is scoped to the component.""" + def card_3d(text, background, amt, left_align): + # JS and CSS can be defined inline or in a file + scr = ScriptX('card3d.js', amt=amt) + align='left' if left_align else 'right' + sty = StyleX('card3d.css', background=f'url({background})', align=align) + return Div(text, Div(), sty, scr) + # Design credit: https://codepen.io/markmiro/pen/wbqMPa + card = card_3d("Mouseover me", bgurl, amt=1.5, left_align=True) + return Div(card, style=cardcss) + +a_cls="s-body text-black/80 col-span-full", +c_cls=f"{col} justify-between bg-purple/10 rounded-[1.25rem] {inset}", +acc_cls=f"{col} gap-4 transition ease-out delay-[300ms]" +qas = [ + ("What is this?", "This is a little demo of a reusable accordion component."), + ("What is FastHTML?", "FastHTML is a Python library for building web apps."), + ("What is HTMX?", "HTMX is a JavaScript library that extends browser interaction behavior.")] +def accordion_demo(): + """UI components can be styled and reused. + UI libraries can be installed using `pip`.""" + accs = [accordion(id=id, question=q, answer=a, + question_cls="text-black s-body", answer_cls=a_cls, container_cls=c_cls) + for id,(q,a) in enumerate(qas)] + return Div(*accs, cls=acc_cls) + +list_class = "m-body text-black list-disc pl-5" +db = database('data/todos.db') +class Todo: + "Use any database system you like" + id:int; title:str; done:bool + def __ft__(self): + "`__ft__` defines how FastHTML renders an object" + return Li("✅ " if self.done else "", self.title) + +todos = db.create(Todo) +def todos_table(): + "This example uses the `fastlite` DB lib" + return Ul(*todos(), cls=list_class) + +def startup(): + if not todos(): + todos.insert(title="Create sample todos", done=True) + todos.insert(title="Create a sample FastHTML app", done=True) + todos.insert(title="Read this todo list") + +async def components(): + return [ + ("Components", "card3d.py", getsource(card_3d_demo), card_3d_demo()), + ("Dynamic", "weather.py", getsource(weather_table), await weather_table()), + ("Reusable", "accordion.py", getsource(accordion_demo), accordion_demo()), + ("Databases", "todos.py", f"{getsource(Todo)}\ntodos = db.create(Todo)\n{getsource(todos_table)}", + Div(H2("DB-generated todo list", cls="text-2xl font-bold mb-4"), todos_table())) + ] + +stacked = [ + ("Build on solid foundations", "FastHTML stands on the shoulders of giants:", [ + ("ASGI", "asgi_logo-color.svg", "https://fastht.ml/about/foundation#sec1"), + ("HTMX", "htmx.svg", "https://fastht.ml/about/foundation#sec1"), + ("HTTP", "http-domain-svgrepo-com.svg", "https://fastht.ml/about/foundation#sec3"), + ("HTML", "html-1.svg", "https://fastht.ml/about/components"), + ]), + ("Use tools you already know", "FastHTML embraces the familiar:", [ + ("Python", "python.svg", "https://fastht.ml/about/tech#sec1"), + ("Uvicorn", "uvicorn.png", "https://fastht.ml/about/tech#sec3"), + ("Starlette", "starlette.svg", "https://fastht.ml/about/tech#sec4"), + ("SQLite", "sqlite370_banner.gif", "https://fastht.ml/about/tech#sec5"), + ]), + ("Deploy anywhere", "FastHTML runs anywhere Python does, including 1-click deploy to:", [ + ("Railway", "railway.svg", "https://railway.app/"), + ("Vercel", "vercel.svg", "https://vercel.com/templates/python/fasthtml-python-boilerplate"), + ("Hugging Face", "hugging-face.svg", "https://huggingface.co/"), + ("PythonAnywhere", "python-anywhere.svg", "https://www.pythonanywhere.com/"), + ]), +] + +benefits = [ + ("Get started fast", "A single Python file is all that's needed to create any app you can think of. Or bring in any Python or JS library you like."), + ("Flexibility", "FastHTML provides full access to HTTP, HTML, JS, and CSS, bringing the foundations of the web to you. There's no limits to what you can build."), + ("Speed & scale", "FastHTML applications are fast and scalable. They're also easy to deploy, since you can use any hosting service that supports Python.") +] + +faqs = [ + ("What kinds of applications can be written with this?", + "It's good for: general purpose web applications (i.e anything you'd build with React, Django, Next.js, etc); quick dashboards, prototypes, and in-company apps (e.g. like what you might use gradio/streamlit/etc for); Analytics/models/dashboards interactive reports; Custom blogs and content-heavy sites where you also want some interactive/dynamic content."), + ("Where can I deploy my FastHTML to? What's needed?", + "You can deploy a FastHTML app to any service or server that supports Python. We have guides and helpers for Railway.app, Vercel, Hugging Face Spaces, Replit, and PythonAnywhere. You can also use any VPS or server, or any on-premise machine with Python installed. All major operating systems are supported."), + ("How does FastHTML relate to FastAPI?", + "FastAPI is one of the inspirations for FastHTML. We are fans of its developer experience and tried to make FastHTML extremely familiar for FastAPI users. FastAPI is designed for creating APIs, whereas FastHTML is designed for creating HTML (i.e \"Hypermedia applications\"). Anything you could create with FastAPI (plus a JS frontend), you could also create with FastHTML, and vice versa -- if you prefer mainly writing JS, you might prefer FastAPI, since you can move a lot of client-side logic into the JS. If you prefer mainly writing Python, you'll probably want to use FastHTML, since you can often avoid using JS entirely."), + ("Is this only for multi-page \"old style\" web apps, or can FastHTML be used for modern SPA apps too?", + "FastHTML is specifically designed to make writing modern SPA apps as fast and easy as possible, whilst also ensuring the apps you write are scalable and performant. By default, FastHTML routes return lightweight \"partials\" that update the DOM directly, rather than doing a full page refresh."), + ("What is HTMX, and what's it got to do with FastHTML?", + "HTMX is best thought of as filling in the missing bits of a web browser -- in fact, web browser manufacturers are considering incorporating similar features directly into future browsers. It is a small javascript library that with a single line of HTML lets you respond to any event from any part of a web page by modifying the DOM in any way you like, all directly from Python. Whilst you don't have to use it with FastHTML, it will dramatically increase the amount of stuff you can do!"), + ("Do I need to know JS? Can I use it if I want, with FastHTML?", + "No, and yes! You can write nearly any standard web app with just Python. However, using a bit of JS can be helpful -- for instance, nearly any existing JS lib can be incorporated into a FastHTML app, and you can sprinkle bits of JS into your pages anywhere you like."), + ("Are FastHTML apps slower than React, Next.js, etc?", + "It depends. Apps using FastHTML and HTMX are often faster than JS-based approaches using big libraries, since they can be very lightweight.") +] + +# Create images with: +# `magick input.jpg -alpha set -background none -vignette 0x0+0+0 -crop 100%x100%+0+0 +repage -resize 112x112 output.png` +gr_test = Div(Ul(Li('ergonomic af'), Li('fast af'), Li('slick af'), Li('SSR af'), + style="list-style-type: square;", cls="m-body text-black"), + P(A('(From twitter)', href='https://x.com/rauchg/status/1807965785585078773', cls="border-b-2 border-b-black/30 hover:border-b-black/80"))) +testimonials = [ + ("FastHTML is as intuitive as FastAPI, lends itself to clean architecture, and its HTML+HTMX structure makes it a good competitor to Django for building webapps. Most importantly, it's fun to use.", + "Daniel Roy Greenfeld", "Co-author", "Two Scoops of Django", "assets/testimonials/daniel-roy.png"), + ("Python has always been a wonderful tool for creating web applications; with FastHTML, it's even better!", + "Giles Thomas", "Founder", "PythonAnywhere", "assets/testimonials/giles-thomas.png"), + ("With FastHTML and Railway, Pythonistas can now have a real web application running in minutes, and can scale it all the way up to sophisticated production deployments.", + "Jake Cooper", "CEO", "Railway.app", "assets/testimonials/jake-cooper.png"), + (gr_test, "Guillermo Rauch", "CEO", "Vercel", "assets/testimonials/guillermo-rauch.jpg"), + # ("FastHTML is a breath of fresh air. It's simple, it's fast, and it's fun to use.", + # "Jake Cooper", "CEO", "Railway.app", "assets/testimonials/jake-cooper.png"), +] diff --git a/css/highlighter-theme.css b/css/highlighter-theme.css new file mode 100644 index 0000000000000000000000000000000000000000..8392c3e73916e6c90df37862bca33bb487a36772 --- /dev/null +++ b/css/highlighter-theme.css @@ -0,0 +1,43 @@ +.hljs { + display: block; + + overflow-x: auto; + padding: 0.5em; + background: #3a2234; + color: #939eeb; + font-family: "Geist Mono"; +} + +.hljs-keyword, +.hljs-built_in, +.hljs-title.function_, +.hljs-title.class_, +.hljs-params, +.hljs-meta { + color: #939eeb; + font-weight: 500; +} + +.hljs-symbol { + color: white; +} + +.hljs-string, +.hljs-subst, +.hljs-attr-name { + color: #ff8087; +} + +.hljs-ln-numbers { + color: #ffffff99; + font-weight: 500; + text-align: right; +} + +.hljs-number { + color: #5af2ba; +} +.hljs-white-char, +.hljs-params .hljs-white-char { + color: white; +} diff --git a/css/input.css b/css/input.css new file mode 100644 index 0000000000000000000000000000000000000000..b5c61c956711f981a41e95f7fcf0038436cfbb22 --- /dev/null +++ b/css/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000000000000000000000000000000000000..1c8e978d20e09e0270e7a890f9f02c9abc24cc40 --- /dev/null +++ b/css/main.css @@ -0,0 +1,274 @@ +@font-face { + font-family: "Geist"; + src: url("../assets/fonts/geist/GeistVF.woff2") format("woff2"), + url("../assets/fonts/geist/GeistVF.woff") format("woff"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "Geist"; + src: url("../assets/fonts/geist/Geist-Medium.woff") format("woff"), + url("../assets/fonts/geist/Geist-Medium.woff2") format("woff2"); + font-weight: 500; + font-style: normal; +} +@font-face { + font-family: "Geist Mono"; + src: url("../assets/fonts/geist-mono/GeistMonoVF.woff2") format("woff2"), + url("../assets/fonts/geist-mono/GeistMonoVF.woff") format("woff"); + font-weight: normal; + font-style: normal; +} + +.font-details-off { + font-feature-settings: "clig" off, "liga" off; +} + +.heading-1 { + font-family: Geist; + font-size: 56px; + font-style: normal; + font-weight: 500; + line-height: 64px; /* 114.286% */ + letter-spacing: -0.84px; +} + +.heading-2 { + font-size: 2.5rem; + font-style: normal; + font-weight: 500; + line-height: 3rem; /* 120% */ + letter-spacing: -0.0375rem; +} + +.xs-mono-body { + font-family: "Geist Mono", monospace; + font-size: 0.875rem; + font-style: normal; + font-weight: 500; + line-height: 180%; /* 1.575rem */ +} +.mono-body { + font-family: "Geist Mono", monospace; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: 180%; /* 1.8rem */ +} + +.mono-s { + font-family: "Geist Mono", monospace; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 180%; /* 28.8px */ +} + +.xs-mono-body { + font-family: "Geist Mono"; + font-size: 0.875rem; + font-style: normal; + font-weight: 500; + line-height: 180%; /* 1.575rem */ +} + +.regular-body, +.s-body { + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 1.5rem; /* 150% */ + letter-spacing: -0.005rem; +} + +.m-body { + font-family: "Geist", sans-serif; + font-size: 1.25rem; + font-style: normal; + font-weight: 400; + line-height: 1.75rem; /* 140% */ + letter-spacing: -0.0125rem; +} + +.l-body { + font-size: 1.5rem; + font-style: normal; + font-weight: 400; + line-height: 2rem; /* 133.333% */ + letter-spacing: -0.015rem; +} + +.heading-3 { + font-size: 1.5rem; + font-style: normal; + font-weight: 500; + line-height: 2rem; /* 133.333% */ + letter-spacing: -0.015rem; +} + +.hide-scrollbar::-webkit-scrollbar { + display: none; +} + +.hide-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.minus-icon { + display: none; +} + +.collapsible-checkbox:checked + label .plus-icon { + display: none; +} + +.collapsible-checkbox:checked + label .minus-icon { + display: inline; +} + +@media screen and (min-width: 1024px) { + .heading-1 { + font-size: 72px; + line-height: 88px; /* 122.222% */ + letter-spacing: -1.08px; + } + + .heading-2 { + font-size: 3.75rem; + line-height: 4.125rem; /* 110% */ + letter-spacing: -0.05625rem; + } + + .heading-3 { + font-size: 2rem; + line-height: 2.5rem; /* 125% */ + letter-spacing: -0.03rem; + } + + .regular-body { + font-size: 1.5rem; + line-height: 2rem; /* 133.333% */ + letter-spacing: -0.015rem; + } +} + +.rotating-shapes { + animation: orbit 16s ease-in-out infinite; + transform: translateZ(0); +} + +.rotating-shapes > * { + animation: rotate 16s ease-in-out infinite; + transform-box: fill-box; + transform-origin: center; + transform: translateZ(0); +} + +@keyframes orbit { + 0% { + transform: rotate(0deg); + } + 8% { + transform: rotate(-5deg); + } + 18.66% { + transform: rotate(98deg); + } + 25% { + transform: rotate(90deg); + } + 26.5% { + transform: rotate(90deg); + } + 33% { + transform: rotate(85deg); + } + 41.666% { + transform: rotate(188deg); + } + 48% { + transform: rotate(180deg); + } + 50% { + transform: rotate(180deg); + } + 58% { + transform: rotate(175deg); + } + 66.666% { + transform: rotate(278deg); + } + 73% { + transform: rotate(270deg); + } + 75% { + transform: rotate(270deg); + } + 83% { + transform: rotate(265deg); + } + 91.666% { + transform: rotate(368deg); + } + 98% { + transform: rotate(360deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + 8% { + transform: rotate(5deg); + } + 18.66% { + transform: rotate(-98deg); + } + 25% { + transform: rotate(-90deg); + } + 26.5% { + transform: rotate(-90deg); + } + 33% { + transform: rotate(-85deg); + } + 41.666% { + transform: rotate(8deg); + } + 48% { + transform: rotate(0deg); + } + 50% { + transform: rotate(0deg); + } + 58% { + transform: rotate(0deg); + } + 66.666% { + transform: rotate(0deg); + } + 73% { + transform: rotate(0deg); + } + 75% { + transform: rotate(0deg); + } + 83% { + transform: rotate(0deg); + } + 91.666% { + transform: rotate(0deg); + } + 98% { + transform: rotate(0deg); + } + 100% { + transform: rotate(0deg); + } +} diff --git a/css/preview-stack.css b/css/preview-stack.css new file mode 100644 index 0000000000000000000000000000000000000000..2ad27f330382cb2f6847b6d3d6e91d855a63a08a --- /dev/null +++ b/css/preview-stack.css @@ -0,0 +1,37 @@ +#preview-stacked-cards { + --cards: 3; + --cardTopPadding: 3rem; + --cardMargin: 1rem; +} + +#preview-stacked-cards { + list-style: none; + padding-left: 0; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: repeat(var(--cards), 1fr); + gap: var(--cardMargin); +} + +#preview-stacked-cards > div:nth-child(1) { + --index: 0; +} +#preview-stacked-cards > div:nth-child(2) { + --index: 1; +} +#preview-stacked-cards > div:nth-child(3) { + --index: 2; +} + +#preview-stacked-cards > div { + position: sticky; + top: 0; + padding-top: calc(var(--index) * var(--cardTopPadding)); +} + +@media screen and (max-width: 1024px) { + #preview-stacked-cards { + --cardTopPadding: 3rem; + --cardMargin: 1rem; + } +} diff --git a/css/stack.css b/css/stack.css new file mode 100644 index 0000000000000000000000000000000000000000..7c717b64c74edbbc4ab4373230ed055cd6e6ef71 --- /dev/null +++ b/css/stack.css @@ -0,0 +1,46 @@ +:root { + --cards: 3; + --cardTopPadding: 6rem; + --cardsHeaderBottomPadding: 24rem; + --cardsSectionGap: 2rem; + --cardMargin: 2rem; + --cardsSectionPaddingBottom: calc( + calc(var(--cards) - 1) * var(--cardTopPadding) + ); + --cardsHeaderBottomStop: 448px; +} + +#stacked-cards { + list-style: none; + padding-left: 0; + gap: var(--cardMargin); + padding-bottom: calc(calc(var(--cards) - 1) * var(--cardTopPadding)); + + display: flex; + flex-direction: column; +} + +#stacked-cards > div:nth-child(1) { + --index: 0; +} +#stacked-cards > div:nth-child(2) { + --index: 1; +} +#stacked-cards > div:nth-child(3) { + --index: 2; +} + +#stacked-cards > div { + transform-origin: bottom; + position: sticky; + top: 0; +} + +@media screen and (max-width: 1024px) { + :root { + --cardTopPadding: 3rem; + --cardsHeaderBottomPadding: 10rem; + --cardsSectionGap: 4rem; + --cardsHeaderBottomStop: 272px; + } +} diff --git a/css/tailwind.css b/css/tailwind.css new file mode 100644 index 0000000000000000000000000000000000000000..155414eed05c05b17c2f06ad9cd741dd2f111f0f --- /dev/null +++ b/css/tailwind.css @@ -0,0 +1 @@ +/*! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1440px){.container{max-width:1440px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.bottom-\[calc\(100\%-300px\)\]{bottom:calc(100% - 300px)}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.z-50{z-index:50}.z-\[1\]{z-index:1}.col-span-full{grid-column:1/-1}.mx-auto{margin-left:auto;margin-right:auto}.my-11{margin-top:2.75rem;margin-bottom:2.75rem}.-mt-8{margin-top:-2rem}.mb-12{margin-bottom:3rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-auto{margin-right:auto}.mt-12{margin-top:3rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1/1}.aspect-video{aspect-ratio:16/9}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-16{height:4rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[2\.75rem\]{height:2.75rem}.h-\[420px\]{height:420px}.h-\[76px\]{height:76px}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.max-h-\[1024px\]{max-height:1024px}.max-h-\[25rem\]{max-height:25rem}.max-h-fit{max-height:-moz-fit-content;max-height:fit-content}.min-h-\[720px\]{min-height:720px}.w-0{width:0}.w-11{width:2.75rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-\[10\.59375rem\]{width:10.59375rem}.w-\[120\%\]{width:120%}.w-\[200\%\]{width:200%}.w-\[4\.5rem\]{width:4.5rem}.w-\[7\.5rem\]{width:7.5rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.min-w-\[900px\]{min-width:900px}.min-w-max{min-width:-moz-max-content;min-width:max-content}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-7xl{max-width:80rem}.max-w-\[1440px\]{max-width:1440px}.max-w-\[2048px\]{max-width:2048px}.max-w-\[21rem\]{max-width:21rem}.max-w-\[32rem\]{max-width:32rem}.max-w-\[350px\]{max-width:350px}.max-w-\[36rem\]{max-width:36rem}.max-w-\[400px\]{max-width:400px}.max-w-\[40rem\]{max-width:40rem}.max-w-\[41rem\]{max-width:41rem}.max-w-\[50rem\]{max-width:50rem}.max-w-\[90rem\]{max-width:90rem}.max-w-full{max-width:100%}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-grow{flex-grow:1}.origin-center{transform-origin:center}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes negative-unbounce{0%{transform:rotate(-4deg)}to{transform:rotate(0deg)}}.animate-negative-unbounce{animation:negative-unbounce .2s ease-in-out forwards}@keyframes positive-unbounce{0%{transform:rotate(4deg)}to{transform:rotate(0deg)}}.animate-positive-unbounce{animation:positive-unbounce .2s ease-in-out forwards}.cursor-pointer{cursor:pointer}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-center{align-content:center}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-8{row-gap:2rem}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[0\.5rem\]{border-radius:.5rem}.rounded-\[1\.25rem\]{border-radius:1.25rem}.rounded-\[62\.5rem\]{border-radius:62.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-l-3xl{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem}.rounded-l-\[0\.5rem\]{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-r-\[0\.5rem\]{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-t-3xl{border-top-left-radius:1.5rem;border-top-right-radius:1.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-white\/20{border-color:#fff3}.border-b-black\/30{border-bottom-color:#0000004d}.border-b-transparent{border-bottom-color:#0000}.border-b-white\/50{border-bottom-color:#ffffff80}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-black\/20{background-color:#0003}.bg-blue{--tw-bg-opacity:1;background-color:rgb(117 117 240/var(--tw-bg-opacity))}.bg-green{--tw-bg-opacity:1;background-color:rgb(60 221 140/var(--tw-bg-opacity))}.bg-grey{--tw-bg-opacity:1;background-color:rgb(243 243 243/var(--tw-bg-opacity))}.bg-inherit{background-color:inherit}.bg-pink{--tw-bg-opacity:1;background-color:rgb(230 153 217/var(--tw-bg-opacity))}.bg-purple{--tw-bg-opacity:1;background-color:rgb(58 34 52/var(--tw-bg-opacity))}.bg-purple\/10{background-color:#3a22341a}.bg-soft-blue{--tw-bg-opacity:1;background-color:rgb(232 232 252/var(--tw-bg-opacity))}.bg-soft-green{--tw-bg-opacity:1;background-color:rgb(212 247 230/var(--tw-bg-opacity))}.bg-soft-pink{--tw-bg-opacity:1;background-color:rgb(255 204 247/var(--tw-bg-opacity))}.bg-soft-purple{--tw-bg-opacity:1;background-color:rgb(237 222 233/var(--tw-bg-opacity))}.bg-soft-yellow{--tw-bg-opacity:1;background-color:rgb(255 238 204/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/20{background-color:#fff3}.bg-white\/50{background-color:#ffffff80}.bg-white\/60{background-color:#fff9}.bg-yellow{--tw-bg-opacity:1;background-color:rgb(255 196 53/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-transparent{--tw-gradient-from:#0000 var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-\[\#3a2234\]{--tw-gradient-to:#3a2234 var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-16{padding-bottom:4rem}.pb-24{padding-bottom:6rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-20{padding-right:5rem}.pr-4{padding-right:1rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-start{text-align:start}.font-geist{font-family:Geist,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-black\/60{color:#0009}.text-black\/80{color:#000c}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-white\/80{color:#fffc}.text-opacity-60{--tw-text-opacity:0.6}.shadow-\[0_2px_2px_rgba\(255\2c 255\2c 255\2c 0\.5\)\2c 0_3px_3px_rgba\(0\2c 0\2c 0\2c 0\.2\)\]{--tw-shadow:0 2px 2px #ffffff80,0 3px 3px #0003;--tw-shadow-colored:0 2px 2px var(--tw-shadow-color),0 3px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[inset_0_2px_4px_rgba\(255\2c 255\2c 255\2c 0\.1\)\2c 0_4px_8px_rgba\(0\2c 0\2c 0\2c 0\.5\)\]{--tw-shadow:inset 0 2px 4px #ffffff1a,0 4px 8px #00000080;--tw-shadow-colored:inset 0 2px 4px var(--tw-shadow-color),0 4px 8px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-lg{--tw-backdrop-blur:blur(16px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.delay-\[300ms\]{transition-delay:.3s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.even\:bg-purple\/5:nth-child(2n){background-color:#3a22340d}.hover\:border-b-black\/80:hover{border-bottom-color:#000c}.hover\:border-b-white:hover{--tw-border-opacity:1;border-bottom-color:rgb(255 255 255/var(--tw-border-opacity))}.hover\:bg-black\/70:hover{background-color:#000000b3}.hover\:bg-black\/80:hover{background-color:#000c}.hover\:bg-white\/80:hover{background-color:#fffc}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.disabled\:opacity-40:disabled{opacity:.4}.group:hover .group-hover\:translate-x-\[-0\.34rem\]{--tw-translate-x:-0.34rem}.group:hover .group-hover\:translate-x-\[-0\.34rem\],.group:hover .group-hover\:translate-x-\[0\.29rem\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:translate-x-\[0\.29rem\]{--tw-translate-x:0.29rem}.group:hover .group-hover\:translate-y-\[-0\.13rem\]{--tw-translate-y:-0.13rem}.group:hover .group-hover\:translate-y-\[-0\.13rem\],.group:hover .group-hover\:translate-y-\[-0\.1rem\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:translate-y-\[-0\.1rem\]{--tw-translate-y:-0.1rem}.group:hover .group-hover\:translate-y-\[0\.57rem\]{--tw-translate-y:0.57rem}.group:hover .group-hover\:rotate-\[-4deg\],.group:hover .group-hover\:translate-y-\[0\.57rem\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:rotate-\[-4deg\]{--tw-rotate:-4deg}.group:hover .group-hover\:rotate-\[15deg\]{--tw-rotate:15deg}.group:hover .group-hover\:rotate-\[15deg\],.group:hover .group-hover\:rotate-\[20deg\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:rotate-\[20deg\]{--tw-rotate:20deg}.group:hover .group-hover\:rotate-\[4deg\]{--tw-rotate:4deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes negative-bounce{0%{transform:rotate(-4deg)}33%{transform:rotate(-3deg)}66%{transform:rotate(-5deg)}to{transform:rotate(-4deg)}}.group:hover .group-hover\:animate-negative-bounce{animation:negative-bounce .6s ease-in-out forwards}@keyframes positive-bounce{0%{transform:rotate(4deg)}33%{transform:rotate(3deg)}66%{transform:rotate(5deg)}to{transform:rotate(4deg)}}.group:hover .group-hover\:animate-positive-bounce{animation:positive-bounce .6s ease-in-out forwards}.peer\/collapsible:checked~.peer-checked\/collapsible\:max-h-\[30rem\]{max-height:30rem}.peer\/collapsible:checked~.peer-checked\/collapsible\:pb-4{padding-bottom:1rem}@media not all and (min-width:1024px){.max-lg\:basis-\[152px\]{flex-basis:152px}.max-lg\:flex-col{flex-direction:column}.max-lg\:items-start{align-items:flex-start}}@media (min-width:1024px){.lg\:-top-\[15\%\]{top:-15%}.lg\:mx-28{margin-left:7rem;margin-right:7rem}.lg\:mx-auto{margin-left:auto;margin-right:auto}.lg\:my-8{margin-top:2rem;margin-bottom:2rem}.lg\:-mr-16{margin-right:-4rem}.lg\:-mt-10{margin-top:-2.5rem}.lg\:-mt-16{margin-top:-4rem}.lg\:mb-8{margin-bottom:2rem}.lg\:mt-6{margin-top:1.5rem}.lg\:flex{display:flex}.lg\:h-14{height:3.5rem}.lg\:h-\[22rem\]{height:22rem}.lg\:h-\[600px\]{height:600px}.lg\:w-14{width:3.5rem}.lg\:w-96{width:24rem}.lg\:w-\[150\%\]{width:150%}.lg\:w-\[26rem\]{width:26rem}.lg\:max-w-7xl{max-width:80rem}.lg\:max-w-\[1440px\]{max-width:1440px}.lg\:max-w-\[43\.375rem\]{max-width:43.375rem}.lg\:max-w-\[45rem\]{max-width:45rem}.lg\:max-w-md{max-width:28rem}.lg\:max-w-xl{max-width:36rem}.lg\:flex-1{flex:1 1 0%}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-start{align-items:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-16{gap:4rem}.lg\:gap-6{gap:1.5rem}.lg\:gap-8{gap:2rem}.lg\:gap-x-12{-moz-column-gap:3rem;column-gap:3rem}.lg\:overflow-hidden{overflow:hidden}.lg\:rounded-t-\[2\.5rem\]{border-top-left-radius:2.5rem;border-top-right-radius:2.5rem}.lg\:p-12{padding:3rem}.lg\:p-8{padding:2rem}.lg\:px-16{padding-left:4rem;padding-right:4rem}.lg\:py-6{padding-top:1.5rem;padding-bottom:1.5rem}.lg\:pl-8{padding-left:2rem}.lg\:pr-6{padding-right:1.5rem}.lg\:pt-12{padding-top:3rem}.lg\:pt-16{padding-top:4rem}.peer\/collapsible:checked~.peer-checked\/collapsible\:lg\:pb-6{padding-bottom:1.5rem}}@media (min-width:1440px){.xl\:mx-auto{margin-left:auto;margin-right:auto}.xl\:flex{display:flex}.xl\:items-center{align-items:center}.xl\:overflow-hidden{overflow:hidden}.xl\:rounded-3xl{border-radius:1.5rem}.xl\:p-12{padding:3rem}} \ No newline at end of file diff --git a/docs/api/README.txt b/docs/api/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..7a38fd9bf511e36b9436d78b4b94946ebc8da450 --- /dev/null +++ b/docs/api/README.txt @@ -0,0 +1,2 @@ +These are the source notebooks for FastHTML. + diff --git a/docs/api/cli.html b/docs/api/cli.html new file mode 100644 index 0000000000000000000000000000000000000000..4c58daf98e670ade80c6355e76bc3bfec4bbfb39 --- /dev/null +++ b/docs/api/cli.html @@ -0,0 +1,869 @@ + + + + + + + + + +Command Line Tools – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Command Line Tools

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

source

+ +
+

railway_deploy

+
+
 railway_deploy (name:str, mount:<function bool_arg>=True)
+
+

Deploy a FastHTML app to Railway

+ + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
namestrThe project name to deploy
mountbool_argTrueCreate a mounted volume at /app/data?
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/cli.html.md b/docs/api/cli.html.md new file mode 100644 index 0000000000000000000000000000000000000000..841a46bde4ff9a95a3018b1f3b5e6d85eee87cf0 --- /dev/null +++ b/docs/api/cli.html.md @@ -0,0 +1,53 @@ +# Command Line Tools + + + + +------------------------------------------------------------------------ + +source + +### railway_link + +> railway_link () + +*Link the current directory to the current project’s Railway service* + +------------------------------------------------------------------------ + +source + +### railway_deploy + +> railway_deploy (name:str, mount:=True) + +*Deploy a FastHTML app to Railway* + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
namestrThe project name to deploy
mountbool_argTrueCreate a mounted volume at /app/data?
diff --git a/docs/api/components.html b/docs/api/components.html new file mode 100644 index 0000000000000000000000000000000000000000..c6d0154e828abc7c668c89f97f776f31a1ec53a1 --- /dev/null +++ b/docs/api/components.html @@ -0,0 +1,1365 @@ + + + + + + + + + + +Components – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Components

+
+ +
+
+ ft_html and ft_hx functions to add some conveniences to ft, along with a full set of basic HTML components, and functions to work with forms and FT conversion +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from collections import UserDict
+from lxml import html as lx
+from pprint import pprint
+
+
+

Str and repr

+

In notebooks, FT components are rendered as their syntax highlighted XML/HTML:

+
+
sentence = P(Strong("FastHTML is ", I("Fast")), id='sentence_id')
+sentence
+
+
<p id="sentence_id">
+<strong>FastHTML is <i>Fast</i></strong></p>
+
+
+

Elsewhere, they are represented as their underlying data structure:

+
+
print(repr(sentence))
+
+
p((strong(('FastHTML is ', i(('Fast',),{})),{}),),{'id': 'sentence_id'})
+
+
+
+

source

+
+
+

FT.__str__

+
+
 FT.__str__ ()
+
+

Return str(self).

+

If they have an id, then that id is used as the component’s str representation:

+
+
f'hx_target=#{sentence}'
+
+
'hx_target=#sentence_id'
+
+
+
+

source

+
+
+

FT.__radd__

+
+
 FT.__radd__ (b)
+
+
+
'hx_target=#' + sentence
+
+
'hx_target=#sentence_id'
+
+
+
+

source

+
+
+

FT.__add__

+
+
 FT.__add__ (b)
+
+
+
sentence + '...'
+
+
'sentence_id...'
+
+
+
+
+

fh_html and fh_hx

+
+

source

+
+
+

attrmap_x

+
+
 attrmap_x (o)
+
+
+

source

+
+
+

ft_html

+
+
 ft_html (tag:str, *c, id=None, cls=None, title=None, style=None,
+          attrmap=None, valmap=None, ft_cls=None, **kwargs)
+
+
+
ft_html('a', **{'@click.away':1})
+
+
<a @click.away="1"></a>
+
+
+
+
ft_html('a', {'@click.away':1})
+
+
<a @click.away="1"></a>
+
+
+
+
ft_html('a', UserDict({'@click.away':1}))
+
+
<a @click.away="1"></a>
+
+
+
+
c = Div(id='someid')
+
+
+
ft_html('a', id=c)
+
+
<a id="someid" name="someid"></a>
+
+
+
+

source

+
+
+

ft_hx

+
+
 ft_hx (tag:str, *c, target_id=None, hx_vals=None, hx_target=None,
+        id=None, cls=None, title=None, style=None, accesskey=None,
+        contenteditable=None, dir=None, draggable=None, enterkeyhint=None,
+        hidden=None, inert=None, inputmode=None, lang=None, popover=None,
+        spellcheck=None, tabindex=None, translate=None, hx_get=None,
+        hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
+        hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
+        hx_select=None, hx_select_oob=None, hx_indicator=None,
+        hx_push_url=None, hx_confirm=None, hx_disable=None,
+        hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
+        hx_headers=None, hx_history=None, hx_history_elt=None,
+        hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
+        hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,
+        hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+        hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+        hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+        hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+        hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,
+        hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,
+        hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+        hx_on__abort=None, hx_on__after_on_load=None,
+        hx_on__after_process_node=None, hx_on__after_request=None,
+        hx_on__after_settle=None, hx_on__after_swap=None,
+        hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+        hx_on__before_process_node=None, hx_on__before_request=None,
+        hx_on__before_swap=None, hx_on__before_send=None,
+        hx_on__before_transition=None, hx_on__config_request=None,
+        hx_on__confirm=None, hx_on__history_cache_error=None,
+        hx_on__history_cache_miss=None,
+        hx_on__history_cache_miss_error=None,
+        hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+        hx_on__before_history_save=None, hx_on__load=None,
+        hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+        hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+        hx_on__oob_error_no_target=None, hx_on__prompt=None,
+        hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+        hx_on__response_error=None, hx_on__send_abort=None,
+        hx_on__send_error=None, hx_on__sse_error=None,
+        hx_on__sse_open=None, hx_on__swap_error=None,
+        hx_on__target_error=None, hx_on__timeout=None,
+        hx_on__validation_validate=None, hx_on__validation_failed=None,
+        hx_on__validation_halted=None, hx_on__xhr_abort=None,
+        hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+        hx_on__xhr_progress=None, **kwargs)
+
+
+
ft_hx('a', hx_vals={'a':1})
+
+
<a hx-vals='{"a": 1}'></a>
+
+
+
+
ft_hx('a', hx_target=c)
+
+
<a hx-target="#someid"></a>
+
+
+

For tags that have a name attribute, it will be set to the value of id if not provided explicitly:

+
+
Form(Button(target_id='foo', id='btn'),
+     hx_post='/', target_id='tgt', id='frm')
+
+
<form hx-post="/" hx-target="#tgt" id="frm" name="frm"><button hx-target="#foo" id="btn" name="btn"></button></form>
+
+
+
+

source

+
+
+

File

+
+
 File (fname)
+
+

Use the unescaped text in file fname directly

+
+
a = Input(name='nm')
+a
+
+
<input name="nm">
+
+
+
+
a(hx_swap_oob='true')
+
+
<input name="nm" hx-swap-oob="true">
+
+
+
+
a
+
+
<input name="nm" hx-swap-oob="true">
+
+
+
+
+

show

+
+

source

+
+
+

show

+
+
 show (ft, *rest, iframe=False, height='auto', style=None)
+
+

Renders FT Components into HTML within a Jupyter notebook.

+

When placed within the show() function, this will render the HTML in Jupyter notebooks.

+
+
show(sentence)
+
+

+FastHTML is Fast

+
+
+

You can also display full embedded pages in an iframe:

+
+
picocss = "https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css"
+picolink = (Link(rel="stylesheet", href=picocss))
+
+fullpage = Html(
+    Head(picolink),
+    Body(
+        H2("Heading 2"),
+        P("Paragraph")
+    )
+)
+
+show(fullpage, height=100, iframe=True)
+
+ +
+
+
+
+

fill_form and find_inputs

+
+

source

+
+
+

fill_form

+
+
 fill_form (form:fastcore.xml.FT, obj)
+
+

Fills named items in form using attributes in obj

+
+
@dataclass
+class TodoItem:
+    title:str; id:int; done:bool; details:str; opt:str='a'
+
+todo = TodoItem(id=2, title="Profit", done=True, details="Details", opt='b')
+check = Label(Input(type="checkbox", cls="checkboxer", name="done", data_foo="bar"), "Done", cls='px-2')
+form = Form(Fieldset(Input(cls="char", id="title", value="a"), check, Input(type="hidden", id="id"),
+                     Select(Option(value='a'), Option(value='b'), name='opt'),
+                     Textarea(id='details'), Button("Save"),
+                     name="stuff"))
+form = fill_form(form, todo)
+assert '<textarea id="details" name="details">Details</textarea>' in to_xml(form)
+form
+
+
<form><fieldset name="stuff">    <input value="Profit" id="title" class="char" name="title">
+<label class="px-2">      <input type="checkbox" name="done" data-foo="bar" class="checkboxer" checked="1">
+Done</label>    <input type="hidden" id="id" name="id" value="2">
+<select name="opt"><option value="a"></option><option value="b" selected="1"></option></select><textarea id="details" name="details">Details</textarea><button>Save</button></fieldset></form>
+
+
+
+
@dataclass
+class MultiSelect:
+    items: list[str]
+
+multiselect = MultiSelect(items=['a', 'c'])
+multiform = Form(Select(Option('a', value='a'), Option('b', value='b'), Option('c', value='c'), multiple='1', name='items'))
+multiform = fill_form(multiform, multiselect)
+assert '<option value="a" selected="1">a</option>' in to_xml(multiform)
+assert '<option value="b">b</option>' in to_xml(multiform)
+assert '<option value="c" selected="1">c</option>' in to_xml(multiform)
+multiform
+
+
<form><select multiple="1" name="items"><option value="a" selected="1">a</option><option value="b">b</option><option value="c" selected="1">c</option></select></form>
+
+
+
+
@dataclass
+class MultiCheck:
+    items: list[str]
+
+multicheck = MultiCheck(items=['a', 'c'])
+multiform = Form(Fieldset(Label(Input(type='checkbox', name='items', value='a'), 'a'),
+                          Label(Input(type='checkbox', name='items', value='b'), 'b'),
+                          Label(Input(type='checkbox', name='items', value='c'), 'c')))
+multiform = fill_form(multiform, multicheck)
+assert '<input type="checkbox" name="items" value="a" checked="1">' in to_xml(multiform)
+assert '<input type="checkbox" name="items" value="b">' in to_xml(multiform)
+assert '<input type="checkbox" name="items" value="c" checked="1">' in to_xml(multiform)
+multiform
+
+
<form><fieldset><label>      <input type="checkbox" name="items" value="a" checked="1">
+a</label><label>      <input type="checkbox" name="items" value="b">
+b</label><label>      <input type="checkbox" name="items" value="c" checked="1">
+c</label></fieldset></form>
+
+
+
+

source

+
+
+

fill_dataclass

+
+
 fill_dataclass (src, dest)
+
+

Modifies dataclass in-place and returns it

+
+
nt = TodoItem('', 0, False, '')
+fill_dataclass(todo, nt)
+nt
+
+
TodoItem(title='Profit', id=2, done=True, details='Details', opt='b')
+
+
+
+

source

+
+
+

find_inputs

+
+
 find_inputs (e, tags='input', **kw)
+
+

Recursively find all elements in e with tags and attrs matching kw

+
+
inps = find_inputs(form, id='title')
+test_eq(len(inps), 1)
+inps
+
+
[input((),{'value': 'Profit', 'id': 'title', 'class': 'char', 'name': 'title'})]
+
+
+

You can also use lxml for more sophisticated searching:

+
+
elem = lx.fromstring(to_xml(form))
+test_eq(elem.xpath("//input[@id='title']/@value"), ['Profit'])
+
+
+

source

+
+
+

getattr

+
+
 __getattr__ (tag)
+
+
+
+

html2ft

+
+

source

+
+
+

html2ft

+
+
 html2ft (html, attr1st=False)
+
+

Convert HTML to an ft expression

+
+
h = to_xml(form)
+hl_md(html2ft(h), 'python')
+
+
Form(
+    Fieldset(
+        Input(value='Profit', id='title', name='title', cls='char'),
+        Label(
+            Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'),
+            'Done',
+            cls='px-2'
+        ),
+        Input(type='hidden', id='id', name='id', value='2'),
+        Select(
+            Option(value='a'),
+            Option(value='b', selected='1'),
+            name='opt'
+        ),
+        Textarea('Details', id='details', name='details'),
+        Button('Save'),
+        name='stuff'
+    )
+)
+
+
+
+
hl_md(html2ft(h, attr1st=True), 'python')
+
+
Form(
+    Fieldset(name='stuff')(
+        Input(value='Profit', id='title', name='title', cls='char')(),
+        Label(cls='px-2')(
+            Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer')(),
+            'Done'
+        ),
+        Input(type='hidden', id='id', name='id', value='2')(),
+        Select(name='opt')(
+            Option(value='a')(),
+            Option(value='b', selected='1')()
+        ),
+        Textarea(id='details', name='details')('Details'),
+        Button()('Save')
+    )
+)
+
+
+
+

source

+
+
+

sse_message

+
+
 sse_message (elm, event='message')
+
+

Convert element elm into a format suitable for SSE streaming

+
+
print(sse_message(Div(P('hi'), P('there'))))
+
+
event: message
+data: <div>
+data:   <p>hi</p>
+data:   <p>there</p>
+data: </div>
+
+
+
+
+
+
+

Tests

+
+
test_html2ft('<input value="Profit" name="title" id="title" class="char">', attr1st=True)
+test_html2ft('<input value="Profit" name="title" id="title" class="char">')
+test_html2ft('<div id="foo"></div>')
+test_html2ft('<div id="foo">hi</div>')
+test_html2ft('<div x-show="open" x-transition:enter="transition duration-300" x-transition:enter-start="opacity-0 scale-90">Hello 👋</div>')
+test_html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>')
+
+
+
assert html2ft('<div id="foo">hi</div>', attr1st=True) == "Div(id='foo')('hi')"
+assert html2ft("""
+  <div x-show="open" x-transition:enter="transition duration-300" x-transition:enter-start="opacity-0 scale-90">Hello 👋</div>
+""") == "Div('Hello 👋', x_show='open', **{'x-transition:enter': 'transition duration-300', 'x-transition:enter-start': 'opacity-0 scale-90'})"
+assert html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>') == "Div('hello', **{'x-transition:enter.scale.80': True, 'x-transition:leave.scale.90': True})"
+assert html2ft("<img alt=' ' />") == "Img(alt=' ')"
+
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/components.html.md b/docs/api/components.html.md new file mode 100644 index 0000000000000000000000000000000000000000..cde1d94a426a03942904840a11da7971784b6c59 --- /dev/null +++ b/docs/api/components.html.md @@ -0,0 +1,559 @@ +# Components + + + + +``` python +from collections import UserDict +from lxml import html as lx +from pprint import pprint +``` + +### Str and repr + +In notebooks, FT components are rendered as their syntax highlighted +XML/HTML: + +``` python +sentence = P(Strong("FastHTML is ", I("Fast")), id='sentence_id') +sentence +``` + +``` html +

+FastHTML is Fast

+``` + +Elsewhere, they are represented as their underlying data structure: + +``` python +print(repr(sentence)) +``` + + p((strong(('FastHTML is ', i(('Fast',),{})),{}),),{'id': 'sentence_id'}) + +------------------------------------------------------------------------ + +source + +### FT.\_\_str\_\_ + +> FT.__str__ () + +*Return str(self).* + +If they have an id, then that id is used as the component’s str +representation: + +``` python +f'hx_target=#{sentence}' +``` + + 'hx_target=#sentence_id' + +------------------------------------------------------------------------ + +source + +### FT.\_\_radd\_\_ + +> FT.__radd__ (b) + +``` python +'hx_target=#' + sentence +``` + + 'hx_target=#sentence_id' + +------------------------------------------------------------------------ + +source + +### FT.\_\_add\_\_ + +> FT.__add__ (b) + +``` python +sentence + '...' +``` + + 'sentence_id...' + +### fh_html and fh_hx + +------------------------------------------------------------------------ + +source + +### attrmap_x + +> attrmap_x (o) + +------------------------------------------------------------------------ + +source + +### ft_html + +> ft_html (tag:str, *c, id=None, cls=None, title=None, style=None, +> attrmap=None, valmap=None, ft_cls=None, **kwargs) + +``` python +ft_html('a', **{'@click.away':1}) +``` + +``` html + +``` + +``` python +ft_html('a', {'@click.away':1}) +``` + +``` html + +``` + +``` python +ft_html('a', UserDict({'@click.away':1})) +``` + +``` html + +``` + +``` python +c = Div(id='someid') +``` + +``` python +ft_html('a', id=c) +``` + +``` html + +``` + +------------------------------------------------------------------------ + +source + +### ft_hx + +> ft_hx (tag:str, *c, target_id=None, hx_vals=None, hx_target=None, +> id=None, cls=None, title=None, style=None, accesskey=None, +> contenteditable=None, dir=None, draggable=None, enterkeyhint=None, +> hidden=None, inert=None, inputmode=None, lang=None, popover=None, +> spellcheck=None, tabindex=None, translate=None, hx_get=None, +> hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, +> hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None, +> hx_select=None, hx_select_oob=None, hx_indicator=None, +> hx_push_url=None, hx_confirm=None, hx_disable=None, +> hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, +> hx_headers=None, hx_history=None, hx_history_elt=None, +> hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +``` python +ft_hx('a', hx_vals={'a':1}) +``` + +``` html + +``` + +``` python +ft_hx('a', hx_target=c) +``` + +``` html + +``` + +For tags that have a `name` attribute, it will be set to the value of +`id` if not provided explicitly: + +``` python +Form(Button(target_id='foo', id='btn'), + hx_post='/', target_id='tgt', id='frm') +``` + +``` html +
+``` + +------------------------------------------------------------------------ + +source + +### File + +> File (fname) + +*Use the unescaped text in file `fname` directly* + +``` python +a = Input(name='nm') +a +``` + +``` html + +``` + +``` python +a(hx_swap_oob='true') +``` + +``` html + +``` + +``` python +a +``` + +``` html + +``` + +### show + +------------------------------------------------------------------------ + +source + +### show + +> show (ft, *rest, iframe=False, height='auto', style=None) + +*Renders FT Components into HTML within a Jupyter notebook.* + +When placed within the +[`show()`](https://www.fastht.ml/docs/api/components.html#show) +function, this will render the HTML in Jupyter notebooks. + +``` python +show(sentence) +``` + +

+FastHTML is Fast

+ +You can also display full embedded pages in an iframe: + +``` python +picocss = "https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css" +picolink = (Link(rel="stylesheet", href=picocss)) + +fullpage = Html( + Head(picolink), + Body( + H2("Heading 2"), + P("Paragraph") + ) +) + +show(fullpage, height=100, iframe=True) +``` + + + +### fill_form and find_inputs + +------------------------------------------------------------------------ + +source + +### fill_form + +> fill_form (form:fastcore.xml.FT, obj) + +*Fills named items in `form` using attributes in `obj`* + +``` python +@dataclass +class TodoItem: + title:str; id:int; done:bool; details:str; opt:str='a' + +todo = TodoItem(id=2, title="Profit", done=True, details="Details", opt='b') +check = Label(Input(type="checkbox", cls="checkboxer", name="done", data_foo="bar"), "Done", cls='px-2') +form = Form(Fieldset(Input(cls="char", id="title", value="a"), check, Input(type="hidden", id="id"), + Select(Option(value='a'), Option(value='b'), name='opt'), + Textarea(id='details'), Button("Save"), + name="stuff")) +form = fill_form(form, todo) +assert '' in to_xml(form) +form +``` + +``` html +
+ +
+``` + +``` python +@dataclass +class MultiSelect: + items: list[str] + +multiselect = MultiSelect(items=['a', 'c']) +multiform = Form(Select(Option('a', value='a'), Option('b', value='b'), Option('c', value='c'), multiple='1', name='items')) +multiform = fill_form(multiform, multiselect) +assert '' in to_xml(multiform) +assert '' in to_xml(multiform) +assert '' in to_xml(multiform) +multiform +``` + +``` html +
+``` + +``` python +@dataclass +class MultiCheck: + items: list[str] + +multicheck = MultiCheck(items=['a', 'c']) +multiform = Form(Fieldset(Label(Input(type='checkbox', name='items', value='a'), 'a'), + Label(Input(type='checkbox', name='items', value='b'), 'b'), + Label(Input(type='checkbox', name='items', value='c'), 'c'))) +multiform = fill_form(multiform, multicheck) +assert '' in to_xml(multiform) +assert '' in to_xml(multiform) +assert '' in to_xml(multiform) +multiform +``` + +``` html +
+``` + +------------------------------------------------------------------------ + +source + +### fill_dataclass + +> fill_dataclass (src, dest) + +*Modifies dataclass in-place and returns it* + +``` python +nt = TodoItem('', 0, False, '') +fill_dataclass(todo, nt) +nt +``` + + TodoItem(title='Profit', id=2, done=True, details='Details', opt='b') + +------------------------------------------------------------------------ + +source + +### find_inputs + +> find_inputs (e, tags='input', **kw) + +*Recursively find all elements in `e` with `tags` and attrs matching +`kw`* + +``` python +inps = find_inputs(form, id='title') +test_eq(len(inps), 1) +inps +``` + + [input((),{'value': 'Profit', 'id': 'title', 'class': 'char', 'name': 'title'})] + +You can also use lxml for more sophisticated searching: + +``` python +elem = lx.fromstring(to_xml(form)) +test_eq(elem.xpath("//input[@id='title']/@value"), ['Profit']) +``` + +------------------------------------------------------------------------ + +source + +### **getattr** + +> __getattr__ (tag) + +### html2ft + +------------------------------------------------------------------------ + +source + +### html2ft + +> html2ft (html, attr1st=False) + +*Convert HTML to an `ft` expression* + +``` python +h = to_xml(form) +hl_md(html2ft(h), 'python') +``` + +``` python +Form( + Fieldset( + Input(value='Profit', id='title', name='title', cls='char'), + Label( + Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'), + 'Done', + cls='px-2' + ), + Input(type='hidden', id='id', name='id', value='2'), + Select( + Option(value='a'), + Option(value='b', selected='1'), + name='opt' + ), + Textarea('Details', id='details', name='details'), + Button('Save'), + name='stuff' + ) +) +``` + +``` python +hl_md(html2ft(h, attr1st=True), 'python') +``` + +``` python +Form( + Fieldset(name='stuff')( + Input(value='Profit', id='title', name='title', cls='char')(), + Label(cls='px-2')( + Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer')(), + 'Done' + ), + Input(type='hidden', id='id', name='id', value='2')(), + Select(name='opt')( + Option(value='a')(), + Option(value='b', selected='1')() + ), + Textarea(id='details', name='details')('Details'), + Button()('Save') + ) +) +``` + +------------------------------------------------------------------------ + +source + +### sse_message + +> sse_message (elm, event='message') + +*Convert element `elm` into a format suitable for SSE streaming* + +``` python +print(sse_message(Div(P('hi'), P('there')))) +``` + + event: message + data:
+ data:

hi

+ data:

there

+ data:
+ +## Tests + +``` python +test_html2ft('', attr1st=True) +test_html2ft('') +test_html2ft('
') +test_html2ft('
hi
') +test_html2ft('
Hello 👋
') +test_html2ft('
hello
') +``` + +``` python +assert html2ft('
hi
', attr1st=True) == "Div(id='foo')('hi')" +assert html2ft(""" +
Hello 👋
+""") == "Div('Hello 👋', x_show='open', **{'x-transition:enter': 'transition duration-300', 'x-transition:enter-start': 'opacity-0 scale-90'})" +assert html2ft('
hello
') == "Div('hello', **{'x-transition:enter.scale.80': True, 'x-transition:leave.scale.90': True})" +assert html2ft(" ") == "Img(alt=' ')" +``` diff --git a/docs/api/core.html b/docs/api/core.html new file mode 100644 index 0000000000000000000000000000000000000000..dd1d280dd04cdda1b37c95824b629afff9642954 --- /dev/null +++ b/docs/api/core.html @@ -0,0 +1,2418 @@ + + + + + + + + + + +Core – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Core

+
+ +
+
+ The FastHTML subclass of Starlette, along with the RouterX and RouteX classes it automatically uses. +
+
+ + +
+ + + + +
+ + + +
+ + + +

This is the source code to fasthtml. You won’t need to read this unless you want to understand how things are built behind the scenes, or need full details of a particular API. The notebook is converted to the Python module fasthtml/core.py using nbdev.

+
+

Imports and utils

+
+
import time
+
+from IPython import display
+from enum import Enum
+from pprint import pprint
+
+from fastcore.test import *
+from starlette.testclient import TestClient
+from starlette.requests import Headers
+from starlette.datastructures import UploadFile
+
+

We write source code first, and then tests come after. The tests serve as both a means to confirm that the code works and also serves as working examples. The first exported function, parsed_date, is an example of this pattern.

+
+

source

+
+

parsed_date

+
+
 parsed_date (s:str)
+
+

Convert s to a datetime

+
+
parsed_date('2pm')
+
+
datetime.datetime(2025, 5, 29, 14, 0)
+
+
+
+
isinstance(date.fromtimestamp(0), date)
+
+
True
+
+
+
+

source

+
+
+

snake2hyphens

+
+
 snake2hyphens (s:str)
+
+

Convert s from snake case to hyphenated and capitalised

+
+
snake2hyphens("snake_case")
+
+
'Snake-Case'
+
+
+
+

source

+
+
+

HtmxHeaders

+
+
 HtmxHeaders (boosted:str|None=None, current_url:str|None=None,
+              history_restore_request:str|None=None, prompt:str|None=None,
+              request:str|None=None, target:str|None=None,
+              trigger_name:str|None=None, trigger:str|None=None)
+
+
+
def test_request(url: str='/', headers: dict={}, method: str='get') -> Request:
+    scope = {
+        'type': 'http',
+        'method': method,
+        'path': url,
+        'headers': Headers(headers).raw,
+        'query_string': b'',
+        'scheme': 'http',
+        'client': ('127.0.0.1', 8000),
+        'server': ('127.0.0.1', 8000),
+    }
+    receive = lambda: {"body": b"", "more_body": False}
+    return Request(scope, receive)
+
+
+
h = test_request(headers=Headers({'HX-Request':'1'}))
+_get_htmx(h.headers)
+
+
HtmxHeaders(boosted=None, current_url=None, history_restore_request=None, prompt=None, request='1', target=None, trigger_name=None, trigger=None)
+
+
+
+
+
+

Request and response

+
+
test_eq(_fix_anno(Union[str,None], 'a'), 'a')
+test_eq(_fix_anno(float, 0.9), 0.9)
+test_eq(_fix_anno(int, '1'), 1)
+test_eq(_fix_anno(int, ['1','2']), 2)
+test_eq(_fix_anno(list[int], ['1','2']), [1,2])
+test_eq(_fix_anno(list[int], '1'), [1])
+
+
+
d = dict(k=int, l=List[int])
+test_eq(_form_arg('k', "1", d), 1)
+test_eq(_form_arg('l', "1", d), [1])
+test_eq(_form_arg('l', ["1","2"], d), [1,2])
+
+
+

source

+
+

HttpHeader

+
+
 HttpHeader (k:str, v:str)
+
+
+
_to_htmx_header('trigger_after_settle')
+
+
'HX-Trigger-After-Settle'
+
+
+
+

source

+
+
+

HtmxResponseHeaders

+
+
 HtmxResponseHeaders (location=None, push_url=None, redirect=None,
+                      refresh=None, replace_url=None, reswap=None,
+                      retarget=None, reselect=None, trigger=None,
+                      trigger_after_settle=None, trigger_after_swap=None)
+
+

HTMX response headers

+
+
HtmxResponseHeaders(trigger_after_settle='hi')
+
+
HttpHeader(k='HX-Trigger-After-Settle', v='hi')
+
+
+
+

source

+
+
+

form2dict

+
+
 form2dict (form:starlette.datastructures.FormData)
+
+

Convert starlette form data to a dict

+
+
d = [('a',1),('a',2),('b',0)]
+fd = FormData(d)
+res = form2dict(fd)
+test_eq(res['a'], [1,2])
+test_eq(res['b'], 0)
+
+
+

source

+
+
+

parse_form

+
+
 parse_form (req:starlette.requests.Request)
+
+

Starlette errors on empty multipart forms, so this checks for that situation

+
+

source

+
+
+

JSONResponse

+
+
 JSONResponse (content:Any, status_code:int=200,
+               headers:Optional[Mapping[str,str]]=None,
+               media_type:str|None=None,
+               background:starlette.background.BackgroundTask|None=None)
+
+

Same as starlette’s version, but auto-stringifies non serializable types

+
+
async def f(req):
+    def _f(p:HttpHeader): ...
+    p = first(_params(_f).values())
+    result = await _from_body(req, p)
+    return JSONResponse(result.__dict__)
+
+client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
+
+d = dict(k='value1',v=['value2','value3'])
+response = client.post('/', data=d)
+print(response.json())
+
+
{'k': 'value1', 'v': 'value3'}
+
+
+
+
async def f(req): return Response(str(req.query_params.getlist('x')))
+client = TestClient(Starlette(routes=[Route('/', f, methods=['GET'])]))
+client.get('/?x=1&x=2').text
+
+
"['1', '2']"
+
+
+
+
def g(req, this:Starlette, a:str, b:HttpHeader): ...
+
+async def f(req):
+    a = await _wrap_req(req, _params(g))
+    return Response(str(a))
+
+client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
+response = client.post('/?a=1', data=d)
+print(response.text)
+
+
[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]
+
+
+
+
def g(req, this:Starlette, a:str, b:HttpHeader): ...
+
+async def f(req):
+    a = await _wrap_req(req, _params(g))
+    return Response(str(a))
+
+client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
+response = client.post('/?a=1', data=d)
+print(response.text)
+
+
[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]
+
+
+

Missing Request Params

+

If a request param has a default value (e.g. a:str=''), the request is valid even if the user doesn’t include the param in their request.

+
+
def g(req, this:Starlette, a:str=''): ...
+
+async def f(req):
+    a = await _wrap_req(req, _params(g))
+    return Response(str(a))
+
+client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
+response = client.post('/', json={}) # no param in request
+print(response.text)
+
+
[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '']
+
+
+

If we remove the default value and re-run the request, we should get the following error Missing required field: a.

+
+
def g(req, this:Starlette, a:str): ...
+
+async def f(req):
+    a = await _wrap_req(req, _params(g))
+    return Response(str(a))
+
+client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
+response = client.post('/', json={}) # no param in request
+print(response.text)
+
+
Missing required field: a
+
+
+
+

source

+
+
+

flat_xt

+
+
 flat_xt (lst)
+
+

Flatten lists

+
+
x = ft('a',1)
+test_eq(flat_xt([x, x, [x,x]]), (x,)*4)
+test_eq(flat_xt(x), (x,))
+
+
+

source

+
+
+

Beforeware

+
+
 Beforeware (f, skip=None)
+
+

Initialize self. See help(type(self)) for accurate signature.

+
+
+
+

Websockets / SSE

+
+
def on_receive(self, msg:str): return f"Message text was: {msg}"
+c = _ws_endp(on_receive)
+cli = TestClient(Starlette(routes=[WebSocketRoute('/', _ws_endp(on_receive))]))
+with cli.websocket_connect('/') as ws:
+    ws.send_text('{"msg":"Hi!"}')
+    data = ws.receive_text()
+    assert data == 'Message text was: Hi!'
+
+
+

source

+
+

EventStream

+
+
 EventStream (s)
+
+

Create a text/event-stream response from s

+
+

source

+
+
+

signal_shutdown

+
+
 signal_shutdown ()
+
+
+
+
+

Routing and application

+
+

source

+
+

uri

+
+
 uri (_arg, **kwargs)
+
+
+

source

+
+
+

decode_uri

+
+
 decode_uri (s)
+
+
+

source

+
+
+

StringConvertor.to_string

+
+
 StringConvertor.to_string (value:str)
+
+
+

source

+
+
+

HTTPConnection.url_path_for

+
+
 HTTPConnection.url_path_for (name:str, **path_params)
+
+
+

source

+
+
+

flat_tuple

+
+
 flat_tuple (o)
+
+

Flatten lists

+
+

source

+
+
+

noop_body

+
+
 noop_body (c, req)
+
+

Default Body wrap function which just returns the content

+
+

source

+
+
+

respond

+
+
 respond (req, heads, bdy)
+
+

Default FT response creation function

+

Render fragment if HX-Request header is present and HX-History-Restore-Request header is absent.

+
+

source

+
+
+

is_full_page

+
+
 is_full_page (req, resp)
+
+
+

source

+
+
+

Redirect

+
+
 Redirect (loc)
+
+

Use HTMX or Starlette RedirectResponse as required to redirect to loc

+

The FastHTML exts param supports the following:

+
+
print(' '.join(htmx_exts))
+
+
morph head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer
+
+
+
+

source

+
+
+

get_key

+
+
 get_key (key=None, fname='.sesskey')
+
+
+

source

+
+
+

qp

+
+
 qp (p:str, **kw)
+
+

Add parameters kw to path p

+

qp adds query parameters to route path strings

+
+
vals = {'a':5, 'b':False, 'c':[1,2], 'd':'bar', 'e':None, 'ab':42}
+
+
+
res = qp('/foo', **vals)
+test_eq(res, '/foo?a=5&b=&c=1&c=2&d=bar&e=&ab=42')
+
+

qp checks to see if each param should be sent as a query parameter or as part of the route, and encodes that properly.

+
+
path = '/foo/{a}/{d}/{ab:int}'
+res = qp(path, **vals)
+test_eq(res, '/foo/5/bar/42?b=&c=1&c=2&e=')
+
+
+

source

+
+
+

def_hdrs

+
+
 def_hdrs (htmx=True, surreal=True)
+
+

Default headers for a FastHTML app

+
+

source

+
+
+

FastHTML

+
+
 FastHTML (debug=False, routes=None, middleware=None, title:str='FastHTML
+           page', exception_handlers=None, on_startup=None,
+           on_shutdown=None, lifespan=None, hdrs=None, ftrs=None,
+           exts=None, before=None, after=None, surreal=True, htmx=True,
+           default_hdrs=True, sess_cls=<class
+           'starlette.middleware.sessions.SessionMiddleware'>,
+           secret_key=None, session_cookie='session_', max_age=31536000,
+           sess_path='/', same_site='lax', sess_https_only=False,
+           sess_domain=None, key_fname='.sesskey', body_wrap=<function
+           noop_body>, htmlkw=None, nb_hdrs=False, canonical=True,
+           **bodykw)
+
+

Creates an Starlette application.

+
+

source

+
+
+

FastHTML.ws

+
+
 FastHTML.ws (path:str, conn=None, disconn=None, name=None,
+              middleware=None)
+
+

Add a websocket route at path

+
+

source

+
+
+

nested_name

+
+
 nested_name (f)
+
+

*Get name of function f using ’_’ to join nested function names*

+
+
def f():
+    def g(): ...
+    return g
+
+
+
func = f()
+nested_name(func)
+
+
'f_g'
+
+
+
+

source

+
+
+

FastHTML.route

+
+
 FastHTML.route (path:str=None, methods=None, name=None,
+                 include_in_schema=True, body_wrap=None)
+
+

Add a route at path

+
+
app = FastHTML()
+@app.get
+def foo(a:str, b:list[int]): ...
+
+foo.to(a='bar', b=[1,2])
+
+
'/foo?a=bar&b=1&b=2'
+
+
+
+
@app.get('/foo/{a}')
+def foo(a:str, b:list[int]): ...
+
+foo.to(a='bar', b=[1,2])
+
+
'/foo/bar?b=1&b=2'
+
+
+
+

source

+
+
+

serve

+
+
 serve (appname=None, app='app', host='0.0.0.0', port=None, reload=True,
+        reload_includes:list[str]|str|None=None,
+        reload_excludes:list[str]|str|None=None)
+
+

Run the app in an async server, with live reload set as the default.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
appnameNoneTypeNoneName of the module
appstrappApp instance to be served
hoststr0.0.0.0If host is 0.0.0.0 will convert to localhost
portNoneTypeNoneIf port is None it will default to 5001 or the PORT environment variable
reloadboolTrueDefault is to reload the app upon code changes
reload_includeslist[str] | str | NoneNoneAdditional files to watch for changes
reload_excludeslist[str] | str | NoneNoneFiles to ignore for changes
+
+

source

+
+
+

Client

+
+
 Client (app, url='http://testserver')
+
+

A simple httpx ASGI client that doesn’t require async

+
+
app = FastHTML(routes=[Route('/', lambda _: Response('test'))])
+cli = Client(app)
+
+cli.get('/').text
+
+
'test'
+
+
+

Note that you can also use Starlette’s TestClient instead of FastHTML’s Client. They should be largely interchangable.

+
+
+
+

FastHTML Tests

+
+
def get_cli(app): return app,TestClient(app),app.route
+
+
+
app,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))
+
+
+
app,cli,rt = get_cli(FastHTML(title="My Custom Title"))
+@app.get
+def foo(): return Div("Hello World")
+
+print(app.routes)
+
+response = cli.get('/foo')
+assert '<title>My Custom Title</title>' in response.text
+
+foo.to(param='value')
+
+
[Route(path='/foo', name='foo', methods=['GET', 'HEAD'])]
+
+
+
'/foo?param=value'
+
+
+
+
app,cli,rt = get_cli(FastHTML())
+
+@rt('/xt2')
+def get(): return H1('bar')
+
+txt = cli.get('/xt2').text
+assert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
+
+
+
@rt("/hi")
+def get(): return 'Hi there'
+
+r = cli.get('/hi')
+r.text
+
+
'Hi there'
+
+
+
+
@rt("/hi")
+def post(): return 'Postal'
+
+cli.post('/hi').text
+
+
'Postal'
+
+
+
+
@app.get("/hostie")
+def show_host(req): return req.headers['host']
+
+cli.get('/hostie').text
+
+
'testserver'
+
+
+
+
@app.get("/setsess")
+def set_sess(session):
+   session['foo'] = 'bar'
+   return 'ok'
+
+@app.ws("/ws")
+def ws(self, msg:str, ws:WebSocket, session): return f"Message text was: {msg} with session {session.get('foo')}, from client: {ws.client}"
+
+cli.get('/setsess')
+with cli.websocket_connect('/ws') as ws:
+    ws.send_text('{"msg":"Hi!"}')
+    data = ws.receive_text()
+assert 'Message text was: Hi! with session bar' in data
+print(data)
+
+
Message text was: Hi! with session bar, from client: Address(host='testclient', port=50000)
+
+
+
+
@rt
+def yoyo(): return 'a yoyo'
+
+cli.post('/yoyo').text
+
+
'a yoyo'
+
+
+
+
@app.get
+def autopost(): return Html(Div('Text.', hx_post=yoyo()))
+print(cli.get('/autopost').text)
+
+
 <!doctype html>
+ <html>
+   <div hx-post="a yoyo">Text.</div>
+ </html>
+
+
+
+
+
@app.get
+def autopost2(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b'))))
+print(cli.get('/autopost2').text)
+
+
 <!doctype html>
+ <html>
+   <body>
+     <div class="px-2" hx-post="/hostie?a=b">Text.</div>
+   </body>
+ </html>
+
+
+
+
+
@app.get
+def autoget2(): return Html(Div('Text.', hx_get=show_host))
+print(cli.get('/autoget2').text)
+
+
 <!doctype html>
+ <html>
+   <div hx-get="/hostie">Text.</div>
+ </html>
+
+
+
+
+
@rt('/user/{nm}', name='gday')
+def get(nm:str=''): return f"Good day to you, {nm}!"
+cli.get('/user/Alexis').text
+
+
'Good day to you, Alexis!'
+
+
+
+
@app.get
+def autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))
+print(cli.get('/autolink').text)
+
+
 <!doctype html>
+ <html>
+   <div href="/user/Alexis">Text.</div>
+ </html>
+
+
+
+
+
@rt('/link')
+def get(req): return f"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}"
+
+cli.get('/link').text
+
+
'http://testserver/user/Alexis; http://testserver/hostie'
+
+
+
+
@app.get("/background")
+async def background_task(request):
+    async def long_running_task():
+        await asyncio.sleep(0.1)
+        print("Background task completed!")
+    return P("Task started"), BackgroundTask(long_running_task)
+
+response = cli.get("/background")
+
+
Background task completed!
+
+
+
+
test_eq(app.router.url_path_for('gday', nm='Jeremy'), '/user/Jeremy')
+
+
+
hxhdr = {'headers':{'hx-request':"1"}}
+
+@rt('/ft')
+def get(): return Title('Foo'),H1('bar')
+
+txt = cli.get('/ft').text
+assert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
+
+@rt('/xt2')
+def get(): return H1('bar')
+
+txt = cli.get('/xt2').text
+assert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
+
+assert cli.get('/xt2', **hxhdr).text.strip() == '<h1>bar</h1>'
+
+@rt('/xt3')
+def get(): return Html(Head(Title('hi')), Body(P('there')))
+
+txt = cli.get('/xt3').text
+assert '<title>FastHTML page</title>' not in txt and '<title>hi</title>' in txt and '<p>there</p>' in txt
+
+
+
@rt('/oops')
+def get(nope): return nope
+test_warns(lambda: cli.get('/oops?nope=1'))
+
+
+
def test_r(cli, path, exp, meth='get', hx=False, **kwargs):
+    if hx: kwargs['headers'] = {'hx-request':"1"}
+    test_eq(getattr(cli, meth)(path, **kwargs).text, exp)
+
+ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet")
+fake_db = [{"name": "Foo"}, {"name": "Bar"}]
+
+
+
@rt('/html/{idx}')
+async def get(idx:int): return Body(H4(f'Next is {idx+1}.'))
+
+
+
@rt("/models/{nm}")
+def get(nm:ModelName): return nm
+
+@rt("/files/{path}")
+async def get(path: Path): return path.with_suffix('.txt')
+
+@rt("/items/")
+def get(idx:int|None = 0): return fake_db[idx]
+
+@rt("/idxl/")
+def get(idx:list[int]): return str(idx)
+
+
+
r = cli.get('/html/1', headers={'hx-request':"1"})
+assert '<h4>Next is 2.</h4>' in r.text
+test_r(cli, '/models/alexnet', 'alexnet')
+test_r(cli, '/files/foo', 'foo.txt')
+test_r(cli, '/items/?idx=1', '{"name":"Bar"}')
+test_r(cli, '/items/', '{"name":"Foo"}')
+assert cli.get('/items/?idx=g').text=='404 Not Found'
+assert cli.get('/items/?idx=g').status_code == 404
+test_r(cli, '/idxl/?idx=1&idx=2', '[1, 2]')
+assert cli.get('/idxl/?idx=1&idx=g').status_code == 404
+
+
+
app = FastHTML()
+rt = app.route
+cli = TestClient(app)
+@app.route(r'/static/{path:path}.jpg')
+def index(path:str): return f'got {path}'
+cli.get('/static/sub/a.b.jpg').text
+
+
'got sub/a.b'
+
+
+
+
app.chk = 'foo'
+
+
+
@app.get("/booly/")
+def _(coming:bool=True): return 'Coming' if coming else 'Not coming'
+
+@app.get("/datie/")
+def _(d:parsed_date): return d
+
+@app.get("/ua")
+async def _(user_agent:str): return user_agent
+
+@app.get("/hxtest")
+def _(htmx): return htmx.request
+
+@app.get("/hxtest2")
+def _(foo:HtmxHeaders, req): return foo.request
+
+@app.get("/app")
+def _(app): return app.chk
+
+@app.get("/app2")
+def _(foo:FastHTML): return foo.chk,HttpHeader("mykey", "myval")
+
+@app.get("/app3")
+def _(foo:FastHTML): return HtmxResponseHeaders(location="http://example.org")
+
+@app.get("/app4")
+def _(foo:FastHTML): return Redirect("http://example.org")
+
+
+
test_r(cli, '/booly/?coming=true', 'Coming')
+test_r(cli, '/booly/?coming=no', 'Not coming')
+date_str = "17th of May, 2024, 2p"
+test_r(cli, f'/datie/?d={date_str}', '2024-05-17 14:00:00')
+test_r(cli, '/ua', 'FastHTML', headers={'User-Agent':'FastHTML'})
+test_r(cli, '/hxtest' , '1', headers={'HX-Request':'1'})
+test_r(cli, '/hxtest2', '1', headers={'HX-Request':'1'})
+test_r(cli, '/app' , 'foo')
+
+
+
r = cli.get('/app2', **hxhdr)
+test_eq(r.text, 'foo')
+test_eq(r.headers['mykey'], 'myval')
+
+
+
r = cli.get('/app3')
+test_eq(r.headers['HX-Location'], 'http://example.org')
+
+
+
r = cli.get('/app4', follow_redirects=False)
+test_eq(r.status_code, 303)
+
+
+
r = cli.get('/app4', headers={'HX-Request':'1'})
+test_eq(r.headers['HX-Redirect'], 'http://example.org')
+
+
+
@rt
+def meta():
+    return ((Title('hi'),H1('hi')),
+        (Meta(property='image'), Meta(property='site_name'))
+    )
+
+t = cli.post('/meta').text
+assert re.search(r'<body>\s*<h1>hi</h1>\s*</body>', t)
+assert '<meta' in t
+
+
+
@app.post('/profile/me')
+def profile_update(username: str): return username
+
+test_r(cli, '/profile/me', 'Alexis', 'post', data={'username' : 'Alexis'})
+test_r(cli, '/profile/me', 'Missing required field: username', 'post', data={})
+
+
+
# Example post request with parameter that has a default value
+@app.post('/pet/dog')
+def pet_dog(dogname: str = None): return dogname
+
+# Working post request with optional parameter
+test_r(cli, '/pet/dog', '', 'post', data={})
+
+
+
@dataclass
+class Bodie: a:int;b:str
+
+@rt("/bodie/{nm}")
+def post(nm:str, data:Bodie):
+    res = asdict(data)
+    res['nm'] = nm
+    return res
+
+@app.post("/bodied/")
+def bodied(data:dict): return data
+
+nt = namedtuple('Bodient', ['a','b'])
+
+@app.post("/bodient/")
+def bodient(data:nt): return asdict(data)
+
+class BodieTD(TypedDict): a:int;b:str='foo'
+
+@app.post("/bodietd/")
+def bodient(data:BodieTD): return data
+
+class Bodie2:
+    a:int|None; b:str
+    def __init__(self, a, b='foo'): store_attr()
+
+@rt("/bodie2/", methods=['get','post'])
+def bodie(d:Bodie2): return f"a: {d.a}; b: {d.b}"
+
+
+
from fasthtml.xtend import Titled
+
+
+
d = dict(a=1, b='foo')
+
+test_r(cli, '/bodie/me', '{"a":1,"b":"foo","nm":"me"}', 'post', data=dict(a=1, b='foo', nm='me'))
+test_r(cli, '/bodied/', '{"a":"1","b":"foo"}', 'post', data=d)
+test_r(cli, '/bodie2/', 'a: 1; b: foo', 'post', data={'a':1})
+test_r(cli, '/bodie2/?a=1&b=foo&nm=me', 'a: 1; b: foo')
+test_r(cli, '/bodient/', '{"a":"1","b":"foo"}', 'post', data=d)
+test_r(cli, '/bodietd/', '{"a":1,"b":"foo"}', 'post', data=d)
+
+
+
# Testing POST with Content-Type: application/json
+@app.post("/")
+def index(it: Bodie): return Titled("It worked!", P(f"{it.a}, {it.b}"))
+
+s = json.dumps({"b": "Lorem", "a": 15})
+response = cli.post('/', headers={"Content-Type": "application/json"}, data=s).text
+assert "<title>It worked!</title>" in response and "<p>15, Lorem</p>" in response
+
+
+
# Testing POST with Content-Type: application/json
+@app.post("/bodytext")
+def index(body): return body
+
+response = cli.post('/bodytext', headers={"Content-Type": "application/json"}, data=s).text
+test_eq(response, '{"b": "Lorem", "a": 15}')
+
+
+
files = [ ('files', ('file1.txt', b'content1')),
+         ('files', ('file2.txt', b'content2')) ]
+
+
+
@rt("/uploads")
+async def post(files:list[UploadFile]):
+    return ','.join([(await file.read()).decode() for file in files])
+
+res = cli.post('/uploads', files=files)
+print(res.status_code)
+print(res.text)
+
+
200
+content1,content2
+
+
+
+
res = cli.post('/uploads', files=[files[0]])
+print(res.status_code)
+print(res.text)
+
+
200
+content1
+
+
+
+
@rt("/setsess")
+def get(sess, foo:str=''):
+    now = datetime.now()
+    sess['auth'] = str(now)
+    return f'Set to {now}'
+
+@rt("/getsess")
+def get(sess): return f'Session time: {sess["auth"]}'
+
+print(cli.get('/setsess').text)
+time.sleep(0.01)
+
+cli.get('/getsess').text
+
+
Set to 2025-05-29 08:31:48.235262
+
+
+
'Session time: 2025-05-29 08:31:48.235262'
+
+
+
+
@rt("/sess-first")
+def post(sess, name: str):
+    sess["name"] = name
+    return str(sess)
+
+cli.post('/sess-first', data={'name': 2})
+
+@rt("/getsess-all")
+def get(sess): return sess['name']
+
+test_eq(cli.get('/getsess-all').text, '2')
+
+
+
@rt("/upload")
+async def post(uf:UploadFile): return (await uf.read()).decode()
+
+with open('../../CHANGELOG.md', 'rb') as f:
+    print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])
+
+
# Release notes
+
+
+
+
@rt("/form-submit/{list_id}")
+def options(list_id: str):
+    headers = {
+        'Access-Control-Allow-Origin': '*',
+        'Access-Control-Allow-Methods': 'POST',
+        'Access-Control-Allow-Headers': '*',
+    }
+    return Response(status_code=200, headers=headers)
+
+
+
h = cli.options('/form-submit/2').headers
+test_eq(h['Access-Control-Allow-Methods'], 'POST')
+
+
+
from fasthtml.authmw import user_pwd_auth
+
+
+
def _not_found(req, exc): return Div('nope')
+
+app,cli,rt = get_cli(FastHTML(exception_handlers={404:_not_found}))
+
+txt = cli.get('/').text
+assert '<div>nope</div>' in txt
+assert '<!doctype html>' in txt
+
+
+
app,cli,rt = get_cli(FastHTML())
+
+@rt("/{name}/{age}")
+def get(name: str, age: int):
+    return Titled(f"Hello {name.title()}, age {age}")
+
+assert '<title>Hello Uma, age 5</title>' in cli.get('/uma/5').text
+assert '404 Not Found' in cli.get('/uma/five').text
+
+
+
auth = user_pwd_auth(testuser='spycraft')
+app,cli,rt = get_cli(FastHTML(middleware=[auth]))
+
+@rt("/locked")
+def get(auth): return 'Hello, ' + auth
+
+test_eq(cli.get('/locked').text, 'not authenticated')
+test_eq(cli.get('/locked', auth=("testuser","spycraft")).text, 'Hello, testuser')
+
+
+
auth = user_pwd_auth(testuser='spycraft')
+app,cli,rt = get_cli(FastHTML(middleware=[auth]))
+
+@rt("/locked")
+def get(auth): return 'Hello, ' + auth
+
+test_eq(cli.get('/locked').text, 'not authenticated')
+test_eq(cli.get('/locked', auth=("testuser","spycraft")).text, 'Hello, testuser')
+
+
+
+

APIRouter

+
+

source

+
+

RouteFuncs

+
+
 RouteFuncs ()
+
+

Initialize self. See help(type(self)) for accurate signature.

+
+

source

+
+
+

APIRouter

+
+
 APIRouter (prefix:str|None=None, body_wrap=<function noop_body>)
+
+

Add routes to an app

+
+
ar = APIRouter()
+
+
+
@ar("/hi")
+def get(): return 'Hi there'
+@ar("/hi")
+def post(): return 'Postal'
+@ar
+def ho(): return 'Ho ho'
+@ar("/hostie")
+def show_host(req): return req.headers['host']
+@ar
+def yoyo(): return 'a yoyo'
+@ar
+def index(): return "home page"
+
+@ar.ws("/ws")
+def ws(self, msg:str): return f"Message text was: {msg}"
+
+
+
app,cli,_ = get_cli(FastHTML())
+ar.to_app(app)
+
+
+
assert str(yoyo) == '/yoyo'
+# ensure route functions are properly discoverable on `APIRouter` and `APIRouter.rt_funcs`
+assert ar.prefix == ''
+assert str(ar.rt_funcs.index) == '/'
+assert str(ar.index) == '/'
+with ExceptionExpected(): ar.blah()
+with ExceptionExpected(): ar.rt_funcs.blah()
+# ensure any route functions named using an HTTPMethod are not discoverable via `rt_funcs`
+assert "get" not in ar.rt_funcs._funcs.keys()
+
+
+
test_eq(cli.get('/hi').text, 'Hi there')
+test_eq(cli.post('/hi').text, 'Postal')
+test_eq(cli.get('/hostie').text, 'testserver')
+test_eq(cli.post('/yoyo').text, 'a yoyo')
+
+test_eq(cli.get('/ho').text, 'Ho ho')
+test_eq(cli.post('/ho').text, 'Ho ho')
+
+
+
with cli.websocket_connect('/ws') as ws:
+    ws.send_text('{"msg":"Hi!"}')
+    data = ws.receive_text()
+    assert data == 'Message text was: Hi!'
+
+
+
ar2 = APIRouter("/products")
+
+
+
@ar2("/hi")
+def get(): return 'Hi there'
+@ar2("/hi")
+def post(): return 'Postal'
+@ar2
+def ho(): return 'Ho ho'
+@ar2("/hostie")
+def show_host(req): return req.headers['host']
+@ar2
+def yoyo(): return 'a yoyo'
+@ar2
+def index(): return "home page"
+
+@ar2.ws("/ws")
+def ws(self, msg:str): return f"Message text was: {msg}"
+
+
+
app,cli,_ = get_cli(FastHTML())
+ar2.to_app(app)
+
+
+
assert str(yoyo) == '/products/yoyo'
+assert ar2.prefix == '/products'
+assert str(ar2.rt_funcs.index) == '/products/'
+assert str(ar2.index) == '/products/'
+assert str(ar.index) == '/'
+with ExceptionExpected(): ar2.blah()
+with ExceptionExpected(): ar2.rt_funcs.blah()
+assert "get" not in ar2.rt_funcs._funcs.keys()
+
+
+
test_eq(cli.get('/products/hi').text, 'Hi there')
+test_eq(cli.post('/products/hi').text, 'Postal')
+test_eq(cli.get('/products/hostie').text, 'testserver')
+test_eq(cli.post('/products/yoyo').text, 'a yoyo')
+
+test_eq(cli.get('/products/ho').text, 'Ho ho')
+test_eq(cli.post('/products/ho').text, 'Ho ho')
+
+
+
with cli.websocket_connect('/products/ws') as ws:
+    ws.send_text('{"msg":"Hi!"}')
+    data = ws.receive_text()
+    assert data == 'Message text was: Hi!'
+
+
+
@ar.get
+def hi2(): return 'Hi there'
+@ar.get("/hi3")
+def _(): return 'Hi there'
+@ar.post("/post2")
+def _(): return 'Postal'
+
+@ar2.get
+def hi2(): return 'Hi there'
+@ar2.get("/hi3")
+def _(): return 'Hi there'
+@ar2.post("/post2")
+def _(): return 'Postal'
+
+
+
+
+

Extras

+
+
app,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))
+
+
+

source

+ +
+

reg_re_param

+
+
 reg_re_param (m, s)
+
+
+

source

+
+
+

FastHTML.static_route_exts

+
+
 FastHTML.static_route_exts (prefix='/', static_path='.', exts='static')
+
+

Add a static route at URL path prefix with files from static_path and exts defined by reg_re_param()

+
+
reg_re_param("imgext", "ico|gif|jpg|jpeg|webm|pdf")
+
+@rt(r'/static/{path:path}{fn}.{ext:imgext}')
+def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"
+
+test_r(cli, '/static/foo/jph.me.ico', 'Getting jph.me.ico from /foo/')
+
+
+
app.static_route_exts()
+assert 'These are the source notebooks for FastHTML' in cli.get('/README.txt').text
+
+
+

source

+
+
+

FastHTML.static_route

+
+
 FastHTML.static_route (ext='', prefix='/', static_path='.')
+
+

Add a static route at URL path prefix with files from static_path and single ext (including the ‘.’)

+
+
app.static_route('.md', static_path='../..')
+assert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text
+
+
+

source

+
+
+

MiddlewareBase

+
+
 MiddlewareBase ()
+
+

Initialize self. See help(type(self)) for accurate signature.

+
+

source

+
+
+

FtResponse

+
+
 FtResponse (content, status_code:int=200, headers=None, cls=<class
+             'starlette.responses.HTMLResponse'>,
+             media_type:str|None=None,
+             background:starlette.background.BackgroundTask|None=None)
+
+

Wrap an FT response with any Starlette Response

+
+
@rt('/ftr')
+def get():
+    cts = Title('Foo'),H1('bar')
+    return FtResponse(cts, status_code=201, headers={'Location':'/foo/1'})
+
+r = cli.get('/ftr')
+
+test_eq(r.status_code, 201)
+test_eq(r.headers['location'], '/foo/1')
+txt = r.text
+assert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
+
+

Test on a single background task:

+
+
def my_slow_task():
+    print('Starting slow task')    
+    time.sleep(0.001)
+    print('Finished slow task')        
+
+@rt('/background')
+def get():
+    return P('BG Task'), BackgroundTask(my_slow_task)
+
+r = cli.get('/background')
+
+test_eq(r.status_code, 200)
+
+
Starting slow task
+Finished slow task
+
+
+

Test multiple background tasks:

+
+
def increment(amount):
+    amount = amount/1000
+    print(f'Sleeping for {amount}s')    
+    time.sleep(amount)
+    print(f'Slept for {amount}s')
+
+
+
@rt
+def backgrounds():
+    tasks = BackgroundTasks()
+    for i in range(3): tasks.add_task(increment, i)
+    return P('BG Tasks'), tasks
+
+r = cli.get('/backgrounds')
+test_eq(r.status_code, 200)
+
+
Sleeping for 0.0s
+Slept for 0.0s
+Sleeping for 0.001s
+Slept for 0.001s
+Sleeping for 0.002s
+Slept for 0.002s
+
+
+
+
@rt
+def backgrounds2():
+    tasks = [BackgroundTask(increment,i) for i in range(3)]
+    return P('BG Tasks'), *tasks
+
+r = cli.get('/backgrounds2')
+test_eq(r.status_code, 200)
+
+
Sleeping for 0.0s
+Slept for 0.0s
+Sleeping for 0.001s
+Slept for 0.001s
+Sleeping for 0.002s
+Slept for 0.002s
+
+
+
+
@rt
+def backgrounds3():
+    tasks = [BackgroundTask(increment,i) for i in range(3)]
+    return {'status':'done'}, *tasks
+
+r = cli.get('/backgrounds3')
+test_eq(r.status_code, 200)
+r.json()
+
+
Sleeping for 0.0s
+Slept for 0.0s
+Sleeping for 0.001s
+Slept for 0.001s
+Sleeping for 0.002s
+Slept for 0.002s
+
+
+
{'status': 'done'}
+
+
+
+

source

+
+
+

unqid

+
+
 unqid (seeded=False)
+
+
+

source

+
+
+

setup_ws

+
+
 setup_ws (app, f=<function noop>)
+
+
+

source

+
+
+

FastHTML.devtools_json

+
+
 FastHTML.devtools_json (path=None, uuid=None)
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/core.html.md b/docs/api/core.html.md new file mode 100644 index 0000000000000000000000000000000000000000..b642be9bdf22c920e0cdf9eaa66b0d3e15ecc64c --- /dev/null +++ b/docs/api/core.html.md @@ -0,0 +1,1693 @@ +# Core + + + + +This is the source code to fasthtml. You won’t need to read this unless +you want to understand how things are built behind the scenes, or need +full details of a particular API. The notebook is converted to the +Python module +[fasthtml/core.py](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py) +using [nbdev](https://nbdev.fast.ai/). + +## Imports and utils + +``` python +import time + +from IPython import display +from enum import Enum +from pprint import pprint + +from fastcore.test import * +from starlette.testclient import TestClient +from starlette.requests import Headers +from starlette.datastructures import UploadFile +``` + +We write source code *first*, and then tests come *after*. The tests +serve as both a means to confirm that the code works and also serves as +working examples. The first exported function, +[`parsed_date`](https://www.fastht.ml/docs/api/core.html#parsed_date), +is an example of this pattern. + +------------------------------------------------------------------------ + +source + +### parsed_date + +> parsed_date (s:str) + +*Convert `s` to a datetime* + +``` python +parsed_date('2pm') +``` + + datetime.datetime(2025, 5, 29, 14, 0) + +``` python +isinstance(date.fromtimestamp(0), date) +``` + + True + +------------------------------------------------------------------------ + +source + +### snake2hyphens + +> snake2hyphens (s:str) + +*Convert `s` from snake case to hyphenated and capitalised* + +``` python +snake2hyphens("snake_case") +``` + + 'Snake-Case' + +------------------------------------------------------------------------ + +source + +### HtmxHeaders + +> HtmxHeaders (boosted:str|None=None, current_url:str|None=None, +> history_restore_request:str|None=None, prompt:str|None=None, +> request:str|None=None, target:str|None=None, +> trigger_name:str|None=None, trigger:str|None=None) + +``` python +def test_request(url: str='/', headers: dict={}, method: str='get') -> Request: + scope = { + 'type': 'http', + 'method': method, + 'path': url, + 'headers': Headers(headers).raw, + 'query_string': b'', + 'scheme': 'http', + 'client': ('127.0.0.1', 8000), + 'server': ('127.0.0.1', 8000), + } + receive = lambda: {"body": b"", "more_body": False} + return Request(scope, receive) +``` + +``` python +h = test_request(headers=Headers({'HX-Request':'1'})) +_get_htmx(h.headers) +``` + + HtmxHeaders(boosted=None, current_url=None, history_restore_request=None, prompt=None, request='1', target=None, trigger_name=None, trigger=None) + +## Request and response + +``` python +test_eq(_fix_anno(Union[str,None], 'a'), 'a') +test_eq(_fix_anno(float, 0.9), 0.9) +test_eq(_fix_anno(int, '1'), 1) +test_eq(_fix_anno(int, ['1','2']), 2) +test_eq(_fix_anno(list[int], ['1','2']), [1,2]) +test_eq(_fix_anno(list[int], '1'), [1]) +``` + +``` python +d = dict(k=int, l=List[int]) +test_eq(_form_arg('k', "1", d), 1) +test_eq(_form_arg('l', "1", d), [1]) +test_eq(_form_arg('l', ["1","2"], d), [1,2]) +``` + +------------------------------------------------------------------------ + +source + +### HttpHeader + +> HttpHeader (k:str, v:str) + +``` python +_to_htmx_header('trigger_after_settle') +``` + + 'HX-Trigger-After-Settle' + +------------------------------------------------------------------------ + +source + +### HtmxResponseHeaders + +> HtmxResponseHeaders (location=None, push_url=None, redirect=None, +> refresh=None, replace_url=None, reswap=None, +> retarget=None, reselect=None, trigger=None, +> trigger_after_settle=None, trigger_after_swap=None) + +*HTMX response headers* + +``` python +HtmxResponseHeaders(trigger_after_settle='hi') +``` + + HttpHeader(k='HX-Trigger-After-Settle', v='hi') + +------------------------------------------------------------------------ + +source + +### form2dict + +> form2dict (form:starlette.datastructures.FormData) + +*Convert starlette form data to a dict* + +``` python +d = [('a',1),('a',2),('b',0)] +fd = FormData(d) +res = form2dict(fd) +test_eq(res['a'], [1,2]) +test_eq(res['b'], 0) +``` + +------------------------------------------------------------------------ + +source + +### parse_form + +> parse_form (req:starlette.requests.Request) + +*Starlette errors on empty multipart forms, so this checks for that +situation* + +------------------------------------------------------------------------ + +source + +### JSONResponse + +> JSONResponse (content:Any, status_code:int=200, +> headers:Optional[Mapping[str,str]]=None, +> media_type:str|None=None, +> background:starlette.background.BackgroundTask|None=None) + +*Same as starlette’s version, but auto-stringifies non serializable +types* + +``` python +async def f(req): + def _f(p:HttpHeader): ... + p = first(_params(_f).values()) + result = await _from_body(req, p) + return JSONResponse(result.__dict__) + +client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])])) + +d = dict(k='value1',v=['value2','value3']) +response = client.post('/', data=d) +print(response.json()) +``` + + {'k': 'value1', 'v': 'value3'} + +``` python +async def f(req): return Response(str(req.query_params.getlist('x'))) +client = TestClient(Starlette(routes=[Route('/', f, methods=['GET'])])) +client.get('/?x=1&x=2').text +``` + + "['1', '2']" + +``` python +def g(req, this:Starlette, a:str, b:HttpHeader): ... + +async def f(req): + a = await _wrap_req(req, _params(g)) + return Response(str(a)) + +client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])])) +response = client.post('/?a=1', data=d) +print(response.text) +``` + + [, , '1', HttpHeader(k='value1', v='value3')] + +``` python +def g(req, this:Starlette, a:str, b:HttpHeader): ... + +async def f(req): + a = await _wrap_req(req, _params(g)) + return Response(str(a)) + +client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])])) +response = client.post('/?a=1', data=d) +print(response.text) +``` + + [, , '1', HttpHeader(k='value1', v='value3')] + +**Missing Request Params** + +If a request param has a default value (e.g. `a:str=''`), the request is +valid even if the user doesn’t include the param in their request. + +``` python +def g(req, this:Starlette, a:str=''): ... + +async def f(req): + a = await _wrap_req(req, _params(g)) + return Response(str(a)) + +client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])])) +response = client.post('/', json={}) # no param in request +print(response.text) +``` + + [, , ''] + +If we remove the default value and re-run the request, we should get the +following error `Missing required field: a`. + +``` python +def g(req, this:Starlette, a:str): ... + +async def f(req): + a = await _wrap_req(req, _params(g)) + return Response(str(a)) + +client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])])) +response = client.post('/', json={}) # no param in request +print(response.text) +``` + + Missing required field: a + +------------------------------------------------------------------------ + +source + +### flat_xt + +> flat_xt (lst) + +*Flatten lists* + +``` python +x = ft('a',1) +test_eq(flat_xt([x, x, [x,x]]), (x,)*4) +test_eq(flat_xt(x), (x,)) +``` + +------------------------------------------------------------------------ + +source + +### Beforeware + +> Beforeware (f, skip=None) + +*Initialize self. See help(type(self)) for accurate signature.* + +## Websockets / SSE + +``` python +def on_receive(self, msg:str): return f"Message text was: {msg}" +c = _ws_endp(on_receive) +cli = TestClient(Starlette(routes=[WebSocketRoute('/', _ws_endp(on_receive))])) +with cli.websocket_connect('/') as ws: + ws.send_text('{"msg":"Hi!"}') + data = ws.receive_text() + assert data == 'Message text was: Hi!' +``` + +------------------------------------------------------------------------ + +source + +### EventStream + +> EventStream (s) + +*Create a text/event-stream response from `s`* + +------------------------------------------------------------------------ + +source + +### signal_shutdown + +> signal_shutdown () + +## Routing and application + +------------------------------------------------------------------------ + +source + +### uri + +> uri (_arg, **kwargs) + +------------------------------------------------------------------------ + +source + +### decode_uri + +> decode_uri (s) + +------------------------------------------------------------------------ + +source + +### StringConvertor.to_string + +> StringConvertor.to_string (value:str) + +------------------------------------------------------------------------ + +source + +### HTTPConnection.url_path_for + +> HTTPConnection.url_path_for (name:str, **path_params) + +------------------------------------------------------------------------ + +source + +### flat_tuple + +> flat_tuple (o) + +*Flatten lists* + +------------------------------------------------------------------------ + +source + +### noop_body + +> noop_body (c, req) + +*Default Body wrap function which just returns the content* + +------------------------------------------------------------------------ + +source + +### respond + +> respond (req, heads, bdy) + +*Default FT response creation function* + +Render fragment if `HX-Request` header is *present* and +`HX-History-Restore-Request` header is *absent.* + +------------------------------------------------------------------------ + +source + +### is_full_page + +> is_full_page (req, resp) + +------------------------------------------------------------------------ + +source + +### Redirect + +> Redirect (loc) + +*Use HTMX or Starlette RedirectResponse as required to redirect to +`loc`* + +The FastHTML `exts` param supports the following: + +``` python +print(' '.join(htmx_exts)) +``` + + morph head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer + +------------------------------------------------------------------------ + +source + +### get_key + +> get_key (key=None, fname='.sesskey') + +------------------------------------------------------------------------ + +source + +### qp + +> qp (p:str, **kw) + +*Add parameters kw to path p* + +[`qp`](https://www.fastht.ml/docs/api/core.html#qp) adds query +parameters to route path strings + +``` python +vals = {'a':5, 'b':False, 'c':[1,2], 'd':'bar', 'e':None, 'ab':42} +``` + +``` python +res = qp('/foo', **vals) +test_eq(res, '/foo?a=5&b=&c=1&c=2&d=bar&e=&ab=42') +``` + +[`qp`](https://www.fastht.ml/docs/api/core.html#qp) checks to see if +each param should be sent as a query parameter or as part of the route, +and encodes that properly. + +``` python +path = '/foo/{a}/{d}/{ab:int}' +res = qp(path, **vals) +test_eq(res, '/foo/5/bar/42?b=&c=1&c=2&e=') +``` + +------------------------------------------------------------------------ + +source + +### def_hdrs + +> def_hdrs (htmx=True, surreal=True) + +*Default headers for a FastHTML app* + +------------------------------------------------------------------------ + +source + +### FastHTML + +> FastHTML (debug=False, routes=None, middleware=None, title:str='FastHTML +> page', exception_handlers=None, on_startup=None, +> on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, +> exts=None, before=None, after=None, surreal=True, htmx=True, +> default_hdrs=True, sess_cls= 'starlette.middleware.sessions.SessionMiddleware'>, +> secret_key=None, session_cookie='session_', max_age=31536000, +> sess_path='/', same_site='lax', sess_https_only=False, +> sess_domain=None, key_fname='.sesskey', body_wrap= noop_body>, htmlkw=None, nb_hdrs=False, canonical=True, +> **bodykw) + +*Creates an Starlette application.* + +------------------------------------------------------------------------ + +source + +### FastHTML.ws + +> FastHTML.ws (path:str, conn=None, disconn=None, name=None, +> middleware=None) + +*Add a websocket route at `path`* + +------------------------------------------------------------------------ + +source + +### nested_name + +> nested_name (f) + +\*Get name of function `f` using ’\_’ to join nested function names\* + +``` python +def f(): + def g(): ... + return g +``` + +``` python +func = f() +nested_name(func) +``` + + 'f_g' + +------------------------------------------------------------------------ + +source + +### FastHTML.route + +> FastHTML.route (path:str=None, methods=None, name=None, +> include_in_schema=True, body_wrap=None) + +*Add a route at `path`* + +``` python +app = FastHTML() +@app.get +def foo(a:str, b:list[int]): ... + +foo.to(a='bar', b=[1,2]) +``` + + '/foo?a=bar&b=1&b=2' + +``` python +@app.get('/foo/{a}') +def foo(a:str, b:list[int]): ... + +foo.to(a='bar', b=[1,2]) +``` + + '/foo/bar?b=1&b=2' + +------------------------------------------------------------------------ + +source + +### serve + +> serve (appname=None, app='app', host='0.0.0.0', port=None, reload=True, +> reload_includes:list[str]|str|None=None, +> reload_excludes:list[str]|str|None=None) + +*Run the app in an async server, with live reload set as the default.* + + ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
appnameNoneTypeNoneName of the module
appstrappApp instance to be served
hoststr0.0.0.0If host is 0.0.0.0 will convert to localhost
portNoneTypeNoneIf port is None it will default to 5001 or the PORT environment +variable
reloadboolTrueDefault is to reload the app upon code changes
reload_includeslist[str] | str | NoneNoneAdditional files to watch for changes
reload_excludeslist[str] | str | NoneNoneFiles to ignore for changes
+ +------------------------------------------------------------------------ + +source + +### Client + +> Client (app, url='http://testserver') + +*A simple httpx ASGI client that doesn’t require `async`* + +``` python +app = FastHTML(routes=[Route('/', lambda _: Response('test'))]) +cli = Client(app) + +cli.get('/').text +``` + + 'test' + +Note that you can also use Starlette’s `TestClient` instead of +FastHTML’s [`Client`](https://www.fastht.ml/docs/api/core.html#client). +They should be largely interchangable. + +## FastHTML Tests + +``` python +def get_cli(app): return app,TestClient(app),app.route +``` + +``` python +app,cli,rt = get_cli(FastHTML(secret_key='soopersecret')) +``` + +``` python +app,cli,rt = get_cli(FastHTML(title="My Custom Title")) +@app.get +def foo(): return Div("Hello World") + +print(app.routes) + +response = cli.get('/foo') +assert 'My Custom Title' in response.text + +foo.to(param='value') +``` + + [Route(path='/foo', name='foo', methods=['GET', 'HEAD'])] + + '/foo?param=value' + +``` python +app,cli,rt = get_cli(FastHTML()) + +@rt('/xt2') +def get(): return H1('bar') + +txt = cli.get('/xt2').text +assert 'FastHTML page' in txt and '

bar

' in txt and '' in txt +``` + +``` python +@rt("/hi") +def get(): return 'Hi there' + +r = cli.get('/hi') +r.text +``` + + 'Hi there' + +``` python +@rt("/hi") +def post(): return 'Postal' + +cli.post('/hi').text +``` + + 'Postal' + +``` python +@app.get("/hostie") +def show_host(req): return req.headers['host'] + +cli.get('/hostie').text +``` + + 'testserver' + +``` python +@app.get("/setsess") +def set_sess(session): + session['foo'] = 'bar' + return 'ok' + +@app.ws("/ws") +def ws(self, msg:str, ws:WebSocket, session): return f"Message text was: {msg} with session {session.get('foo')}, from client: {ws.client}" + +cli.get('/setsess') +with cli.websocket_connect('/ws') as ws: + ws.send_text('{"msg":"Hi!"}') + data = ws.receive_text() +assert 'Message text was: Hi! with session bar' in data +print(data) +``` + + Message text was: Hi! with session bar, from client: Address(host='testclient', port=50000) + +``` python +@rt +def yoyo(): return 'a yoyo' + +cli.post('/yoyo').text +``` + + 'a yoyo' + +``` python +@app.get +def autopost(): return Html(Div('Text.', hx_post=yoyo())) +print(cli.get('/autopost').text) +``` + + + +
Text.
+ + +``` python +@app.get +def autopost2(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b')))) +print(cli.get('/autopost2').text) +``` + + + + +
Text.
+ + + +``` python +@app.get +def autoget2(): return Html(Div('Text.', hx_get=show_host)) +print(cli.get('/autoget2').text) +``` + + + +
Text.
+ + +``` python +@rt('/user/{nm}', name='gday') +def get(nm:str=''): return f"Good day to you, {nm}!" +cli.get('/user/Alexis').text +``` + + 'Good day to you, Alexis!' + +``` python +@app.get +def autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis'))) +print(cli.get('/autolink').text) +``` + + + +
Text.
+ + +``` python +@rt('/link') +def get(req): return f"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}" + +cli.get('/link').text +``` + + 'http://testserver/user/Alexis; http://testserver/hostie' + +``` python +@app.get("/background") +async def background_task(request): + async def long_running_task(): + await asyncio.sleep(0.1) + print("Background task completed!") + return P("Task started"), BackgroundTask(long_running_task) + +response = cli.get("/background") +``` + + Background task completed! + +``` python +test_eq(app.router.url_path_for('gday', nm='Jeremy'), '/user/Jeremy') +``` + +``` python +hxhdr = {'headers':{'hx-request':"1"}} + +@rt('/ft') +def get(): return Title('Foo'),H1('bar') + +txt = cli.get('/ft').text +assert 'Foo' in txt and '

bar

' in txt and '' in txt + +@rt('/xt2') +def get(): return H1('bar') + +txt = cli.get('/xt2').text +assert 'FastHTML page' in txt and '

bar

' in txt and '' in txt + +assert cli.get('/xt2', **hxhdr).text.strip() == '

bar

' + +@rt('/xt3') +def get(): return Html(Head(Title('hi')), Body(P('there'))) + +txt = cli.get('/xt3').text +assert 'FastHTML page' not in txt and 'hi' in txt and '

there

' in txt +``` + +``` python +@rt('/oops') +def get(nope): return nope +test_warns(lambda: cli.get('/oops?nope=1')) +``` + +``` python +def test_r(cli, path, exp, meth='get', hx=False, **kwargs): + if hx: kwargs['headers'] = {'hx-request':"1"} + test_eq(getattr(cli, meth)(path, **kwargs).text, exp) + +ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet") +fake_db = [{"name": "Foo"}, {"name": "Bar"}] +``` + +``` python +@rt('/html/{idx}') +async def get(idx:int): return Body(H4(f'Next is {idx+1}.')) +``` + +``` python +@rt("/models/{nm}") +def get(nm:ModelName): return nm + +@rt("/files/{path}") +async def get(path: Path): return path.with_suffix('.txt') + +@rt("/items/") +def get(idx:int|None = 0): return fake_db[idx] + +@rt("/idxl/") +def get(idx:list[int]): return str(idx) +``` + +``` python +r = cli.get('/html/1', headers={'hx-request':"1"}) +assert '

Next is 2.

' in r.text +test_r(cli, '/models/alexnet', 'alexnet') +test_r(cli, '/files/foo', 'foo.txt') +test_r(cli, '/items/?idx=1', '{"name":"Bar"}') +test_r(cli, '/items/', '{"name":"Foo"}') +assert cli.get('/items/?idx=g').text=='404 Not Found' +assert cli.get('/items/?idx=g').status_code == 404 +test_r(cli, '/idxl/?idx=1&idx=2', '[1, 2]') +assert cli.get('/idxl/?idx=1&idx=g').status_code == 404 +``` + +``` python +app = FastHTML() +rt = app.route +cli = TestClient(app) +@app.route(r'/static/{path:path}.jpg') +def index(path:str): return f'got {path}' +cli.get('/static/sub/a.b.jpg').text +``` + + 'got sub/a.b' + +``` python +app.chk = 'foo' +``` + +``` python +@app.get("/booly/") +def _(coming:bool=True): return 'Coming' if coming else 'Not coming' + +@app.get("/datie/") +def _(d:parsed_date): return d + +@app.get("/ua") +async def _(user_agent:str): return user_agent + +@app.get("/hxtest") +def _(htmx): return htmx.request + +@app.get("/hxtest2") +def _(foo:HtmxHeaders, req): return foo.request + +@app.get("/app") +def _(app): return app.chk + +@app.get("/app2") +def _(foo:FastHTML): return foo.chk,HttpHeader("mykey", "myval") + +@app.get("/app3") +def _(foo:FastHTML): return HtmxResponseHeaders(location="http://example.org") + +@app.get("/app4") +def _(foo:FastHTML): return Redirect("http://example.org") +``` + +``` python +test_r(cli, '/booly/?coming=true', 'Coming') +test_r(cli, '/booly/?coming=no', 'Not coming') +date_str = "17th of May, 2024, 2p" +test_r(cli, f'/datie/?d={date_str}', '2024-05-17 14:00:00') +test_r(cli, '/ua', 'FastHTML', headers={'User-Agent':'FastHTML'}) +test_r(cli, '/hxtest' , '1', headers={'HX-Request':'1'}) +test_r(cli, '/hxtest2', '1', headers={'HX-Request':'1'}) +test_r(cli, '/app' , 'foo') +``` + +``` python +r = cli.get('/app2', **hxhdr) +test_eq(r.text, 'foo') +test_eq(r.headers['mykey'], 'myval') +``` + +``` python +r = cli.get('/app3') +test_eq(r.headers['HX-Location'], 'http://example.org') +``` + +``` python +r = cli.get('/app4', follow_redirects=False) +test_eq(r.status_code, 303) +``` + +``` python +r = cli.get('/app4', headers={'HX-Request':'1'}) +test_eq(r.headers['HX-Redirect'], 'http://example.org') +``` + +``` python +@rt +def meta(): + return ((Title('hi'),H1('hi')), + (Meta(property='image'), Meta(property='site_name')) + ) + +t = cli.post('/meta').text +assert re.search(r'\s*

hi

\s*', t) +assert 'It worked!" in response and "

15, Lorem

" in response +``` + +``` python +# Testing POST with Content-Type: application/json +@app.post("/bodytext") +def index(body): return body + +response = cli.post('/bodytext', headers={"Content-Type": "application/json"}, data=s).text +test_eq(response, '{"b": "Lorem", "a": 15}') +``` + +``` python +files = [ ('files', ('file1.txt', b'content1')), + ('files', ('file2.txt', b'content2')) ] +``` + +``` python +@rt("/uploads") +async def post(files:list[UploadFile]): + return ','.join([(await file.read()).decode() for file in files]) + +res = cli.post('/uploads', files=files) +print(res.status_code) +print(res.text) +``` + + 200 + content1,content2 + +``` python +res = cli.post('/uploads', files=[files[0]]) +print(res.status_code) +print(res.text) +``` + + 200 + content1 + +``` python +@rt("/setsess") +def get(sess, foo:str=''): + now = datetime.now() + sess['auth'] = str(now) + return f'Set to {now}' + +@rt("/getsess") +def get(sess): return f'Session time: {sess["auth"]}' + +print(cli.get('/setsess').text) +time.sleep(0.01) + +cli.get('/getsess').text +``` + + Set to 2025-05-29 08:31:48.235262 + + 'Session time: 2025-05-29 08:31:48.235262' + +``` python +@rt("/sess-first") +def post(sess, name: str): + sess["name"] = name + return str(sess) + +cli.post('/sess-first', data={'name': 2}) + +@rt("/getsess-all") +def get(sess): return sess['name'] + +test_eq(cli.get('/getsess-all').text, '2') +``` + +``` python +@rt("/upload") +async def post(uf:UploadFile): return (await uf.read()).decode() + +with open('../../CHANGELOG.md', 'rb') as f: + print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15]) +``` + + # Release notes + +``` python +@rt("/form-submit/{list_id}") +def options(list_id: str): + headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST', + 'Access-Control-Allow-Headers': '*', + } + return Response(status_code=200, headers=headers) +``` + +``` python +h = cli.options('/form-submit/2').headers +test_eq(h['Access-Control-Allow-Methods'], 'POST') +``` + +``` python +from fasthtml.authmw import user_pwd_auth +``` + +``` python +def _not_found(req, exc): return Div('nope') + +app,cli,rt = get_cli(FastHTML(exception_handlers={404:_not_found})) + +txt = cli.get('/').text +assert '
nope
' in txt +assert '' in txt +``` + +``` python +app,cli,rt = get_cli(FastHTML()) + +@rt("/{name}/{age}") +def get(name: str, age: int): + return Titled(f"Hello {name.title()}, age {age}") + +assert 'Hello Uma, age 5' in cli.get('/uma/5').text +assert '404 Not Found' in cli.get('/uma/five').text +``` + +``` python +auth = user_pwd_auth(testuser='spycraft') +app,cli,rt = get_cli(FastHTML(middleware=[auth])) + +@rt("/locked") +def get(auth): return 'Hello, ' + auth + +test_eq(cli.get('/locked').text, 'not authenticated') +test_eq(cli.get('/locked', auth=("testuser","spycraft")).text, 'Hello, testuser') +``` + +``` python +auth = user_pwd_auth(testuser='spycraft') +app,cli,rt = get_cli(FastHTML(middleware=[auth])) + +@rt("/locked") +def get(auth): return 'Hello, ' + auth + +test_eq(cli.get('/locked').text, 'not authenticated') +test_eq(cli.get('/locked', auth=("testuser","spycraft")).text, 'Hello, testuser') +``` + +## APIRouter + +------------------------------------------------------------------------ + +source + +### RouteFuncs + +> RouteFuncs () + +*Initialize self. See help(type(self)) for accurate signature.* + +------------------------------------------------------------------------ + +source + +### APIRouter + +> APIRouter (prefix:str|None=None, body_wrap=) + +*Add routes to an app* + +``` python +ar = APIRouter() +``` + +``` python +@ar("/hi") +def get(): return 'Hi there' +@ar("/hi") +def post(): return 'Postal' +@ar +def ho(): return 'Ho ho' +@ar("/hostie") +def show_host(req): return req.headers['host'] +@ar +def yoyo(): return 'a yoyo' +@ar +def index(): return "home page" + +@ar.ws("/ws") +def ws(self, msg:str): return f"Message text was: {msg}" +``` + +``` python +app,cli,_ = get_cli(FastHTML()) +ar.to_app(app) +``` + +``` python +assert str(yoyo) == '/yoyo' +# ensure route functions are properly discoverable on `APIRouter` and `APIRouter.rt_funcs` +assert ar.prefix == '' +assert str(ar.rt_funcs.index) == '/' +assert str(ar.index) == '/' +with ExceptionExpected(): ar.blah() +with ExceptionExpected(): ar.rt_funcs.blah() +# ensure any route functions named using an HTTPMethod are not discoverable via `rt_funcs` +assert "get" not in ar.rt_funcs._funcs.keys() +``` + +``` python +test_eq(cli.get('/hi').text, 'Hi there') +test_eq(cli.post('/hi').text, 'Postal') +test_eq(cli.get('/hostie').text, 'testserver') +test_eq(cli.post('/yoyo').text, 'a yoyo') + +test_eq(cli.get('/ho').text, 'Ho ho') +test_eq(cli.post('/ho').text, 'Ho ho') +``` + +``` python +with cli.websocket_connect('/ws') as ws: + ws.send_text('{"msg":"Hi!"}') + data = ws.receive_text() + assert data == 'Message text was: Hi!' +``` + +``` python +ar2 = APIRouter("/products") +``` + +``` python +@ar2("/hi") +def get(): return 'Hi there' +@ar2("/hi") +def post(): return 'Postal' +@ar2 +def ho(): return 'Ho ho' +@ar2("/hostie") +def show_host(req): return req.headers['host'] +@ar2 +def yoyo(): return 'a yoyo' +@ar2 +def index(): return "home page" + +@ar2.ws("/ws") +def ws(self, msg:str): return f"Message text was: {msg}" +``` + +``` python +app,cli,_ = get_cli(FastHTML()) +ar2.to_app(app) +``` + +``` python +assert str(yoyo) == '/products/yoyo' +assert ar2.prefix == '/products' +assert str(ar2.rt_funcs.index) == '/products/' +assert str(ar2.index) == '/products/' +assert str(ar.index) == '/' +with ExceptionExpected(): ar2.blah() +with ExceptionExpected(): ar2.rt_funcs.blah() +assert "get" not in ar2.rt_funcs._funcs.keys() +``` + +``` python +test_eq(cli.get('/products/hi').text, 'Hi there') +test_eq(cli.post('/products/hi').text, 'Postal') +test_eq(cli.get('/products/hostie').text, 'testserver') +test_eq(cli.post('/products/yoyo').text, 'a yoyo') + +test_eq(cli.get('/products/ho').text, 'Ho ho') +test_eq(cli.post('/products/ho').text, 'Ho ho') +``` + +``` python +with cli.websocket_connect('/products/ws') as ws: + ws.send_text('{"msg":"Hi!"}') + data = ws.receive_text() + assert data == 'Message text was: Hi!' +``` + +``` python +@ar.get +def hi2(): return 'Hi there' +@ar.get("/hi3") +def _(): return 'Hi there' +@ar.post("/post2") +def _(): return 'Postal' + +@ar2.get +def hi2(): return 'Hi there' +@ar2.get("/hi3") +def _(): return 'Hi there' +@ar2.post("/post2") +def _(): return 'Postal' +``` + +## Extras + +``` python +app,cli,rt = get_cli(FastHTML(secret_key='soopersecret')) +``` + +------------------------------------------------------------------------ + +source + +### cookie + +> cookie (key:str, value='', max_age=None, expires=None, path='/', +> domain=None, secure=False, httponly=False, samesite='lax') + +*Create a ‘set-cookie’ +[`HttpHeader`](https://www.fastht.ml/docs/api/core.html#httpheader)* + +``` python +@rt("/setcookie") +def get(req): return cookie('now', datetime.now()) + +@rt("/getcookie") +def get(now:parsed_date): return f'Cookie was set at time {now.time()}' + +print(cli.get('/setcookie').text) +time.sleep(0.01) +cli.get('/getcookie').text +``` + + + + + FastHTML page + + + + + + + + 'Cookie was set at time 08:31:49.013668' + +------------------------------------------------------------------------ + +source + +### reg_re_param + +> reg_re_param (m, s) + +------------------------------------------------------------------------ + +source + +### FastHTML.static_route_exts + +> FastHTML.static_route_exts (prefix='/', static_path='.', exts='static') + +*Add a static route at URL path `prefix` with files from `static_path` +and `exts` defined by +[`reg_re_param()`](https://www.fastht.ml/docs/api/core.html#reg_re_param)* + +``` python +reg_re_param("imgext", "ico|gif|jpg|jpeg|webm|pdf") + +@rt(r'/static/{path:path}{fn}.{ext:imgext}') +def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}" + +test_r(cli, '/static/foo/jph.me.ico', 'Getting jph.me.ico from /foo/') +``` + +``` python +app.static_route_exts() +assert 'These are the source notebooks for FastHTML' in cli.get('/README.txt').text +``` + +------------------------------------------------------------------------ + +source + +### FastHTML.static_route + +> FastHTML.static_route (ext='', prefix='/', static_path='.') + +*Add a static route at URL path `prefix` with files from `static_path` +and single `ext` (including the ‘.’)* + +``` python +app.static_route('.md', static_path='../..') +assert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text +``` + +------------------------------------------------------------------------ + +source + +### MiddlewareBase + +> MiddlewareBase () + +*Initialize self. See help(type(self)) for accurate signature.* + +------------------------------------------------------------------------ + +source + +### FtResponse + +> FtResponse (content, status_code:int=200, headers=None, cls= 'starlette.responses.HTMLResponse'>, +> media_type:str|None=None, +> background:starlette.background.BackgroundTask|None=None) + +*Wrap an FT response with any Starlette `Response`* + +``` python +@rt('/ftr') +def get(): + cts = Title('Foo'),H1('bar') + return FtResponse(cts, status_code=201, headers={'Location':'/foo/1'}) + +r = cli.get('/ftr') + +test_eq(r.status_code, 201) +test_eq(r.headers['location'], '/foo/1') +txt = r.text +assert 'Foo' in txt and '

bar

' in txt and '' in txt +``` + +Test on a single background task: + +``` python +def my_slow_task(): + print('Starting slow task') + time.sleep(0.001) + print('Finished slow task') + +@rt('/background') +def get(): + return P('BG Task'), BackgroundTask(my_slow_task) + +r = cli.get('/background') + +test_eq(r.status_code, 200) +``` + + Starting slow task + Finished slow task + +Test multiple background tasks: + +``` python +def increment(amount): + amount = amount/1000 + print(f'Sleeping for {amount}s') + time.sleep(amount) + print(f'Slept for {amount}s') +``` + +``` python +@rt +def backgrounds(): + tasks = BackgroundTasks() + for i in range(3): tasks.add_task(increment, i) + return P('BG Tasks'), tasks + +r = cli.get('/backgrounds') +test_eq(r.status_code, 200) +``` + + Sleeping for 0.0s + Slept for 0.0s + Sleeping for 0.001s + Slept for 0.001s + Sleeping for 0.002s + Slept for 0.002s + +``` python +@rt +def backgrounds2(): + tasks = [BackgroundTask(increment,i) for i in range(3)] + return P('BG Tasks'), *tasks + +r = cli.get('/backgrounds2') +test_eq(r.status_code, 200) +``` + + Sleeping for 0.0s + Slept for 0.0s + Sleeping for 0.001s + Slept for 0.001s + Sleeping for 0.002s + Slept for 0.002s + +``` python +@rt +def backgrounds3(): + tasks = [BackgroundTask(increment,i) for i in range(3)] + return {'status':'done'}, *tasks + +r = cli.get('/backgrounds3') +test_eq(r.status_code, 200) +r.json() +``` + + Sleeping for 0.0s + Slept for 0.0s + Sleeping for 0.001s + Slept for 0.001s + Sleeping for 0.002s + Slept for 0.002s + + {'status': 'done'} + +------------------------------------------------------------------------ + +source + +### unqid + +> unqid (seeded=False) + +------------------------------------------------------------------------ + +source + +### setup_ws + +> setup_ws (app, f=) + +------------------------------------------------------------------------ + +source + +### FastHTML.devtools_json + +> FastHTML.devtools_json (path=None, uuid=None) diff --git a/docs/api/js.html b/docs/api/js.html new file mode 100644 index 0000000000000000000000000000000000000000..4a304622f60f4b5fd413e298a4f2a39f90021168 --- /dev/null +++ b/docs/api/js.html @@ -0,0 +1,1181 @@ + + + + + + + + + + +Javascript examples – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Javascript examples

+
+ +
+
+ Basic external Javascript lib wrappers +
+
+ + +
+ + + + +
+ + + +
+ + + +

To expedite fast development, FastHTML comes with several built-in Javascript and formatting components. These are largely provided to demonstrate FastHTML JS patterns. There’s far too many JS libs for FastHTML to wrap them all, and as shown here the code to add FastHTML support is very simple anyway.

+
+

source

+
+

light_media

+
+
 light_media (css:str)
+
+

Render light media for day mode views

+ + + + + + + + + + + + + + + +
TypeDetails
cssstrCSS to be included in the light media query
+
+
light_media('.body {color: green;}')
+
+
<style>@media (prefers-color-scheme: light) {.body {color: green;}}</style>
+
+
+
+

source

+
+
+

dark_media

+
+
 dark_media (css:str)
+
+

Render dark media for night mode views

+ + + + + + + + + + + + + + + +
TypeDetails
cssstrCSS to be included in the dark media query
+
+
dark_media('.body {color: white;}')
+
+
<style>@media (prefers-color-scheme:  dark) {.body {color: white;}}</style>
+
+
+
+

source

+
+
+

MarkdownJS

+
+
 MarkdownJS (sel='.marked')
+
+

Implements browser-based markdown rendering.

+ + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstr.markedCSS selector for markdown elements
+

Usage example here.

+
+
__file__ = '../../fasthtml/katex.js'
+
+
+

source

+
+
+

KatexMarkdownJS

+
+
 KatexMarkdownJS (sel='.marked', inline_delim='$', display_delim='$$',
+                  math_envs=None)
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstr.markedCSS selector for markdown elements
inline_delimstr$Delimiter for inline math
display_delimstr$$Delimiter for long math
math_envsNoneTypeNoneList of environments to render as display math
+

KatexMarkdown usage example:

+
longexample = r"""
+Long example:
+
+$$\begin{array}{c}
+
+\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
+= \frac{4\pi}{c}\vec{\mathbf{j}}    \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+
+\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+
+\nabla \cdot \vec{\mathbf{B}} & = 0
+
+\end{array}$$
+"""
+
+app, rt = fast_app(hdrs=[KatexMarkdownJS()])
+
+@rt('/')
+def get():
+    return Titled("Katex Examples", 
+        # Assigning 'marked' class to components renders content as markdown
+        P(cls='marked')("Inline example: $\sqrt{3x-1}+(1+x)^2$"),
+        Div(cls='marked')(longexample)
+    )
+
+

source

+
+
+

HighlightJS

+
+
 HighlightJS (sel='pre code:not([data-highlighted="yes"])',
+              langs:str|list|tuple='python', light='atom-one-light',
+              dark='atom-one-dark')
+
+

Implements browser-based syntax highlighting. Usage example here.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstrpre code:not([data-highlighted=“yes”])CSS selector for code elements. Default is industry standard, be careful before adjusting it
langsstr | list | tuplepythonLanguage(s) to highlight
lightstratom-one-lightLight theme
darkstratom-one-darkDark theme
+
+

source

+
+
+

SortableJS

+
+
 SortableJS (sel='.sortable', ghost_class='blue-background-class')
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstr.sortableCSS selector for sortable elements
ghost_classstrblue-background-classWhen an element is being dragged, this is the class used to distinguish it from the rest
+
+

source

+
+
+

MermaidJS

+
+
 MermaidJS (sel='.language-mermaid', theme='base')
+
+

Implements browser-based Mermaid diagram rendering.

+ + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstr.language-mermaidCSS selector for mermaid elements
themestrbaseMermaid theme to use
+
app, rt = fast_app(hdrs=[MermaidJS()])
+@rt('/')
+def get():
+    return Titled("Mermaid Examples", 
+        # Assigning 'marked' class to components renders content as markdown
+        Pre(Code(cls ="language-mermaid")('''flowchart TD
+            A[main] --> B["fact(5)"] --> C["fact(4)"] --> D["fact(3)"] --> E["fact(2)"] --> F["fact(1)"] --> G["fact(0)"]
+           ''')))
+

In a markdown file, just like a code cell you can define

+

```mermaid

+
    graph TD
+    A --> B 
+    B --> C 
+    C --> E
+

```

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/js.html.md b/docs/api/js.html.md new file mode 100644 index 0000000000000000000000000000000000000000..021e51b437b8d4b1df523826c756ef676973bf43 --- /dev/null +++ b/docs/api/js.html.md @@ -0,0 +1,365 @@ +# Javascript examples + + + + +To expedite fast development, FastHTML comes with several built-in +Javascript and formatting components. These are largely provided to +demonstrate FastHTML JS patterns. There’s far too many JS libs for +FastHTML to wrap them all, and as shown here the code to add FastHTML +support is very simple anyway. + +------------------------------------------------------------------------ + +source + +### light_media + +> light_media (css:str) + +*Render light media for day mode views* + + + + + + + + + + + + + + + + +
TypeDetails
cssstrCSS to be included in the light media query
+ +``` python +light_media('.body {color: green;}') +``` + +``` html + +``` + +------------------------------------------------------------------------ + +source + +### dark_media + +> dark_media (css:str) + +*Render dark media for night mode views* + + + + + + + + + + + + + + + + +
TypeDetails
cssstrCSS to be included in the dark media query
+ +``` python +dark_media('.body {color: white;}') +``` + +``` html + +``` + +------------------------------------------------------------------------ + +source + +### MarkdownJS + +> MarkdownJS (sel='.marked') + +*Implements browser-based markdown rendering.* + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstr.markedCSS selector for markdown elements
+ +Usage example +[here](../tutorials/quickstart_for_web_devs.html#rendering-markdown). + +``` python +__file__ = '../../fasthtml/katex.js' +``` + +------------------------------------------------------------------------ + +source + +### KatexMarkdownJS + +> KatexMarkdownJS (sel='.marked', inline_delim='$', display_delim='$$', +> math_envs=None) + + ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstr.markedCSS selector for markdown elements
inline_delimstr$Delimiter for inline math
display_delimstr$$Delimiter for long math
math_envsNoneTypeNoneList of environments to render as display math
+ +KatexMarkdown usage example: + +``` python +longexample = r""" +Long example: + +$$\begin{array}{c} + +\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & += \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\ + +\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\ + +\nabla \cdot \vec{\mathbf{B}} & = 0 + +\end{array}$$ +""" + +app, rt = fast_app(hdrs=[KatexMarkdownJS()]) + +@rt('/') +def get(): + return Titled("Katex Examples", + # Assigning 'marked' class to components renders content as markdown + P(cls='marked')("Inline example: $\sqrt{3x-1}+(1+x)^2$"), + Div(cls='marked')(longexample) + ) +``` + +------------------------------------------------------------------------ + +source + +### HighlightJS + +> HighlightJS (sel='pre code:not([data-highlighted="yes"])', +> langs:str|list|tuple='python', light='atom-one-light', +> dark='atom-one-dark') + +*Implements browser-based syntax highlighting. Usage example +[here](../tutorials/quickstart_for_web_devs.html#code-highlighting).* + + ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstrpre code:not([data-highlighted=“yes”])CSS selector for code elements. Default is industry standard, be +careful before adjusting it
langsstr | list | tuplepythonLanguage(s) to highlight
lightstratom-one-lightLight theme
darkstratom-one-darkDark theme
+ +------------------------------------------------------------------------ + +source + +### SortableJS + +> SortableJS (sel='.sortable', ghost_class='blue-background-class') + + ++++++ + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstr.sortableCSS selector for sortable elements
ghost_classstrblue-background-classWhen an element is being dragged, this is the class used to +distinguish it from the rest
+ +------------------------------------------------------------------------ + +source + +### MermaidJS + +> MermaidJS (sel='.language-mermaid', theme='base') + +*Implements browser-based Mermaid diagram rendering.* + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
selstr.language-mermaidCSS selector for mermaid elements
themestrbaseMermaid theme to use
+ +``` python +app, rt = fast_app(hdrs=[MermaidJS()]) +@rt('/') +def get(): + return Titled("Mermaid Examples", + # Assigning 'marked' class to components renders content as markdown + Pre(Code(cls ="language-mermaid")('''flowchart TD + A[main] --> B["fact(5)"] --> C["fact(4)"] --> D["fact(3)"] --> E["fact(2)"] --> F["fact(1)"] --> G["fact(0)"] + '''))) +``` + +In a markdown file, just like a code cell you can define + +\`\`\`mermaid + + graph TD + A --> B + B --> C + C --> E + +\`\`\` diff --git a/docs/api/jupyter.html b/docs/api/jupyter.html new file mode 100644 index 0000000000000000000000000000000000000000..ec66e3fe538b612d369f0cd6cb5eb4c6ec344e4c --- /dev/null +++ b/docs/api/jupyter.html @@ -0,0 +1,1199 @@ + + + + + + + + + + +Jupyter compatibility – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Jupyter compatibility

+
+ +
+
+ Use FastHTML in Jupyter notebooks +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from httpx import get, AsyncClient
+
+
+

Helper functions

+
+

source

+
+

nb_serve

+
+
 nb_serve (app, log_level='error', port=8000, host='0.0.0.0', **kwargs)
+
+

Start a Jupyter compatible uvicorn server with ASGI app on port with log_level

+
+

source

+
+
+

nb_serve_async

+
+
 nb_serve_async (app, log_level='error', port=8000, host='0.0.0.0',
+                 **kwargs)
+
+

Async version of nb_serve

+
+

source

+
+
+

is_port_free

+
+
 is_port_free (port, host='localhost')
+
+

Check if port is free on host

+
+

source

+
+
+

wait_port_free

+
+
 wait_port_free (port, host='localhost', max_wait=3)
+
+

Wait for port to be free on host

+
+
+
+

Using FastHTML in Jupyter

+
+

source

+
+

show

+
+
 show (*s, iframe=False, height='auto', style=None)
+
+

Same as fasthtml.components.show, but also adds htmx.process()

+
+

source

+
+
+

render_ft

+
+
 render_ft ()
+
+
+

source

+
+
+

htmx_config_port

+
+
 htmx_config_port (port=8000)
+
+
+

source

+
+
+

JupyUvi

+
+
 JupyUvi (app, log_level='error', host='0.0.0.0', port=8000, start=True,
+          **kwargs)
+
+

Start and stop a Jupyter compatible uvicorn server with ASGI app on port with log_level

+

Creating an object of this class also starts the Uvicorn server. It runs in a separate thread, so you can use normal HTTP client functions in a notebook.

+
+
app = FastHTML()
+rt = app.route
+
+@app.route
+def index(): return 'hi'
+
+port = 8000
+server = JupyUvi(app, port=port)
+
+ + +
+
+
+
get(f'http://localhost:{port}').text
+
+
'hi'
+
+
+

You can stop the server, modify routes, and start the server again without restarting the notebook or recreating the server or application.

+
+
server.stop()
+
+
+
app = FastHTML()
+rt = app.route
+
+@app.route
+async def index(): return 'hi'
+
+server = JupyUvi(app, port=port, start=False)
+await server.start_async()
+
+ + +
+
+
+
print((await AsyncClient().get(f'http://localhost:{port}')).text)
+
+
hi
+
+
+
+

source

+
+
+

JupyUviAsync

+
+
 JupyUviAsync (app, log_level='error', host='0.0.0.0', port=8000,
+               **kwargs)
+
+

Start and stop an async Jupyter compatible uvicorn server with ASGI app on port with log_level

+
+
server = JupyUviAsync(app, port=port)
+await server.start()
+
+ + +
+
+
+
async with AsyncClient() as client:
+    r = await client.get(f'http://localhost:{port}')
+print(r.text)
+
+
hi
+
+
+
+
server.stop()
+
+
+
+

Using a notebook as a web app

+

You can also run an HTMX web app directly in a notebook. To make this work, you have to add the default FastHTML headers to the DOM of the notebook with show(*def_hdrs()). Additionally, you might find it convenient to use auto_id mode, in which the ID of an FT object is automatically generated if not provided.

+
+
fh_cfg['auto_id' ]=True
+
+

After importing fasthtml.jupyter and calling render_ft(), FT components render directly in the notebook.

+
+
show(*def_hdrs())
+render_ft()
+
+ + + +
+
+
+
(c := Div('Cogito ergo sum'))
+
+
+
+Cogito ergo sum +
+ +
+
+
+

Handlers are written just like a regular web app:

+
+
server = JupyUvi(app, port=port)
+
+ + +
+
+
+
@rt
+def hoho(): return P('loaded!'), Div('hee hee', id=c, hx_swap_oob='true')
+
+

All the usual hx_* attributes can be used:

+
+
P('not loaded', hx_get=hoho, hx_trigger='load')
+
+
+

+not loaded +

+ +
+
+
+

FT components can be used directly both as id values and as hx_target values.

+
+
(c := Div(''))
+
+
+
+ +
+ +
+
+
+
+
@rt
+def foo(): return Div('foo bar')
+P('hi', hx_get=foo, hx_trigger='load', hx_target=c)
+
+
+

+hi +

+ +
+
+
+
+
server.stop()
+
+
+
+

Running apps in an IFrame

+

Using an IFrame can be a good idea to get complete isolation of the styles and scripts in an app. The HTMX function creates an auto-sizing IFrame for a web app.

+
+

source

+
+
+

HTMX

+
+
 HTMX (path='', app=None, host='localhost', port=8000, height='auto',
+       link=False, iframe=True)
+
+

An iframe which displays the HTMX application in a notebook.

+
+
@rt
+def index():
+    return Div(
+        P(A('Click me', hx_get=update, hx_target='#result')),
+        P(A('No me!', hx_get=update, hx_target='#result')),
+        Div(id='result'))
+
+@rt
+def update(): return Div(P('Hi!'),P('There!'))
+
+
+
server.start()
+
+
+
# Run the notebook locally to see the HTMX iframe in action
+HTMX()
+
+ +
+
+
+
server.stop()
+
+
+

source

+
+
+

ws_client

+
+
 ws_client (app, nm='', host='localhost', port=8000, ws_connect='/ws',
+            frame=True, link=True, **kwargs)
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/jupyter.html.md b/docs/api/jupyter.html.md new file mode 100644 index 0000000000000000000000000000000000000000..54e992638e7c48e3feed0ef151bace8c9d6ef57c --- /dev/null +++ b/docs/api/jupyter.html.md @@ -0,0 +1,388 @@ +# Jupyter compatibility + + + + +``` python +from httpx import get, AsyncClient +``` + +## Helper functions + +------------------------------------------------------------------------ + +source + +### nb_serve + +> nb_serve (app, log_level='error', port=8000, host='0.0.0.0', **kwargs) + +*Start a Jupyter compatible uvicorn server with ASGI `app` on `port` +with `log_level`* + +------------------------------------------------------------------------ + +source + +### nb_serve_async + +> nb_serve_async (app, log_level='error', port=8000, host='0.0.0.0', +> **kwargs) + +*Async version of +[`nb_serve`](https://www.fastht.ml/docs/api/jupyter.html#nb_serve)* + +------------------------------------------------------------------------ + +source + +### is_port_free + +> is_port_free (port, host='localhost') + +*Check if `port` is free on `host`* + +------------------------------------------------------------------------ + +source + +### wait_port_free + +> wait_port_free (port, host='localhost', max_wait=3) + +*Wait for `port` to be free on `host`* + +## Using FastHTML in Jupyter + +------------------------------------------------------------------------ + +source + +### show + +> show (*s, iframe=False, height='auto', style=None) + +*Same as fasthtml.components.show, but also adds `htmx.process()`* + +------------------------------------------------------------------------ + +source + +### render_ft + +> render_ft () + +------------------------------------------------------------------------ + +source + +### htmx_config_port + +> htmx_config_port (port=8000) + +------------------------------------------------------------------------ + +source + +### JupyUvi + +> JupyUvi (app, log_level='error', host='0.0.0.0', port=8000, start=True, +> **kwargs) + +*Start and stop a Jupyter compatible uvicorn server with ASGI `app` on +`port` with `log_level`* + +Creating an object of this class also starts the Uvicorn server. It runs +in a separate thread, so you can use normal HTTP client functions in a +notebook. + +``` python +app = FastHTML() +rt = app.route + +@app.route +def index(): return 'hi' + +port = 8000 +server = JupyUvi(app, port=port) +``` + + + +``` python +get(f'http://localhost:{port}').text +``` + + 'hi' + +You can stop the server, modify routes, and start the server again +without restarting the notebook or recreating the server or application. + +``` python +server.stop() +``` + +``` python +app = FastHTML() +rt = app.route + +@app.route +async def index(): return 'hi' + +server = JupyUvi(app, port=port, start=False) +await server.start_async() +``` + + + +``` python +print((await AsyncClient().get(f'http://localhost:{port}')).text) +``` + + hi + +------------------------------------------------------------------------ + +source + +### JupyUviAsync + +> JupyUviAsync (app, log_level='error', host='0.0.0.0', port=8000, +> **kwargs) + +*Start and stop an async Jupyter compatible uvicorn server with ASGI +`app` on `port` with `log_level`* + +``` python +server = JupyUviAsync(app, port=port) +await server.start() +``` + + + +``` python +async with AsyncClient() as client: + r = await client.get(f'http://localhost:{port}') +print(r.text) +``` + + hi + +``` python +server.stop() +``` + +### Using a notebook as a web app + +You can also run an HTMX web app directly in a notebook. To make this +work, you have to add the default FastHTML headers to the DOM of the +notebook with `show(*def_hdrs())`. Additionally, you might find it +convenient to use *auto_id* mode, in which the ID of an `FT` object is +automatically generated if not provided. + +``` python +fh_cfg['auto_id' ]=True +``` + +After importing `fasthtml.jupyter` and calling +[`render_ft()`](https://www.fastht.ml/docs/api/jupyter.html#render_ft), +FT components render directly in the notebook. + +``` python +show(*def_hdrs()) +render_ft() +``` + + + + + +``` python +(c := Div('Cogito ergo sum')) +``` + +
+ +
+ +Cogito ergo sum + +
+ + + +
+ +Handlers are written just like a regular web app: + +``` python +server = JupyUvi(app, port=port) +``` + + + +``` python +@rt +def hoho(): return P('loaded!'), Div('hee hee', id=c, hx_swap_oob='true') +``` + +All the usual `hx_*` attributes can be used: + +``` python +P('not loaded', hx_get=hoho, hx_trigger='load') +``` + +
+ +

+ +not loaded +

+ + + +
+ +FT components can be used directly both as `id` values and as +`hx_target` values. + +``` python +(c := Div('')) +``` + +
+ +
+ +
+ + + +
+ +``` python +@rt +def foo(): return Div('foo bar') +P('hi', hx_get=foo, hx_trigger='load', hx_target=c) +``` + +
+ +

+ +hi +

+ + + +
+ +``` python +server.stop() +``` + +### Running apps in an IFrame + +Using an IFrame can be a good idea to get complete isolation of the +styles and scripts in an app. The +[`HTMX`](https://www.fastht.ml/docs/api/jupyter.html#htmx) function +creates an auto-sizing IFrame for a web app. + +------------------------------------------------------------------------ + +source + +### HTMX + +> HTMX (path='', app=None, host='localhost', port=8000, height='auto', +> link=False, iframe=True) + +*An iframe which displays the HTMX application in a notebook.* + +``` python +@rt +def index(): + return Div( + P(A('Click me', hx_get=update, hx_target='#result')), + P(A('No me!', hx_get=update, hx_target='#result')), + Div(id='result')) + +@rt +def update(): return Div(P('Hi!'),P('There!')) +``` + +``` python +server.start() +``` + +``` python +# Run the notebook locally to see the HTMX iframe in action +HTMX() +``` + + + +``` python +server.stop() +``` + +------------------------------------------------------------------------ + +source + +### ws_client + +> ws_client (app, nm='', host='localhost', port=8000, ws_connect='/ws', +> frame=True, link=True, **kwargs) diff --git a/docs/api/oauth.html b/docs/api/oauth.html new file mode 100644 index 0000000000000000000000000000000000000000..d254763ddcad61970873fe6eeb3c730b1bd590d3 --- /dev/null +++ b/docs/api/oauth.html @@ -0,0 +1,1154 @@ + + + + + + + + + + +OAuth – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

OAuth

+
+ +
+
+ Basic scaffolding for handling OAuth +
+
+ + +
+ + + + +
+ + + +
+ + + +

See the docs page for an explanation of how to use this.

+
+
from IPython.display import Markdown
+
+
+

source

+
+

GoogleAppClient

+
+
 GoogleAppClient (client_id, client_secret, code=None, scope=None,
+                  project_id=None, **kwargs)
+
+

A WebApplicationClient for Google oauth2

+
+

source

+
+
+

GitHubAppClient

+
+
 GitHubAppClient (client_id, client_secret, code=None, scope=None,
+                  **kwargs)
+
+

A WebApplicationClient for GitHub oauth2

+
+

source

+
+
+

HuggingFaceClient

+
+
 HuggingFaceClient (client_id, client_secret, code=None, scope=None,
+                    state=None, **kwargs)
+
+

A WebApplicationClient for HuggingFace oauth2

+
+

source

+
+
+

DiscordAppClient

+
+
 DiscordAppClient (client_id, client_secret, is_user=False, perms=0,
+                   scope=None, **kwargs)
+
+

A WebApplicationClient for Discord oauth2

+
+

source

+
+
+

Auth0AppClient

+
+
 Auth0AppClient (domain, client_id, client_secret, code=None, scope=None,
+                 redirect_uri='', **kwargs)
+
+

A WebApplicationClient for Auth0 OAuth2

+
+
# cli = GoogleAppClient.from_file('client_secret.json')
+
+
+

source

+
+ +
+

redir_url

+
+
 redir_url (request, redir_path, scheme=None)
+
+

Get the redir url for the host in request

+
+
@rt
+def index(request):
+    redir = redir_url(request, redir_path)
+    return A('login', href=cli.login_link(redir), target='_blank')
+
+
+

source

+
+
+

_AppClient.parse_response

+
+
 _AppClient.parse_response (code, redirect_uri)
+
+

Get the token from the oauth2 server response

+
+

source

+
+
+

_AppClient.get_info

+
+
 _AppClient.get_info (token=None)
+
+

Get the info for authenticated user

+
+

source

+
+
+

_AppClient.retr_info

+
+
 _AppClient.retr_info (code, redirect_uri)
+
+

Combines parse_response and get_info

+
+
@rt(redir_path)
+def get(request, code:str):
+    redir = redir_url(request, redir_path)
+    info = cli.retr_info(code, redir)
+    return P(f'Login successful for {info["name"]}!')
+
+
+
# HTMX()
+
+
+
server.stop()
+
+
+

source

+
+
+

_AppClient.retr_id

+
+
 _AppClient.retr_id (code, redirect_uri)
+
+

Call retr_info and then return id/subscriber value

+

After logging in via the provider, the user will be redirected back to the supplied redirect URL. The request to this URL will contain a code parameter, which is used to get an access token and fetch the user’s profile information. See the explanation here for a worked example. You can either:

+
    +
  • Use client.retr_info(code) to get all the profile information, or
  • +
  • Use client.retr_id(code) to get just the user’s ID.
  • +
+

After either of these calls, you can also access the access token (used to revoke access, for example) with client.token["access_token"].

+
+

source

+
+
+

url_match

+
+
 url_match (url, patterns=('^(localhost|127\\.0\\.0\\.1)(:\\d+)?$',))
+
+
+

source

+
+
+

OAuth

+
+
 OAuth (app, cli, skip=None, redir_path='/redirect', error_path='/error',
+        logout_path='/logout', login_path='/login', https=True,
+        http_patterns=('^(localhost|127\\.0\\.0\\.1)(:\\d+)?$',))
+
+

Initialize self. See help(type(self)) for accurate signature.

+
+
+

Google helpers

+
+

source

+
+
+

GoogleAppClient.consent_url

+
+
 GoogleAppClient.consent_url (proj=None)
+
+

Get Google OAuth consent screen URL

+
+

source

+
+
+

GoogleAppClient.consent_url

+
+
 GoogleAppClient.consent_url (proj=None)
+
+

Get Google OAuth consent screen URL

+
+

source

+
+
+

Credentials.update

+
+
 Credentials.update ()
+
+

Refresh the credentials if they are expired, and return them

+
+

source

+
+
+

Credentials.update

+
+
 Credentials.update ()
+
+

Refresh the credentials if they are expired, and return them

+
+

source

+
+
+

Credentials.save

+
+
 Credentials.save (fname)
+
+

Save credentials to fname

+
+

source

+
+
+

Credentials.save

+
+
 Credentials.save (fname)
+
+

Save credentials to fname

+
+

source

+
+
+

load_creds

+
+
 load_creds (fname)
+
+

Load credentials from fname

+
+

source

+
+
+

GoogleAppClient.creds

+
+
 GoogleAppClient.creds ()
+
+

Create Credentials from the client, refreshing if needed

+
+

source

+
+
+

GoogleAppClient.creds

+
+
 GoogleAppClient.creds ()
+
+

Create Credentials from the client, refreshing if needed

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/oauth.html.md b/docs/api/oauth.html.md new file mode 100644 index 0000000000000000000000000000000000000000..2f5a123ae284d73c31dcddd02733144855a34e4e --- /dev/null +++ b/docs/api/oauth.html.md @@ -0,0 +1,352 @@ +# OAuth + + + + +See the [docs page](https://www.fastht.ml/docs/explains/oauth.html) for +an explanation of how to use this. + +``` python +from IPython.display import Markdown +``` + +------------------------------------------------------------------------ + +source + +### GoogleAppClient + +> GoogleAppClient (client_id, client_secret, code=None, scope=None, +> project_id=None, **kwargs) + +*A `WebApplicationClient` for Google oauth2* + +------------------------------------------------------------------------ + +source + +### GitHubAppClient + +> GitHubAppClient (client_id, client_secret, code=None, scope=None, +> **kwargs) + +*A `WebApplicationClient` for GitHub oauth2* + +------------------------------------------------------------------------ + +source + +### HuggingFaceClient + +> HuggingFaceClient (client_id, client_secret, code=None, scope=None, +> state=None, **kwargs) + +*A `WebApplicationClient` for HuggingFace oauth2* + +------------------------------------------------------------------------ + +source + +### DiscordAppClient + +> DiscordAppClient (client_id, client_secret, is_user=False, perms=0, +> scope=None, **kwargs) + +*A `WebApplicationClient` for Discord oauth2* + +------------------------------------------------------------------------ + +source + +### Auth0AppClient + +> Auth0AppClient (domain, client_id, client_secret, code=None, scope=None, +> redirect_uri='', **kwargs) + +*A `WebApplicationClient` for Auth0 OAuth2* + +``` python +# cli = GoogleAppClient.from_file('client_secret.json') +``` + +------------------------------------------------------------------------ + +source + +### WebApplicationClient.login_link + +> WebApplicationClient.login_link (redirect_uri, scope=None, state=None, +> **kwargs) + +*Get a login link for this client* + +Generating a login link that sends the user to the OAuth provider is +done with `client.login_link()`. + +It can sometimes be useful to pass state to the OAuth provider, so that +when the user returns you can pick up where they left off. This can be +done by passing the `state` parameter. + +``` python +from fasthtml.jupyter import * +``` + +``` python +redir_path = '/redirect' +port = 8000 +``` + +``` python +app,rt = fast_app() +server = JupyUvi(app, port=port) +``` + + + +------------------------------------------------------------------------ + +source + +### redir_url + +> redir_url (request, redir_path, scheme=None) + +*Get the redir url for the host in `request`* + +``` python +@rt +def index(request): + redir = redir_url(request, redir_path) + return A('login', href=cli.login_link(redir), target='_blank') +``` + +------------------------------------------------------------------------ + +source + +### \_AppClient.parse_response + +> _AppClient.parse_response (code, redirect_uri) + +*Get the token from the oauth2 server response* + +------------------------------------------------------------------------ + +source + +### \_AppClient.get_info + +> _AppClient.get_info (token=None) + +*Get the info for authenticated user* + +------------------------------------------------------------------------ + +source + +### \_AppClient.retr_info + +> _AppClient.retr_info (code, redirect_uri) + +*Combines `parse_response` and `get_info`* + +``` python +@rt(redir_path) +def get(request, code:str): + redir = redir_url(request, redir_path) + info = cli.retr_info(code, redir) + return P(f'Login successful for {info["name"]}!') +``` + +``` python +# HTMX() +``` + +``` python +server.stop() +``` + +------------------------------------------------------------------------ + +source + +### \_AppClient.retr_id + +> _AppClient.retr_id (code, redirect_uri) + +*Call `retr_info` and then return id/subscriber value* + +After logging in via the provider, the user will be redirected back to +the supplied redirect URL. The request to this URL will contain a `code` +parameter, which is used to get an access token and fetch the user’s +profile information. See [the explanation +here](https://www.fastht.ml/docs/explains/oauth.html) for a worked +example. You can either: + +- Use client.retr_info(code) to get all the profile information, or +- Use client.retr_id(code) to get just the user’s ID. + +After either of these calls, you can also access the access token (used +to revoke access, for example) with `client.token["access_token"]`. + +------------------------------------------------------------------------ + +source + +### url_match + +> url_match (url, patterns=('^(localhost|127\\.0\\.0\\.1)(:\\d+)?$',)) + +------------------------------------------------------------------------ + +source + +### OAuth + +> OAuth (app, cli, skip=None, redir_path='/redirect', error_path='/error', +> logout_path='/logout', login_path='/login', https=True, +> http_patterns=('^(localhost|127\\.0\\.0\\.1)(:\\d+)?$',)) + +*Initialize self. See help(type(self)) for accurate signature.* + +### Google helpers + +------------------------------------------------------------------------ + +source + +### GoogleAppClient.consent_url + +> GoogleAppClient.consent_url (proj=None) + +*Get Google OAuth consent screen URL* + +------------------------------------------------------------------------ + +source + +### GoogleAppClient.consent_url + +> GoogleAppClient.consent_url (proj=None) + +*Get Google OAuth consent screen URL* + +------------------------------------------------------------------------ + +source + +### Credentials.update + +> Credentials.update () + +*Refresh the credentials if they are expired, and return them* + +------------------------------------------------------------------------ + +source + +### Credentials.update + +> Credentials.update () + +*Refresh the credentials if they are expired, and return them* + +------------------------------------------------------------------------ + +source + +### Credentials.save + +> Credentials.save (fname) + +*Save credentials to `fname`* + +------------------------------------------------------------------------ + +source + +### Credentials.save + +> Credentials.save (fname) + +*Save credentials to `fname`* + +------------------------------------------------------------------------ + +source + +### load_creds + +> load_creds (fname) + +*Load credentials from `fname`* + +------------------------------------------------------------------------ + +source + +### GoogleAppClient.creds + +> GoogleAppClient.creds () + +*Create `Credentials` from the client, refreshing if needed* + +------------------------------------------------------------------------ + +source + +### GoogleAppClient.creds + +> GoogleAppClient.creds () + +*Create `Credentials` from the client, refreshing if needed* diff --git a/docs/api/pico.html b/docs/api/pico.html new file mode 100644 index 0000000000000000000000000000000000000000..8210cf1ada8302ad9b95212439c7745ce34a7aa9 --- /dev/null +++ b/docs/api/pico.html @@ -0,0 +1,1290 @@ + + + + + + + + + + +Pico.css components – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Pico.css components

+
+ +
+
+ Basic components for generating Pico CSS tags +
+
+ + +
+ + + + +
+ + + +
+ + + +

picocondlink is the class-conditional css link tag, and picolink is the regular tag.

+
+
show(picocondlink)
+
+ + +
+
+
+

source

+
+

set_pico_cls

+
+
 set_pico_cls ()
+
+

Run this to make jupyter outputs styled with pico:

+
+
set_pico_cls()
+
+ +
+
+
+

source

+
+
+

Card

+
+
 Card (*c, header=None, footer=None, target_id=None, hx_vals=None,
+       hx_target=None, id=None, cls=None, title=None, style=None,
+       accesskey=None, contenteditable=None, dir=None, draggable=None,
+       enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+       lang=None, popover=None, spellcheck=None, tabindex=None,
+       translate=None, hx_get=None, hx_post=None, hx_put=None,
+       hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
+       hx_swap_oob=None, hx_include=None, hx_select=None,
+       hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+       hx_confirm=None, hx_disable=None, hx_replace_url=None,
+       hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+       hx_history=None, hx_history_elt=None, hx_inherit=None,
+       hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
+       hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,
+       hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,
+       hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,
+       hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,
+       hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,
+       hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,
+       hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+       hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+       hx_on__after_on_load=None, hx_on__after_process_node=None,
+       hx_on__after_request=None, hx_on__after_settle=None,
+       hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+       hx_on__before_on_load=None, hx_on__before_process_node=None,
+       hx_on__before_request=None, hx_on__before_swap=None,
+       hx_on__before_send=None, hx_on__before_transition=None,
+       hx_on__config_request=None, hx_on__confirm=None,
+       hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+       hx_on__history_cache_miss_error=None,
+       hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+       hx_on__before_history_save=None, hx_on__load=None,
+       hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+       hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+       hx_on__oob_error_no_target=None, hx_on__prompt=None,
+       hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+       hx_on__response_error=None, hx_on__send_abort=None,
+       hx_on__send_error=None, hx_on__sse_error=None,
+       hx_on__sse_open=None, hx_on__swap_error=None,
+       hx_on__target_error=None, hx_on__timeout=None,
+       hx_on__validation_validate=None, hx_on__validation_failed=None,
+       hx_on__validation_halted=None, hx_on__xhr_abort=None,
+       hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+       hx_on__xhr_progress=None, **kwargs)
+
+

A PicoCSS Card, implemented as an Article with optional Header and Footer

+
+
show(Card('body', header=P('head'), footer=P('foot')))
+
+
+

head

+
+body +

foot

+
+
+
+
+
+

source

+
+
+

Group

+
+
 Group (*c, target_id=None, hx_vals=None, hx_target=None, id=None,
+        cls=None, title=None, style=None, accesskey=None,
+        contenteditable=None, dir=None, draggable=None, enterkeyhint=None,
+        hidden=None, inert=None, inputmode=None, lang=None, popover=None,
+        spellcheck=None, tabindex=None, translate=None, hx_get=None,
+        hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
+        hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
+        hx_select=None, hx_select_oob=None, hx_indicator=None,
+        hx_push_url=None, hx_confirm=None, hx_disable=None,
+        hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
+        hx_headers=None, hx_history=None, hx_history_elt=None,
+        hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
+        hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,
+        hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+        hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+        hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+        hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+        hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,
+        hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,
+        hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+        hx_on__abort=None, hx_on__after_on_load=None,
+        hx_on__after_process_node=None, hx_on__after_request=None,
+        hx_on__after_settle=None, hx_on__after_swap=None,
+        hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+        hx_on__before_process_node=None, hx_on__before_request=None,
+        hx_on__before_swap=None, hx_on__before_send=None,
+        hx_on__before_transition=None, hx_on__config_request=None,
+        hx_on__confirm=None, hx_on__history_cache_error=None,
+        hx_on__history_cache_miss=None,
+        hx_on__history_cache_miss_error=None,
+        hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+        hx_on__before_history_save=None, hx_on__load=None,
+        hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+        hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+        hx_on__oob_error_no_target=None, hx_on__prompt=None,
+        hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+        hx_on__response_error=None, hx_on__send_abort=None,
+        hx_on__send_error=None, hx_on__sse_error=None,
+        hx_on__sse_open=None, hx_on__swap_error=None,
+        hx_on__target_error=None, hx_on__timeout=None,
+        hx_on__validation_validate=None, hx_on__validation_failed=None,
+        hx_on__validation_halted=None, hx_on__xhr_abort=None,
+        hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+        hx_on__xhr_progress=None, **kwargs)
+
+

A PicoCSS Group, implemented as a Fieldset with role ‘group’

+
+
show(Group(Input(), Button("Save")))
+
+
+ + +
+
+
+
+

source

+
+ +
+

Grid

+
+
 Grid (*c, cls='grid', target_id=None, hx_vals=None, hx_target=None,
+       id=None, title=None, style=None, accesskey=None,
+       contenteditable=None, dir=None, draggable=None, enterkeyhint=None,
+       hidden=None, inert=None, inputmode=None, lang=None, popover=None,
+       spellcheck=None, tabindex=None, translate=None, hx_get=None,
+       hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
+       hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
+       hx_select=None, hx_select_oob=None, hx_indicator=None,
+       hx_push_url=None, hx_confirm=None, hx_disable=None,
+       hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
+       hx_headers=None, hx_history=None, hx_history_elt=None,
+       hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
+       hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,
+       hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+       hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+       hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+       hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+       hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,
+       hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,
+       hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+       hx_on__abort=None, hx_on__after_on_load=None,
+       hx_on__after_process_node=None, hx_on__after_request=None,
+       hx_on__after_settle=None, hx_on__after_swap=None,
+       hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+       hx_on__before_process_node=None, hx_on__before_request=None,
+       hx_on__before_swap=None, hx_on__before_send=None,
+       hx_on__before_transition=None, hx_on__config_request=None,
+       hx_on__confirm=None, hx_on__history_cache_error=None,
+       hx_on__history_cache_miss=None,
+       hx_on__history_cache_miss_error=None,
+       hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+       hx_on__before_history_save=None, hx_on__load=None,
+       hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+       hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+       hx_on__oob_error_no_target=None, hx_on__prompt=None,
+       hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+       hx_on__response_error=None, hx_on__send_abort=None,
+       hx_on__send_error=None, hx_on__sse_error=None,
+       hx_on__sse_open=None, hx_on__swap_error=None,
+       hx_on__target_error=None, hx_on__timeout=None,
+       hx_on__validation_validate=None, hx_on__validation_failed=None,
+       hx_on__validation_halted=None, hx_on__xhr_abort=None,
+       hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+       hx_on__xhr_progress=None, **kwargs)
+
+

A PicoCSS Grid, implemented as child Divs in a Div with class ‘grid’

+
+
colors = [Input(type="color", value=o) for o in ('#e66465', '#53d2c5', '#f6b73c')]
+show(Grid(*colors))
+
+
+
+
+
+
+
+
+
+
+
+
+

source

+
+
+

DialogX

+
+
 DialogX (*c, open=None, header=None, footer=None, id=None,
+          target_id=None, hx_vals=None, hx_target=None, cls=None,
+          title=None, style=None, accesskey=None, contenteditable=None,
+          dir=None, draggable=None, enterkeyhint=None, hidden=None,
+          inert=None, inputmode=None, lang=None, popover=None,
+          spellcheck=None, tabindex=None, translate=None, hx_get=None,
+          hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
+          hx_trigger=None, hx_swap=None, hx_swap_oob=None,
+          hx_include=None, hx_select=None, hx_select_oob=None,
+          hx_indicator=None, hx_push_url=None, hx_confirm=None,
+          hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,
+          hx_ext=None, hx_headers=None, hx_history=None,
+          hx_history_elt=None, hx_inherit=None, hx_params=None,
+          hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None,
+          hx_validate=None, hx_on_blur=None, hx_on_change=None,
+          hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,
+          hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,
+          hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,
+          hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,
+          hx_on_mousedown=None, hx_on_mouseenter=None,
+          hx_on_mouseleave=None, hx_on_mousemove=None,
+          hx_on_mouseout=None, hx_on_mouseover=None, hx_on_mouseup=None,
+          hx_on_wheel=None, hx_on__abort=None, hx_on__after_on_load=None,
+          hx_on__after_process_node=None, hx_on__after_request=None,
+          hx_on__after_settle=None, hx_on__after_swap=None,
+          hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+          hx_on__before_process_node=None, hx_on__before_request=None,
+          hx_on__before_swap=None, hx_on__before_send=None,
+          hx_on__before_transition=None, hx_on__config_request=None,
+          hx_on__confirm=None, hx_on__history_cache_error=None,
+          hx_on__history_cache_miss=None,
+          hx_on__history_cache_miss_error=None,
+          hx_on__history_cache_miss_load=None,
+          hx_on__history_restore=None, hx_on__before_history_save=None,
+          hx_on__load=None, hx_on__no_sse_source_error=None,
+          hx_on__on_load_error=None, hx_on__oob_after_swap=None,
+          hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,
+          hx_on__prompt=None, hx_on__pushed_into_history=None,
+          hx_on__replaced_in_history=None, hx_on__response_error=None,
+          hx_on__send_abort=None, hx_on__send_error=None,
+          hx_on__sse_error=None, hx_on__sse_open=None,
+          hx_on__swap_error=None, hx_on__target_error=None,
+          hx_on__timeout=None, hx_on__validation_validate=None,
+          hx_on__validation_failed=None, hx_on__validation_halted=None,
+          hx_on__xhr_abort=None, hx_on__xhr_loadend=None,
+          hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)
+
+

A PicoCSS Dialog, with children inside a Card

+
+
hdr = Div(Button(aria_label="Close", rel="prev"), P('confirm'))
+ftr = Div(Button('Cancel', cls="secondary"), Button('Confirm'))
+d = DialogX('thank you!', header=hdr, footer=ftr, open=None, id='dlgtest')
+# use js or htmx to display modal
+
+
+

source

+
+
+

Container

+
+
 Container (*args, target_id=None, hx_vals=None, hx_target=None, id=None,
+            cls=None, title=None, style=None, accesskey=None,
+            contenteditable=None, dir=None, draggable=None,
+            enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+            lang=None, popover=None, spellcheck=None, tabindex=None,
+            translate=None, hx_get=None, hx_post=None, hx_put=None,
+            hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
+            hx_swap_oob=None, hx_include=None, hx_select=None,
+            hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+            hx_confirm=None, hx_disable=None, hx_replace_url=None,
+            hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+            hx_history=None, hx_history_elt=None, hx_inherit=None,
+            hx_params=None, hx_preserve=None, hx_prompt=None,
+            hx_request=None, hx_sync=None, hx_validate=None,
+            hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,
+            hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,
+            hx_on_reset=None, hx_on_select=None, hx_on_submit=None,
+            hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,
+            hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,
+            hx_on_mouseenter=None, hx_on_mouseleave=None,
+            hx_on_mousemove=None, hx_on_mouseout=None,
+            hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+            hx_on__abort=None, hx_on__after_on_load=None,
+            hx_on__after_process_node=None, hx_on__after_request=None,
+            hx_on__after_settle=None, hx_on__after_swap=None,
+            hx_on__before_cleanup_element=None,
+            hx_on__before_on_load=None, hx_on__before_process_node=None,
+            hx_on__before_request=None, hx_on__before_swap=None,
+            hx_on__before_send=None, hx_on__before_transition=None,
+            hx_on__config_request=None, hx_on__confirm=None,
+            hx_on__history_cache_error=None,
+            hx_on__history_cache_miss=None,
+            hx_on__history_cache_miss_error=None,
+            hx_on__history_cache_miss_load=None,
+            hx_on__history_restore=None, hx_on__before_history_save=None,
+            hx_on__load=None, hx_on__no_sse_source_error=None,
+            hx_on__on_load_error=None, hx_on__oob_after_swap=None,
+            hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,
+            hx_on__prompt=None, hx_on__pushed_into_history=None,
+            hx_on__replaced_in_history=None, hx_on__response_error=None,
+            hx_on__send_abort=None, hx_on__send_error=None,
+            hx_on__sse_error=None, hx_on__sse_open=None,
+            hx_on__swap_error=None, hx_on__target_error=None,
+            hx_on__timeout=None, hx_on__validation_validate=None,
+            hx_on__validation_failed=None, hx_on__validation_halted=None,
+            hx_on__xhr_abort=None, hx_on__xhr_loadend=None,
+            hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)
+
+

A PicoCSS Container, implemented as a Main with class ‘container’

+
+

source

+
+
+

PicoBusy

+
+
 PicoBusy ()
+
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/pico.html.md b/docs/api/pico.html.md new file mode 100644 index 0000000000000000000000000000000000000000..35283a0bb79c6398974fa8d32ea4b23ca1f22827 --- /dev/null +++ b/docs/api/pico.html.md @@ -0,0 +1,430 @@ +# Pico.css components + + + + +`picocondlink` is the class-conditional css `link` tag, and `picolink` +is the regular tag. + +``` python +show(picocondlink) +``` + + + + +------------------------------------------------------------------------ + +source + +### set_pico_cls + +> set_pico_cls () + +Run this to make jupyter outputs styled with pico: + +``` python +set_pico_cls() +``` + + + +------------------------------------------------------------------------ + +source + +### Card + +> Card (*c, header=None, footer=None, target_id=None, hx_vals=None, +> hx_target=None, id=None, cls=None, title=None, style=None, +> accesskey=None, contenteditable=None, dir=None, draggable=None, +> enterkeyhint=None, hidden=None, inert=None, inputmode=None, +> lang=None, popover=None, spellcheck=None, tabindex=None, +> translate=None, hx_get=None, hx_post=None, hx_put=None, +> hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None, +> hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, +> hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None, +> hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None, +> hx_on_invalid=None, hx_on_reset=None, hx_on_select=None, +> hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None, +> hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None, +> hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +*A PicoCSS Card, implemented as an Article with optional Header and +Footer* + +``` python +show(Card('body', header=P('head'), footer=P('foot'))) +``` + +
+

head

+
+body +

foot

+
+
+ +------------------------------------------------------------------------ + +source + +### Group + +> Group (*c, target_id=None, hx_vals=None, hx_target=None, id=None, +> cls=None, title=None, style=None, accesskey=None, +> contenteditable=None, dir=None, draggable=None, enterkeyhint=None, +> hidden=None, inert=None, inputmode=None, lang=None, popover=None, +> spellcheck=None, tabindex=None, translate=None, hx_get=None, +> hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, +> hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None, +> hx_select=None, hx_select_oob=None, hx_indicator=None, +> hx_push_url=None, hx_confirm=None, hx_disable=None, +> hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, +> hx_headers=None, hx_history=None, hx_history_elt=None, +> hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +*A PicoCSS Group, implemented as a Fieldset with role ‘group’* + +``` python +show(Group(Input(), Button("Save"))) +``` + +
+ + +
+ +------------------------------------------------------------------------ + +source + +### Search + +> Search (*c, target_id=None, hx_vals=None, hx_target=None, id=None, +> cls=None, title=None, style=None, accesskey=None, +> contenteditable=None, dir=None, draggable=None, +> enterkeyhint=None, hidden=None, inert=None, inputmode=None, +> lang=None, popover=None, spellcheck=None, tabindex=None, +> translate=None, hx_get=None, hx_post=None, hx_put=None, +> hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None, +> hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +*A PicoCSS Search, implemented as a Form with role ‘search’* + +``` python +show(Search(Input(type="search"), Button("Search"))) +``` + +
+ + +
+ +------------------------------------------------------------------------ + +source + +### Grid + +> Grid (*c, cls='grid', target_id=None, hx_vals=None, hx_target=None, +> id=None, title=None, style=None, accesskey=None, +> contenteditable=None, dir=None, draggable=None, enterkeyhint=None, +> hidden=None, inert=None, inputmode=None, lang=None, popover=None, +> spellcheck=None, tabindex=None, translate=None, hx_get=None, +> hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, +> hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None, +> hx_select=None, hx_select_oob=None, hx_indicator=None, +> hx_push_url=None, hx_confirm=None, hx_disable=None, +> hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, +> hx_headers=None, hx_history=None, hx_history_elt=None, +> hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +*A PicoCSS Grid, implemented as child Divs in a Div with class ‘grid’* + +``` python +colors = [Input(type="color", value=o) for o in ('#e66465', '#53d2c5', '#f6b73c')] +show(Grid(*colors)) +``` + +
+
+
+
+
+
+
+
+ +------------------------------------------------------------------------ + +source + +### DialogX + +> DialogX (*c, open=None, header=None, footer=None, id=None, +> target_id=None, hx_vals=None, hx_target=None, cls=None, +> title=None, style=None, accesskey=None, contenteditable=None, +> dir=None, draggable=None, enterkeyhint=None, hidden=None, +> inert=None, inputmode=None, lang=None, popover=None, +> spellcheck=None, tabindex=None, translate=None, hx_get=None, +> hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, +> hx_trigger=None, hx_swap=None, hx_swap_oob=None, +> hx_include=None, hx_select=None, hx_select_oob=None, +> hx_indicator=None, hx_push_url=None, hx_confirm=None, +> hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, +> hx_ext=None, hx_headers=None, hx_history=None, +> hx_history_elt=None, hx_inherit=None, hx_params=None, +> hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None, +> hx_validate=None, hx_on_blur=None, hx_on_change=None, +> hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None, +> hx_on_invalid=None, hx_on_reset=None, hx_on_select=None, +> hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None, +> hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None, +> hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, +> hx_on_mouseout=None, hx_on_mouseover=None, hx_on_mouseup=None, +> hx_on_wheel=None, hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, +> hx_on__history_restore=None, hx_on__before_history_save=None, +> hx_on__load=None, hx_on__no_sse_source_error=None, +> hx_on__on_load_error=None, hx_on__oob_after_swap=None, +> hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None, +> hx_on__prompt=None, hx_on__pushed_into_history=None, +> hx_on__replaced_in_history=None, hx_on__response_error=None, +> hx_on__send_abort=None, hx_on__send_error=None, +> hx_on__sse_error=None, hx_on__sse_open=None, +> hx_on__swap_error=None, hx_on__target_error=None, +> hx_on__timeout=None, hx_on__validation_validate=None, +> hx_on__validation_failed=None, hx_on__validation_halted=None, +> hx_on__xhr_abort=None, hx_on__xhr_loadend=None, +> hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs) + +*A PicoCSS Dialog, with children inside a Card* + +``` python +hdr = Div(Button(aria_label="Close", rel="prev"), P('confirm')) +ftr = Div(Button('Cancel', cls="secondary"), Button('Confirm')) +d = DialogX('thank you!', header=hdr, footer=ftr, open=None, id='dlgtest') +# use js or htmx to display modal +``` + +------------------------------------------------------------------------ + +source + +### Container + +> Container (*args, target_id=None, hx_vals=None, hx_target=None, id=None, +> cls=None, title=None, style=None, accesskey=None, +> contenteditable=None, dir=None, draggable=None, +> enterkeyhint=None, hidden=None, inert=None, inputmode=None, +> lang=None, popover=None, spellcheck=None, tabindex=None, +> translate=None, hx_get=None, hx_post=None, hx_put=None, +> hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None, +> hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, +> hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None, +> hx_on_focus=None, hx_on_input=None, hx_on_invalid=None, +> hx_on_reset=None, hx_on_select=None, hx_on_submit=None, +> hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None, +> hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None, +> hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, +> hx_on__history_restore=None, hx_on__before_history_save=None, +> hx_on__load=None, hx_on__no_sse_source_error=None, +> hx_on__on_load_error=None, hx_on__oob_after_swap=None, +> hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None, +> hx_on__prompt=None, hx_on__pushed_into_history=None, +> hx_on__replaced_in_history=None, hx_on__response_error=None, +> hx_on__send_abort=None, hx_on__send_error=None, +> hx_on__sse_error=None, hx_on__sse_open=None, +> hx_on__swap_error=None, hx_on__target_error=None, +> hx_on__timeout=None, hx_on__validation_validate=None, +> hx_on__validation_failed=None, hx_on__validation_halted=None, +> hx_on__xhr_abort=None, hx_on__xhr_loadend=None, +> hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs) + +*A PicoCSS Container, implemented as a Main with class ‘container’* + +------------------------------------------------------------------------ + +source + +### PicoBusy + +> PicoBusy () diff --git a/docs/api/svg.html b/docs/api/svg.html new file mode 100644 index 0000000000000000000000000000000000000000..383fb1285419bd2069db7aa82446a24af4e58878 --- /dev/null +++ b/docs/api/svg.html @@ -0,0 +1,1694 @@ + + + + + + + + + + +SVG – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

SVG

+
+ +
+
+ Simple SVG FT elements +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from nbdev.showdoc import show_doc
+
+

You can create SVGs directly from strings, for instance (as always, use NotStr or Safe to tell FastHTML to not escape the text):

+
+
svg = '<svg width="50" height="50"><circle cx="20" cy="20" r="15" fill="red"></circle></svg>'
+show(NotStr(svg))
+
+ +
+
+

You can also use libraries such as fa6-icons.

+

To create and modify SVGs using a Python API, use the FT elements in fasthtml.svg, discussed below.

+

Note: fasthtml.common does NOT automatically export SVG elements. To get access to them, you need to import fasthtml.svg like so

+
from fasthtml.svg import *
+
+

source

+
+

Svg

+
+
 Svg (*args, viewBox=None, h=None, w=None, height=None, width=None,
+      xmlns='http://www.w3.org/2000/svg', **kwargs)
+
+

An SVG tag; xmlns is added automatically, and viewBox defaults to height and width if not provided

+

To create your own SVGs, use SVG. It will automatically set the viewBox from height and width if not provided.

+

All of our shapes will have some convenient kwargs added by using ft_svg:

+
+

source

+
+
+

ft_svg

+
+
 ft_svg (tag:str, *c, transform=None, opacity=None, clip=None, mask=None,
+         filter=None, vector_effect=None, pointer_events=None,
+         target_id=None, hx_vals=None, hx_target=None, id=None, cls=None,
+         title=None, style=None, accesskey=None, contenteditable=None,
+         dir=None, draggable=None, enterkeyhint=None, hidden=None,
+         inert=None, inputmode=None, lang=None, popover=None,
+         spellcheck=None, tabindex=None, translate=None, hx_get=None,
+         hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
+         hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
+         hx_select=None, hx_select_oob=None, hx_indicator=None,
+         hx_push_url=None, hx_confirm=None, hx_disable=None,
+         hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
+         hx_headers=None, hx_history=None, hx_history_elt=None,
+         hx_inherit=None, hx_params=None, hx_preserve=None,
+         hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None,
+         hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,
+         hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,
+         hx_on_reset=None, hx_on_select=None, hx_on_submit=None,
+         hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,
+         hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,
+         hx_on_mouseenter=None, hx_on_mouseleave=None,
+         hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+         hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+         hx_on__after_on_load=None, hx_on__after_process_node=None,
+         hx_on__after_request=None, hx_on__after_settle=None,
+         hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+         hx_on__before_on_load=None, hx_on__before_process_node=None,
+         hx_on__before_request=None, hx_on__before_swap=None,
+         hx_on__before_send=None, hx_on__before_transition=None,
+         hx_on__config_request=None, hx_on__confirm=None,
+         hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+         hx_on__history_cache_miss_error=None,
+         hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+         hx_on__before_history_save=None, hx_on__load=None,
+         hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+         hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+         hx_on__oob_error_no_target=None, hx_on__prompt=None,
+         hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+         hx_on__response_error=None, hx_on__send_abort=None,
+         hx_on__send_error=None, hx_on__sse_error=None,
+         hx_on__sse_open=None, hx_on__swap_error=None,
+         hx_on__target_error=None, hx_on__timeout=None,
+         hx_on__validation_validate=None, hx_on__validation_failed=None,
+         hx_on__validation_halted=None, hx_on__xhr_abort=None,
+         hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+         hx_on__xhr_progress=None)
+
+

Create a standard FT element with some SVG-specific attrs

+
+
+

Basic shapes

+

We’ll define a simple function to display SVG shapes in this notebook:

+
+
def demo(el, h=50, w=50): return show(Svg(h=h,w=w)(el))
+
+
+

source

+
+

Rect

+
+
 Rect (width, height, x=0, y=0, fill=None, stroke=None, stroke_width=None,
+       rx=None, ry=None, transform=None, opacity=None, clip=None,
+       mask=None, filter=None, vector_effect=None, pointer_events=None,
+       target_id=None, hx_vals=None, hx_target=None, id=None, cls=None,
+       title=None, style=None, accesskey=None, contenteditable=None,
+       dir=None, draggable=None, enterkeyhint=None, hidden=None,
+       inert=None, inputmode=None, lang=None, popover=None,
+       spellcheck=None, tabindex=None, translate=None, hx_get=None,
+       hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
+       hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
+       hx_select=None, hx_select_oob=None, hx_indicator=None,
+       hx_push_url=None, hx_confirm=None, hx_disable=None,
+       hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
+       hx_headers=None, hx_history=None, hx_history_elt=None,
+       hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
+       hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,
+       hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+       hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+       hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+       hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+       hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,
+       hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,
+       hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+       hx_on__abort=None, hx_on__after_on_load=None,
+       hx_on__after_process_node=None, hx_on__after_request=None,
+       hx_on__after_settle=None, hx_on__after_swap=None,
+       hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+       hx_on__before_process_node=None, hx_on__before_request=None,
+       hx_on__before_swap=None, hx_on__before_send=None,
+       hx_on__before_transition=None, hx_on__config_request=None,
+       hx_on__confirm=None, hx_on__history_cache_error=None,
+       hx_on__history_cache_miss=None,
+       hx_on__history_cache_miss_error=None,
+       hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+       hx_on__before_history_save=None, hx_on__load=None,
+       hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+       hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+       hx_on__oob_error_no_target=None, hx_on__prompt=None,
+       hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+       hx_on__response_error=None, hx_on__send_abort=None,
+       hx_on__send_error=None, hx_on__sse_error=None,
+       hx_on__sse_open=None, hx_on__swap_error=None,
+       hx_on__target_error=None, hx_on__timeout=None,
+       hx_on__validation_validate=None, hx_on__validation_failed=None,
+       hx_on__validation_halted=None, hx_on__xhr_abort=None,
+       hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+       hx_on__xhr_progress=None)
+
+

A standard SVG rect element

+

All our shapes just create regular FT elements. The only extra functionality provided by most of them is to add additional defined kwargs to improve auto-complete in IDEs and notebooks, and re-order parameters so that positional args can also be used to save a bit of typing, e.g:

+
+
demo(Rect(30, 30, fill='blue', rx=8, ry=8))
+
+ +
+
+
+

source

+
+
+

Circle

+
+
 Circle (r, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,
+         transform=None, opacity=None, clip=None, mask=None, filter=None,
+         vector_effect=None, pointer_events=None, target_id=None,
+         hx_vals=None, hx_target=None, id=None, cls=None, title=None,
+         style=None, accesskey=None, contenteditable=None, dir=None,
+         draggable=None, enterkeyhint=None, hidden=None, inert=None,
+         inputmode=None, lang=None, popover=None, spellcheck=None,
+         tabindex=None, translate=None, hx_get=None, hx_post=None,
+         hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+         hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+         hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+         hx_confirm=None, hx_disable=None, hx_replace_url=None,
+         hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+         hx_history=None, hx_history_elt=None, hx_inherit=None,
+         hx_params=None, hx_preserve=None, hx_prompt=None,
+         hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,
+         hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+         hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+         hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+         hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+         hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,
+         hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,
+         hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+         hx_on__abort=None, hx_on__after_on_load=None,
+         hx_on__after_process_node=None, hx_on__after_request=None,
+         hx_on__after_settle=None, hx_on__after_swap=None,
+         hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+         hx_on__before_process_node=None, hx_on__before_request=None,
+         hx_on__before_swap=None, hx_on__before_send=None,
+         hx_on__before_transition=None, hx_on__config_request=None,
+         hx_on__confirm=None, hx_on__history_cache_error=None,
+         hx_on__history_cache_miss=None,
+         hx_on__history_cache_miss_error=None,
+         hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+         hx_on__before_history_save=None, hx_on__load=None,
+         hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+         hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+         hx_on__oob_error_no_target=None, hx_on__prompt=None,
+         hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+         hx_on__response_error=None, hx_on__send_abort=None,
+         hx_on__send_error=None, hx_on__sse_error=None,
+         hx_on__sse_open=None, hx_on__swap_error=None,
+         hx_on__target_error=None, hx_on__timeout=None,
+         hx_on__validation_validate=None, hx_on__validation_failed=None,
+         hx_on__validation_halted=None, hx_on__xhr_abort=None,
+         hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+         hx_on__xhr_progress=None)
+
+

A standard SVG circle element

+
+
demo(Circle(20, 25, 25, stroke='red', stroke_width=3))
+
+ +
+
+
+

source

+
+
+

Ellipse

+
+
 Ellipse (rx, ry, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,
+          transform=None, opacity=None, clip=None, mask=None, filter=None,
+          vector_effect=None, pointer_events=None, target_id=None,
+          hx_vals=None, hx_target=None, id=None, cls=None, title=None,
+          style=None, accesskey=None, contenteditable=None, dir=None,
+          draggable=None, enterkeyhint=None, hidden=None, inert=None,
+          inputmode=None, lang=None, popover=None, spellcheck=None,
+          tabindex=None, translate=None, hx_get=None, hx_post=None,
+          hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+          hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+          hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+          hx_confirm=None, hx_disable=None, hx_replace_url=None,
+          hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+          hx_history=None, hx_history_elt=None, hx_inherit=None,
+          hx_params=None, hx_preserve=None, hx_prompt=None,
+          hx_request=None, hx_sync=None, hx_validate=None,
+          hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,
+          hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,
+          hx_on_reset=None, hx_on_select=None, hx_on_submit=None,
+          hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,
+          hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,
+          hx_on_mouseenter=None, hx_on_mouseleave=None,
+          hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+          hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+          hx_on__after_on_load=None, hx_on__after_process_node=None,
+          hx_on__after_request=None, hx_on__after_settle=None,
+          hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+          hx_on__before_on_load=None, hx_on__before_process_node=None,
+          hx_on__before_request=None, hx_on__before_swap=None,
+          hx_on__before_send=None, hx_on__before_transition=None,
+          hx_on__config_request=None, hx_on__confirm=None,
+          hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+          hx_on__history_cache_miss_error=None,
+          hx_on__history_cache_miss_load=None,
+          hx_on__history_restore=None, hx_on__before_history_save=None,
+          hx_on__load=None, hx_on__no_sse_source_error=None,
+          hx_on__on_load_error=None, hx_on__oob_after_swap=None,
+          hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,
+          hx_on__prompt=None, hx_on__pushed_into_history=None,
+          hx_on__replaced_in_history=None, hx_on__response_error=None,
+          hx_on__send_abort=None, hx_on__send_error=None,
+          hx_on__sse_error=None, hx_on__sse_open=None,
+          hx_on__swap_error=None, hx_on__target_error=None,
+          hx_on__timeout=None, hx_on__validation_validate=None,
+          hx_on__validation_failed=None, hx_on__validation_halted=None,
+          hx_on__xhr_abort=None, hx_on__xhr_loadend=None,
+          hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)
+
+

A standard SVG ellipse element

+
+
demo(Ellipse(20, 10, 25, 25))
+
+ +
+
+
+

source

+
+
+

transformd

+
+
 transformd (translate=None, scale=None, rotate=None, skewX=None,
+             skewY=None, matrix=None)
+
+

Create an SVG transform kwarg dict

+
+
rot = transformd(rotate=(45, 25, 25))
+rot
+
+
{'transform': 'rotate(45,25,25)'}
+
+
+
+
demo(Ellipse(20, 10, 25, 25, **rot))
+
+ +
+
+
+

source

+
+
+

Line

+
+
 Line (x1, y1, x2=0, y2=0, stroke='black', w=None, stroke_width=1,
+       transform=None, opacity=None, clip=None, mask=None, filter=None,
+       vector_effect=None, pointer_events=None, target_id=None,
+       hx_vals=None, hx_target=None, id=None, cls=None, title=None,
+       style=None, accesskey=None, contenteditable=None, dir=None,
+       draggable=None, enterkeyhint=None, hidden=None, inert=None,
+       inputmode=None, lang=None, popover=None, spellcheck=None,
+       tabindex=None, translate=None, hx_get=None, hx_post=None,
+       hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+       hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+       hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+       hx_confirm=None, hx_disable=None, hx_replace_url=None,
+       hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+       hx_history=None, hx_history_elt=None, hx_inherit=None,
+       hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
+       hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,
+       hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,
+       hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,
+       hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,
+       hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,
+       hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,
+       hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+       hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+       hx_on__after_on_load=None, hx_on__after_process_node=None,
+       hx_on__after_request=None, hx_on__after_settle=None,
+       hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+       hx_on__before_on_load=None, hx_on__before_process_node=None,
+       hx_on__before_request=None, hx_on__before_swap=None,
+       hx_on__before_send=None, hx_on__before_transition=None,
+       hx_on__config_request=None, hx_on__confirm=None,
+       hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+       hx_on__history_cache_miss_error=None,
+       hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+       hx_on__before_history_save=None, hx_on__load=None,
+       hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+       hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+       hx_on__oob_error_no_target=None, hx_on__prompt=None,
+       hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+       hx_on__response_error=None, hx_on__send_abort=None,
+       hx_on__send_error=None, hx_on__sse_error=None,
+       hx_on__sse_open=None, hx_on__swap_error=None,
+       hx_on__target_error=None, hx_on__timeout=None,
+       hx_on__validation_validate=None, hx_on__validation_failed=None,
+       hx_on__validation_halted=None, hx_on__xhr_abort=None,
+       hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+       hx_on__xhr_progress=None)
+
+

A standard SVG line element

+
+
demo(Line(20, 30, w=3))
+
+ +
+
+
+

source

+
+
+

Polyline

+
+
 Polyline (*args, points=None, fill=None, stroke=None, stroke_width=None,
+           transform=None, opacity=None, clip=None, mask=None,
+           filter=None, vector_effect=None, pointer_events=None,
+           target_id=None, hx_vals=None, hx_target=None, id=None,
+           cls=None, title=None, style=None, accesskey=None,
+           contenteditable=None, dir=None, draggable=None,
+           enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+           lang=None, popover=None, spellcheck=None, tabindex=None,
+           translate=None, hx_get=None, hx_post=None, hx_put=None,
+           hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
+           hx_swap_oob=None, hx_include=None, hx_select=None,
+           hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+           hx_confirm=None, hx_disable=None, hx_replace_url=None,
+           hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+           hx_history=None, hx_history_elt=None, hx_inherit=None,
+           hx_params=None, hx_preserve=None, hx_prompt=None,
+           hx_request=None, hx_sync=None, hx_validate=None,
+           hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,
+           hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,
+           hx_on_reset=None, hx_on_select=None, hx_on_submit=None,
+           hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,
+           hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,
+           hx_on_mouseenter=None, hx_on_mouseleave=None,
+           hx_on_mousemove=None, hx_on_mouseout=None,
+           hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+           hx_on__abort=None, hx_on__after_on_load=None,
+           hx_on__after_process_node=None, hx_on__after_request=None,
+           hx_on__after_settle=None, hx_on__after_swap=None,
+           hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+           hx_on__before_process_node=None, hx_on__before_request=None,
+           hx_on__before_swap=None, hx_on__before_send=None,
+           hx_on__before_transition=None, hx_on__config_request=None,
+           hx_on__confirm=None, hx_on__history_cache_error=None,
+           hx_on__history_cache_miss=None,
+           hx_on__history_cache_miss_error=None,
+           hx_on__history_cache_miss_load=None,
+           hx_on__history_restore=None, hx_on__before_history_save=None,
+           hx_on__load=None, hx_on__no_sse_source_error=None,
+           hx_on__on_load_error=None, hx_on__oob_after_swap=None,
+           hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,
+           hx_on__prompt=None, hx_on__pushed_into_history=None,
+           hx_on__replaced_in_history=None, hx_on__response_error=None,
+           hx_on__send_abort=None, hx_on__send_error=None,
+           hx_on__sse_error=None, hx_on__sse_open=None,
+           hx_on__swap_error=None, hx_on__target_error=None,
+           hx_on__timeout=None, hx_on__validation_validate=None,
+           hx_on__validation_failed=None, hx_on__validation_halted=None,
+           hx_on__xhr_abort=None, hx_on__xhr_loadend=None,
+           hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)
+
+

A standard SVG polyline element

+
+
demo(Polyline((0,0), (10,10), (20,0), (30,10), (40,0),
+              fill='yellow', stroke='blue', stroke_width=2))
+
+ +
+
+
+
demo(Polyline(points='0,0 10,10 20,0 30,10 40,0', fill='purple', stroke_width=2))
+
+ +
+
+
+

source

+
+
+

Polygon

+
+
 Polygon (*args, points=None, fill=None, stroke=None, stroke_width=None,
+          transform=None, opacity=None, clip=None, mask=None, filter=None,
+          vector_effect=None, pointer_events=None, target_id=None,
+          hx_vals=None, hx_target=None, id=None, cls=None, title=None,
+          style=None, accesskey=None, contenteditable=None, dir=None,
+          draggable=None, enterkeyhint=None, hidden=None, inert=None,
+          inputmode=None, lang=None, popover=None, spellcheck=None,
+          tabindex=None, translate=None, hx_get=None, hx_post=None,
+          hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+          hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+          hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+          hx_confirm=None, hx_disable=None, hx_replace_url=None,
+          hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+          hx_history=None, hx_history_elt=None, hx_inherit=None,
+          hx_params=None, hx_preserve=None, hx_prompt=None,
+          hx_request=None, hx_sync=None, hx_validate=None,
+          hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,
+          hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,
+          hx_on_reset=None, hx_on_select=None, hx_on_submit=None,
+          hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,
+          hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,
+          hx_on_mouseenter=None, hx_on_mouseleave=None,
+          hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+          hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+          hx_on__after_on_load=None, hx_on__after_process_node=None,
+          hx_on__after_request=None, hx_on__after_settle=None,
+          hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+          hx_on__before_on_load=None, hx_on__before_process_node=None,
+          hx_on__before_request=None, hx_on__before_swap=None,
+          hx_on__before_send=None, hx_on__before_transition=None,
+          hx_on__config_request=None, hx_on__confirm=None,
+          hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+          hx_on__history_cache_miss_error=None,
+          hx_on__history_cache_miss_load=None,
+          hx_on__history_restore=None, hx_on__before_history_save=None,
+          hx_on__load=None, hx_on__no_sse_source_error=None,
+          hx_on__on_load_error=None, hx_on__oob_after_swap=None,
+          hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,
+          hx_on__prompt=None, hx_on__pushed_into_history=None,
+          hx_on__replaced_in_history=None, hx_on__response_error=None,
+          hx_on__send_abort=None, hx_on__send_error=None,
+          hx_on__sse_error=None, hx_on__sse_open=None,
+          hx_on__swap_error=None, hx_on__target_error=None,
+          hx_on__timeout=None, hx_on__validation_validate=None,
+          hx_on__validation_failed=None, hx_on__validation_halted=None,
+          hx_on__xhr_abort=None, hx_on__xhr_loadend=None,
+          hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)
+
+

A standard SVG polygon element

+
+
demo(Polygon((25,5), (43.3,15), (43.3,35), (25,45), (6.7,35), (6.7,15), 
+             fill='lightblue', stroke='navy', stroke_width=2))
+
+ +
+
+
+
demo(Polygon(points='25,5 43.3,15 43.3,35 25,45 6.7,35 6.7,15',
+             fill='lightgreen', stroke='darkgreen', stroke_width=2))
+
+ +
+
+
+

source

+
+
+

Text

+
+
 Text (*args, x=0, y=0, font_family=None, font_size=None, fill=None,
+       text_anchor=None, dominant_baseline=None, font_weight=None,
+       font_style=None, text_decoration=None, transform=None,
+       opacity=None, clip=None, mask=None, filter=None,
+       vector_effect=None, pointer_events=None, target_id=None,
+       hx_vals=None, hx_target=None, id=None, cls=None, title=None,
+       style=None, accesskey=None, contenteditable=None, dir=None,
+       draggable=None, enterkeyhint=None, hidden=None, inert=None,
+       inputmode=None, lang=None, popover=None, spellcheck=None,
+       tabindex=None, translate=None, hx_get=None, hx_post=None,
+       hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+       hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+       hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+       hx_confirm=None, hx_disable=None, hx_replace_url=None,
+       hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+       hx_history=None, hx_history_elt=None, hx_inherit=None,
+       hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
+       hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,
+       hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,
+       hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,
+       hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,
+       hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,
+       hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,
+       hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+       hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+       hx_on__after_on_load=None, hx_on__after_process_node=None,
+       hx_on__after_request=None, hx_on__after_settle=None,
+       hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+       hx_on__before_on_load=None, hx_on__before_process_node=None,
+       hx_on__before_request=None, hx_on__before_swap=None,
+       hx_on__before_send=None, hx_on__before_transition=None,
+       hx_on__config_request=None, hx_on__confirm=None,
+       hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+       hx_on__history_cache_miss_error=None,
+       hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+       hx_on__before_history_save=None, hx_on__load=None,
+       hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+       hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+       hx_on__oob_error_no_target=None, hx_on__prompt=None,
+       hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+       hx_on__response_error=None, hx_on__send_abort=None,
+       hx_on__send_error=None, hx_on__sse_error=None,
+       hx_on__sse_open=None, hx_on__swap_error=None,
+       hx_on__target_error=None, hx_on__timeout=None,
+       hx_on__validation_validate=None, hx_on__validation_failed=None,
+       hx_on__validation_halted=None, hx_on__xhr_abort=None,
+       hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+       hx_on__xhr_progress=None)
+
+

A standard SVG text element

+
+
demo(Text("Hello!", x=10, y=30))
+
+Hello! +
+
+
+
+
+

Paths

+

Paths in SVGs are more complex, so we add a small (optional) fluent interface for constructing them:

+
+

source

+
+

PathFT

+
+
 PathFT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs)
+
+

A ‘Fast Tag’ structure, containing tag,children,and attrs

+
+

source

+
+
+

Path

+
+
 Path (d='', fill=None, stroke=None, stroke_width=None, transform=None,
+       opacity=None, clip=None, mask=None, filter=None,
+       vector_effect=None, pointer_events=None, target_id=None,
+       hx_vals=None, hx_target=None, id=None, cls=None, title=None,
+       style=None, accesskey=None, contenteditable=None, dir=None,
+       draggable=None, enterkeyhint=None, hidden=None, inert=None,
+       inputmode=None, lang=None, popover=None, spellcheck=None,
+       tabindex=None, translate=None, hx_get=None, hx_post=None,
+       hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+       hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+       hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+       hx_confirm=None, hx_disable=None, hx_replace_url=None,
+       hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+       hx_history=None, hx_history_elt=None, hx_inherit=None,
+       hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
+       hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,
+       hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,
+       hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,
+       hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,
+       hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,
+       hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,
+       hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+       hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+       hx_on__after_on_load=None, hx_on__after_process_node=None,
+       hx_on__after_request=None, hx_on__after_settle=None,
+       hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+       hx_on__before_on_load=None, hx_on__before_process_node=None,
+       hx_on__before_request=None, hx_on__before_swap=None,
+       hx_on__before_send=None, hx_on__before_transition=None,
+       hx_on__config_request=None, hx_on__confirm=None,
+       hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+       hx_on__history_cache_miss_error=None,
+       hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+       hx_on__before_history_save=None, hx_on__load=None,
+       hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+       hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+       hx_on__oob_error_no_target=None, hx_on__prompt=None,
+       hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+       hx_on__response_error=None, hx_on__send_abort=None,
+       hx_on__send_error=None, hx_on__sse_error=None,
+       hx_on__sse_open=None, hx_on__swap_error=None,
+       hx_on__target_error=None, hx_on__timeout=None,
+       hx_on__validation_validate=None, hx_on__validation_failed=None,
+       hx_on__validation_halted=None, hx_on__xhr_abort=None,
+       hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+       hx_on__xhr_progress=None)
+
+

Create a standard path SVG element. This is a special object

+

Let’s create a square shape, but using Path instead of Rect:

+
    +
  • M(10, 10): Move to starting point (10, 10)
  • +
  • L(40, 10): Line to (40, 10) - top edge
  • +
  • L(40, 40): Line to (40, 40) - right edge
  • +
  • L(10, 40): Line to (10, 40) - bottom edge
  • +
  • Z(): Close path - connects back to start
  • +
+

M = Move to, L = Line to, Z = Close path

+
+
demo(Path(fill='none', stroke='purple', stroke_width=2
+         ).M(10, 10).L(40, 10).L(40, 40).L(10, 40).Z())
+
+ +
+
+

Using curves we can create a spiral:

+
+
p = (Path(fill='none', stroke='purple', stroke_width=2)
+     .M(25, 25)
+     .C(25, 25, 20, 20, 30, 20)
+     .C(40, 20, 40, 30, 30, 30)
+     .C(20, 30, 20, 15, 35, 15)
+     .C(50, 15, 50, 35, 25, 35)
+     .C(0, 35, 0, 10, 40, 10)
+     .C(80, 10, 80, 40, 25, 40))
+demo(p, 50, 100)
+
+ +
+
+

Using arcs and curves we can create a map marker icon:

+
+
p = (Path(fill='red')
+     .M(25,45)
+     .C(25,45,10,35,10,25)
+     .A(15,15,0,1,1,40,25)
+     .C(40,35,25,45,25,45)
+     .Z())
+demo(p)
+
+ +
+
+

Behind the scenes it’s just creating regular SVG path d attr – you can pass d in directly if you prefer.

+
+
print(p.d)
+
+
 M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z
+
+
+
+
demo(Path(d='M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z'))
+
+ +
+
+
+

source

+
+
+

PathFT.M

+
+
 PathFT.M (x, y)
+
+

Move to.

+
+

source

+
+
+

PathFT.L

+
+
 PathFT.L (x, y)
+
+

Line to.

+
+

source

+
+
+

PathFT.H

+
+
 PathFT.H (x)
+
+

Horizontal line to.

+
+

source

+
+
+

PathFT.V

+
+
 PathFT.V (y)
+
+

Vertical line to.

+
+

source

+
+
+

PathFT.Z

+
+
 PathFT.Z ()
+
+

Close path.

+
+

source

+
+
+

PathFT.C

+
+
 PathFT.C (x1, y1, x2, y2, x, y)
+
+

Cubic Bézier curve.

+
+

source

+
+
+

PathFT.S

+
+
 PathFT.S (x2, y2, x, y)
+
+

Smooth cubic Bézier curve.

+
+

source

+
+
+

PathFT.Q

+
+
 PathFT.Q (x1, y1, x, y)
+
+

Quadratic Bézier curve.

+
+

source

+
+
+

PathFT.T

+
+
 PathFT.T (x, y)
+
+

Smooth quadratic Bézier curve.

+
+

source

+
+
+

PathFT.A

+
+
 PathFT.A (rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y)
+
+

Elliptical Arc.

+
+
+
+

HTMX helpers

+
+

source

+
+

SvgOob

+
+
 SvgOob (*args, **kwargs)
+
+

Wraps an SVG shape as required for an HTMX OOB swap

+

When returning an SVG shape out-of-band (OOB) in HTMX, you need to wrap it with SvgOob to have it appear correctly. (SvgOob is just a shortcut for Template(Svg(...)), which is the trick that makes SVG OOB swaps work.)

+
+

source

+
+
+

SvgInb

+
+
 SvgInb (*args, **kwargs)
+
+

Wraps an SVG shape as required for an HTMX inband swap

+

When returning an SVG shape in-band in HTMX, either have the calling element include hx_select='svg>*', or **svg_inb (which are two ways of saying the same thing), or wrap the response with SvgInb to have it appear correctly. (SvgInb is just a shortcut for the tuple (Svg(...), HtmxResponseHeaders(hx_reselect='svg>*')), which is the trick that makes SVG in-band swaps work.)

+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/svg.html.md b/docs/api/svg.html.md new file mode 100644 index 0000000000000000000000000000000000000000..5989e329645ecf46d4bb6dfd181809a0f488c38a --- /dev/null +++ b/docs/api/svg.html.md @@ -0,0 +1,914 @@ +# SVG + + + + +``` python +from nbdev.showdoc import show_doc +``` + +You can create SVGs directly from strings, for instance (as always, use +`NotStr` or `Safe` to tell FastHTML to not escape the text): + +``` python +svg = '' +show(NotStr(svg)) +``` + + + +You can also use libraries such as +[fa6-icons](https://www.fastht.ml/docs/fa6-icons/). + +To create and modify SVGs using a Python API, use the FT elements in +`fasthtml.svg`, discussed below. + +**Note**: `fasthtml.common` does NOT automatically export SVG elements. +To get access to them, you need to import `fasthtml.svg` like so + +``` python +from fasthtml.svg import * +``` + +------------------------------------------------------------------------ + +source + +### Svg + +> Svg (*args, viewBox=None, h=None, w=None, height=None, width=None, +> xmlns='http://www.w3.org/2000/svg', **kwargs) + +*An SVG tag; xmlns is added automatically, and viewBox defaults to +height and width if not provided* + +To create your own SVGs, use `SVG`. It will automatically set the +`viewBox` from height and width if not provided. + +All of our shapes will have some convenient kwargs added by using +[`ft_svg`](https://www.fastht.ml/docs/api/svg.html#ft_svg): + +------------------------------------------------------------------------ + +source + +### ft_svg + +> ft_svg (tag:str, *c, transform=None, opacity=None, clip=None, mask=None, +> filter=None, vector_effect=None, pointer_events=None, +> target_id=None, hx_vals=None, hx_target=None, id=None, cls=None, +> title=None, style=None, accesskey=None, contenteditable=None, +> dir=None, draggable=None, enterkeyhint=None, hidden=None, +> inert=None, inputmode=None, lang=None, popover=None, +> spellcheck=None, tabindex=None, translate=None, hx_get=None, +> hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, +> hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None, +> hx_select=None, hx_select_oob=None, hx_indicator=None, +> hx_push_url=None, hx_confirm=None, hx_disable=None, +> hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, +> hx_headers=None, hx_history=None, hx_history_elt=None, +> hx_inherit=None, hx_params=None, hx_preserve=None, +> hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None, +> hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None, +> hx_on_focus=None, hx_on_input=None, hx_on_invalid=None, +> hx_on_reset=None, hx_on_select=None, hx_on_submit=None, +> hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None, +> hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None, +> hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None) + +*Create a standard `FT` element with some SVG-specific attrs* + +## Basic shapes + +We’ll define a simple function to display SVG shapes in this notebook: + +``` python +def demo(el, h=50, w=50): return show(Svg(h=h,w=w)(el)) +``` + +------------------------------------------------------------------------ + +source + +### Rect + +> Rect (width, height, x=0, y=0, fill=None, stroke=None, stroke_width=None, +> rx=None, ry=None, transform=None, opacity=None, clip=None, +> mask=None, filter=None, vector_effect=None, pointer_events=None, +> target_id=None, hx_vals=None, hx_target=None, id=None, cls=None, +> title=None, style=None, accesskey=None, contenteditable=None, +> dir=None, draggable=None, enterkeyhint=None, hidden=None, +> inert=None, inputmode=None, lang=None, popover=None, +> spellcheck=None, tabindex=None, translate=None, hx_get=None, +> hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, +> hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None, +> hx_select=None, hx_select_oob=None, hx_indicator=None, +> hx_push_url=None, hx_confirm=None, hx_disable=None, +> hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, +> hx_headers=None, hx_history=None, hx_history_elt=None, +> hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None) + +*A standard SVG `rect` element* + +All our shapes just create regular `FT` elements. The only extra +functionality provided by most of them is to add additional defined +kwargs to improve auto-complete in IDEs and notebooks, and re-order +parameters so that positional args can also be used to save a bit of +typing, e.g: + +``` python +demo(Rect(30, 30, fill='blue', rx=8, ry=8)) +``` + + + +------------------------------------------------------------------------ + +source + +### Circle + +> Circle (r, cx=0, cy=0, fill=None, stroke=None, stroke_width=None, +> transform=None, opacity=None, clip=None, mask=None, filter=None, +> vector_effect=None, pointer_events=None, target_id=None, +> hx_vals=None, hx_target=None, id=None, cls=None, title=None, +> style=None, accesskey=None, contenteditable=None, dir=None, +> draggable=None, enterkeyhint=None, hidden=None, inert=None, +> inputmode=None, lang=None, popover=None, spellcheck=None, +> tabindex=None, translate=None, hx_get=None, hx_post=None, +> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, +> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None) + +*A standard SVG `circle` element* + +``` python +demo(Circle(20, 25, 25, stroke='red', stroke_width=3)) +``` + + + +------------------------------------------------------------------------ + +source + +### Ellipse + +> Ellipse (rx, ry, cx=0, cy=0, fill=None, stroke=None, stroke_width=None, +> transform=None, opacity=None, clip=None, mask=None, filter=None, +> vector_effect=None, pointer_events=None, target_id=None, +> hx_vals=None, hx_target=None, id=None, cls=None, title=None, +> style=None, accesskey=None, contenteditable=None, dir=None, +> draggable=None, enterkeyhint=None, hidden=None, inert=None, +> inputmode=None, lang=None, popover=None, spellcheck=None, +> tabindex=None, translate=None, hx_get=None, hx_post=None, +> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, +> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, +> hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None, +> hx_on_focus=None, hx_on_input=None, hx_on_invalid=None, +> hx_on_reset=None, hx_on_select=None, hx_on_submit=None, +> hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None, +> hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None, +> hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, +> hx_on__history_restore=None, hx_on__before_history_save=None, +> hx_on__load=None, hx_on__no_sse_source_error=None, +> hx_on__on_load_error=None, hx_on__oob_after_swap=None, +> hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None, +> hx_on__prompt=None, hx_on__pushed_into_history=None, +> hx_on__replaced_in_history=None, hx_on__response_error=None, +> hx_on__send_abort=None, hx_on__send_error=None, +> hx_on__sse_error=None, hx_on__sse_open=None, +> hx_on__swap_error=None, hx_on__target_error=None, +> hx_on__timeout=None, hx_on__validation_validate=None, +> hx_on__validation_failed=None, hx_on__validation_halted=None, +> hx_on__xhr_abort=None, hx_on__xhr_loadend=None, +> hx_on__xhr_loadstart=None, hx_on__xhr_progress=None) + +*A standard SVG `ellipse` element* + +``` python +demo(Ellipse(20, 10, 25, 25)) +``` + + + +------------------------------------------------------------------------ + +source + +### transformd + +> transformd (translate=None, scale=None, rotate=None, skewX=None, +> skewY=None, matrix=None) + +*Create an SVG `transform` kwarg dict* + +``` python +rot = transformd(rotate=(45, 25, 25)) +rot +``` + + {'transform': 'rotate(45,25,25)'} + +``` python +demo(Ellipse(20, 10, 25, 25, **rot)) +``` + + + +------------------------------------------------------------------------ + +source + +### Line + +> Line (x1, y1, x2=0, y2=0, stroke='black', w=None, stroke_width=1, +> transform=None, opacity=None, clip=None, mask=None, filter=None, +> vector_effect=None, pointer_events=None, target_id=None, +> hx_vals=None, hx_target=None, id=None, cls=None, title=None, +> style=None, accesskey=None, contenteditable=None, dir=None, +> draggable=None, enterkeyhint=None, hidden=None, inert=None, +> inputmode=None, lang=None, popover=None, spellcheck=None, +> tabindex=None, translate=None, hx_get=None, hx_post=None, +> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, +> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, +> hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None, +> hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None, +> hx_on_invalid=None, hx_on_reset=None, hx_on_select=None, +> hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None, +> hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None, +> hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None) + +*A standard SVG `line` element* + +``` python +demo(Line(20, 30, w=3)) +``` + + + +------------------------------------------------------------------------ + +source + +### Polyline + +> Polyline (*args, points=None, fill=None, stroke=None, stroke_width=None, +> transform=None, opacity=None, clip=None, mask=None, +> filter=None, vector_effect=None, pointer_events=None, +> target_id=None, hx_vals=None, hx_target=None, id=None, +> cls=None, title=None, style=None, accesskey=None, +> contenteditable=None, dir=None, draggable=None, +> enterkeyhint=None, hidden=None, inert=None, inputmode=None, +> lang=None, popover=None, spellcheck=None, tabindex=None, +> translate=None, hx_get=None, hx_post=None, hx_put=None, +> hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None, +> hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, +> hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None, +> hx_on_focus=None, hx_on_input=None, hx_on_invalid=None, +> hx_on_reset=None, hx_on_select=None, hx_on_submit=None, +> hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None, +> hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None, +> hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, +> hx_on__history_restore=None, hx_on__before_history_save=None, +> hx_on__load=None, hx_on__no_sse_source_error=None, +> hx_on__on_load_error=None, hx_on__oob_after_swap=None, +> hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None, +> hx_on__prompt=None, hx_on__pushed_into_history=None, +> hx_on__replaced_in_history=None, hx_on__response_error=None, +> hx_on__send_abort=None, hx_on__send_error=None, +> hx_on__sse_error=None, hx_on__sse_open=None, +> hx_on__swap_error=None, hx_on__target_error=None, +> hx_on__timeout=None, hx_on__validation_validate=None, +> hx_on__validation_failed=None, hx_on__validation_halted=None, +> hx_on__xhr_abort=None, hx_on__xhr_loadend=None, +> hx_on__xhr_loadstart=None, hx_on__xhr_progress=None) + +*A standard SVG `polyline` element* + +``` python +demo(Polyline((0,0), (10,10), (20,0), (30,10), (40,0), + fill='yellow', stroke='blue', stroke_width=2)) +``` + + + +``` python +demo(Polyline(points='0,0 10,10 20,0 30,10 40,0', fill='purple', stroke_width=2)) +``` + + + +------------------------------------------------------------------------ + +source + +### Polygon + +> Polygon (*args, points=None, fill=None, stroke=None, stroke_width=None, +> transform=None, opacity=None, clip=None, mask=None, filter=None, +> vector_effect=None, pointer_events=None, target_id=None, +> hx_vals=None, hx_target=None, id=None, cls=None, title=None, +> style=None, accesskey=None, contenteditable=None, dir=None, +> draggable=None, enterkeyhint=None, hidden=None, inert=None, +> inputmode=None, lang=None, popover=None, spellcheck=None, +> tabindex=None, translate=None, hx_get=None, hx_post=None, +> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, +> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, +> hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None, +> hx_on_focus=None, hx_on_input=None, hx_on_invalid=None, +> hx_on_reset=None, hx_on_select=None, hx_on_submit=None, +> hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None, +> hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None, +> hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, +> hx_on__history_restore=None, hx_on__before_history_save=None, +> hx_on__load=None, hx_on__no_sse_source_error=None, +> hx_on__on_load_error=None, hx_on__oob_after_swap=None, +> hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None, +> hx_on__prompt=None, hx_on__pushed_into_history=None, +> hx_on__replaced_in_history=None, hx_on__response_error=None, +> hx_on__send_abort=None, hx_on__send_error=None, +> hx_on__sse_error=None, hx_on__sse_open=None, +> hx_on__swap_error=None, hx_on__target_error=None, +> hx_on__timeout=None, hx_on__validation_validate=None, +> hx_on__validation_failed=None, hx_on__validation_halted=None, +> hx_on__xhr_abort=None, hx_on__xhr_loadend=None, +> hx_on__xhr_loadstart=None, hx_on__xhr_progress=None) + +*A standard SVG `polygon` element* + +``` python +demo(Polygon((25,5), (43.3,15), (43.3,35), (25,45), (6.7,35), (6.7,15), + fill='lightblue', stroke='navy', stroke_width=2)) +``` + + + +``` python +demo(Polygon(points='25,5 43.3,15 43.3,35 25,45 6.7,35 6.7,15', + fill='lightgreen', stroke='darkgreen', stroke_width=2)) +``` + + + +------------------------------------------------------------------------ + +source + +### Text + +> Text (*args, x=0, y=0, font_family=None, font_size=None, fill=None, +> text_anchor=None, dominant_baseline=None, font_weight=None, +> font_style=None, text_decoration=None, transform=None, +> opacity=None, clip=None, mask=None, filter=None, +> vector_effect=None, pointer_events=None, target_id=None, +> hx_vals=None, hx_target=None, id=None, cls=None, title=None, +> style=None, accesskey=None, contenteditable=None, dir=None, +> draggable=None, enterkeyhint=None, hidden=None, inert=None, +> inputmode=None, lang=None, popover=None, spellcheck=None, +> tabindex=None, translate=None, hx_get=None, hx_post=None, +> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, +> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, +> hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None, +> hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None, +> hx_on_invalid=None, hx_on_reset=None, hx_on_select=None, +> hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None, +> hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None, +> hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None) + +*A standard SVG `text` element* + +``` python +demo(Text("Hello!", x=10, y=30)) +``` + +Hello! + +## Paths + +Paths in SVGs are more complex, so we add a small (optional) fluent +interface for constructing them: + +------------------------------------------------------------------------ + +source + +### PathFT + +> PathFT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs) + +*A ‘Fast Tag’ structure, containing `tag`,`children`,and `attrs`* + +------------------------------------------------------------------------ + +source + +### Path + +> Path (d='', fill=None, stroke=None, stroke_width=None, transform=None, +> opacity=None, clip=None, mask=None, filter=None, +> vector_effect=None, pointer_events=None, target_id=None, +> hx_vals=None, hx_target=None, id=None, cls=None, title=None, +> style=None, accesskey=None, contenteditable=None, dir=None, +> draggable=None, enterkeyhint=None, hidden=None, inert=None, +> inputmode=None, lang=None, popover=None, spellcheck=None, +> tabindex=None, translate=None, hx_get=None, hx_post=None, +> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, +> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, +> hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None, +> hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None, +> hx_on_invalid=None, hx_on_reset=None, hx_on_select=None, +> hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None, +> hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None, +> hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None) + +*Create a standard `path` SVG element. This is a special object* + +Let’s create a square shape, but using +[`Path`](https://www.fastht.ml/docs/api/svg.html#path) instead of +[`Rect`](https://www.fastht.ml/docs/api/svg.html#rect): + +- M(10, 10): Move to starting point (10, 10) +- L(40, 10): Line to (40, 10) - top edge +- L(40, 40): Line to (40, 40) - right edge +- L(10, 40): Line to (10, 40) - bottom edge +- Z(): Close path - connects back to start + +M = Move to, L = Line to, Z = Close path + +``` python +demo(Path(fill='none', stroke='purple', stroke_width=2 + ).M(10, 10).L(40, 10).L(40, 40).L(10, 40).Z()) +``` + + + +Using curves we can create a spiral: + +``` python +p = (Path(fill='none', stroke='purple', stroke_width=2) + .M(25, 25) + .C(25, 25, 20, 20, 30, 20) + .C(40, 20, 40, 30, 30, 30) + .C(20, 30, 20, 15, 35, 15) + .C(50, 15, 50, 35, 25, 35) + .C(0, 35, 0, 10, 40, 10) + .C(80, 10, 80, 40, 25, 40)) +demo(p, 50, 100) +``` + + + +Using arcs and curves we can create a map marker icon: + +``` python +p = (Path(fill='red') + .M(25,45) + .C(25,45,10,35,10,25) + .A(15,15,0,1,1,40,25) + .C(40,35,25,45,25,45) + .Z()) +demo(p) +``` + + + +Behind the scenes it’s just creating regular SVG path `d` attr – you can +pass `d` in directly if you prefer. + +``` python +print(p.d) +``` + + M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z + +``` python +demo(Path(d='M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z')) +``` + + + +------------------------------------------------------------------------ + +source + +### PathFT.M + +> PathFT.M (x, y) + +*Move to.* + +------------------------------------------------------------------------ + +source + +### PathFT.L + +> PathFT.L (x, y) + +*Line to.* + +------------------------------------------------------------------------ + +source + +### PathFT.H + +> PathFT.H (x) + +*Horizontal line to.* + +------------------------------------------------------------------------ + +source + +### PathFT.V + +> PathFT.V (y) + +*Vertical line to.* + +------------------------------------------------------------------------ + +source + +### PathFT.Z + +> PathFT.Z () + +*Close path.* + +------------------------------------------------------------------------ + +source + +### PathFT.C + +> PathFT.C (x1, y1, x2, y2, x, y) + +*Cubic Bézier curve.* + +------------------------------------------------------------------------ + +source + +### PathFT.S + +> PathFT.S (x2, y2, x, y) + +*Smooth cubic Bézier curve.* + +------------------------------------------------------------------------ + +source + +### PathFT.Q + +> PathFT.Q (x1, y1, x, y) + +*Quadratic Bézier curve.* + +------------------------------------------------------------------------ + +source + +### PathFT.T + +> PathFT.T (x, y) + +*Smooth quadratic Bézier curve.* + +------------------------------------------------------------------------ + +source + +### PathFT.A + +> PathFT.A (rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y) + +*Elliptical Arc.* + +## HTMX helpers + +------------------------------------------------------------------------ + +source + +### SvgOob + +> SvgOob (*args, **kwargs) + +*Wraps an SVG shape as required for an HTMX OOB swap* + +When returning an SVG shape out-of-band (OOB) in HTMX, you need to wrap +it with [`SvgOob`](https://www.fastht.ml/docs/api/svg.html#svgoob) to +have it appear correctly. +([`SvgOob`](https://www.fastht.ml/docs/api/svg.html#svgoob) is just a +shortcut for `Template(Svg(...))`, which is the trick that makes SVG OOB +swaps work.) + +------------------------------------------------------------------------ + +source + +### SvgInb + +> SvgInb (*args, **kwargs) + +*Wraps an SVG shape as required for an HTMX inband swap* + +When returning an SVG shape in-band in HTMX, either have the calling +element include `hx_select='svg>*'`, or `**svg_inb` (which are two ways +of saying the same thing), or wrap the response with +[`SvgInb`](https://www.fastht.ml/docs/api/svg.html#svginb) to have it +appear correctly. +([`SvgInb`](https://www.fastht.ml/docs/api/svg.html#svginb) is just a +shortcut for the tuple +`(Svg(...), HtmxResponseHeaders(hx_reselect='svg>*'))`, which is the +trick that makes SVG in-band swaps work.) diff --git a/docs/api/xtend.html b/docs/api/xtend.html new file mode 100644 index 0000000000000000000000000000000000000000..f941d30bcdfed0b29bffc3f6537041646cabe99b --- /dev/null +++ b/docs/api/xtend.html @@ -0,0 +1,1488 @@ + + + + + + + + + + +Component extensions – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Component extensions

+
+ +
+
+ Simple extensions to standard HTML components, such as adding sensible defaults +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from pprint import pprint
+
+
+

source

+
+

A

+
+
 A (*c, hx_get=None, target_id=None, hx_swap=None, href='#', hx_vals=None,
+    hx_target=None, id=None, cls=None, title=None, style=None,
+    accesskey=None, contenteditable=None, dir=None, draggable=None,
+    enterkeyhint=None, hidden=None, inert=None, inputmode=None, lang=None,
+    popover=None, spellcheck=None, tabindex=None, translate=None,
+    hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
+    hx_trigger=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+    hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+    hx_confirm=None, hx_disable=None, hx_replace_url=None,
+    hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None,
+    hx_history_elt=None, hx_inherit=None, hx_params=None,
+    hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None,
+    hx_validate=None, hx_on_blur=None, hx_on_change=None,
+    hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,
+    hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,
+    hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,
+    hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,
+    hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,
+    hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+    hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+    hx_on__after_on_load=None, hx_on__after_process_node=None,
+    hx_on__after_request=None, hx_on__after_settle=None,
+    hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+    hx_on__before_on_load=None, hx_on__before_process_node=None,
+    hx_on__before_request=None, hx_on__before_swap=None,
+    hx_on__before_send=None, hx_on__before_transition=None,
+    hx_on__config_request=None, hx_on__confirm=None,
+    hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+    hx_on__history_cache_miss_error=None,
+    hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+    hx_on__before_history_save=None, hx_on__load=None,
+    hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+    hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+    hx_on__oob_error_no_target=None, hx_on__prompt=None,
+    hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+    hx_on__response_error=None, hx_on__send_abort=None,
+    hx_on__send_error=None, hx_on__sse_error=None, hx_on__sse_open=None,
+    hx_on__swap_error=None, hx_on__target_error=None, hx_on__timeout=None,
+    hx_on__validation_validate=None, hx_on__validation_failed=None,
+    hx_on__validation_halted=None, hx_on__xhr_abort=None,
+    hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+    hx_on__xhr_progress=None, **kwargs)
+
+

An A tag; href defaults to ‘#’ for more concise use with HTMX

+
+
A('text', ht_get='/get', target_id='id')
+
+
<a href="#" ht-get="/get" hx-target="#id">text</a>
+
+
+
+

source

+
+
+

AX

+
+
 AX (txt, hx_get=None, target_id=None, hx_swap=None, href='#',
+     hx_vals=None, hx_target=None, id=None, cls=None, title=None,
+     style=None, accesskey=None, contenteditable=None, dir=None,
+     draggable=None, enterkeyhint=None, hidden=None, inert=None,
+     inputmode=None, lang=None, popover=None, spellcheck=None,
+     tabindex=None, translate=None, hx_post=None, hx_put=None,
+     hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap_oob=None,
+     hx_include=None, hx_select=None, hx_select_oob=None,
+     hx_indicator=None, hx_push_url=None, hx_confirm=None,
+     hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,
+     hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None,
+     hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
+     hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,
+     hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+     hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+     hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+     hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+     hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,
+     hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,
+     hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+     hx_on__abort=None, hx_on__after_on_load=None,
+     hx_on__after_process_node=None, hx_on__after_request=None,
+     hx_on__after_settle=None, hx_on__after_swap=None,
+     hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+     hx_on__before_process_node=None, hx_on__before_request=None,
+     hx_on__before_swap=None, hx_on__before_send=None,
+     hx_on__before_transition=None, hx_on__config_request=None,
+     hx_on__confirm=None, hx_on__history_cache_error=None,
+     hx_on__history_cache_miss=None, hx_on__history_cache_miss_error=None,
+     hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+     hx_on__before_history_save=None, hx_on__load=None,
+     hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+     hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+     hx_on__oob_error_no_target=None, hx_on__prompt=None,
+     hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+     hx_on__response_error=None, hx_on__send_abort=None,
+     hx_on__send_error=None, hx_on__sse_error=None, hx_on__sse_open=None,
+     hx_on__swap_error=None, hx_on__target_error=None,
+     hx_on__timeout=None, hx_on__validation_validate=None,
+     hx_on__validation_failed=None, hx_on__validation_halted=None,
+     hx_on__xhr_abort=None, hx_on__xhr_loadend=None,
+     hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)
+
+

An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be positional params

+
+
AX('text', '/get', 'id')
+
+
<a href="#" hx-get="/get" hx-target="#id">text</a>
+
+
+
+
+

Forms

+
+

source

+
+

Form

+
+
 Form (*c, enctype='multipart/form-data', target_id=None, hx_vals=None,
+       hx_target=None, id=None, cls=None, title=None, style=None,
+       accesskey=None, contenteditable=None, dir=None, draggable=None,
+       enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+       lang=None, popover=None, spellcheck=None, tabindex=None,
+       translate=None, hx_get=None, hx_post=None, hx_put=None,
+       hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
+       hx_swap_oob=None, hx_include=None, hx_select=None,
+       hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+       hx_confirm=None, hx_disable=None, hx_replace_url=None,
+       hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+       hx_history=None, hx_history_elt=None, hx_inherit=None,
+       hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
+       hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,
+       hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,
+       hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,
+       hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,
+       hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,
+       hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,
+       hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,
+       hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,
+       hx_on__after_on_load=None, hx_on__after_process_node=None,
+       hx_on__after_request=None, hx_on__after_settle=None,
+       hx_on__after_swap=None, hx_on__before_cleanup_element=None,
+       hx_on__before_on_load=None, hx_on__before_process_node=None,
+       hx_on__before_request=None, hx_on__before_swap=None,
+       hx_on__before_send=None, hx_on__before_transition=None,
+       hx_on__config_request=None, hx_on__confirm=None,
+       hx_on__history_cache_error=None, hx_on__history_cache_miss=None,
+       hx_on__history_cache_miss_error=None,
+       hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+       hx_on__before_history_save=None, hx_on__load=None,
+       hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+       hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+       hx_on__oob_error_no_target=None, hx_on__prompt=None,
+       hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+       hx_on__response_error=None, hx_on__send_abort=None,
+       hx_on__send_error=None, hx_on__sse_error=None,
+       hx_on__sse_open=None, hx_on__swap_error=None,
+       hx_on__target_error=None, hx_on__timeout=None,
+       hx_on__validation_validate=None, hx_on__validation_failed=None,
+       hx_on__validation_halted=None, hx_on__xhr_abort=None,
+       hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+       hx_on__xhr_progress=None, **kwargs)
+
+

A Form tag; identical to plain ft_hx version except default enctype='multipart/form-data'

+
+

source

+
+
+

Hidden

+
+
 Hidden (value:Any='', id:Any=None, target_id=None, hx_vals=None,
+         hx_target=None, cls=None, title=None, style=None, accesskey=None,
+         contenteditable=None, dir=None, draggable=None,
+         enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+         lang=None, popover=None, spellcheck=None, tabindex=None,
+         translate=None, hx_get=None, hx_post=None, hx_put=None,
+         hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
+         hx_swap_oob=None, hx_include=None, hx_select=None,
+         hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+         hx_confirm=None, hx_disable=None, hx_replace_url=None,
+         hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+         hx_history=None, hx_history_elt=None, hx_inherit=None,
+         hx_params=None, hx_preserve=None, hx_prompt=None,
+         hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,
+         hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+         hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+         hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+         hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+         hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,
+         hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,
+         hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+         hx_on__abort=None, hx_on__after_on_load=None,
+         hx_on__after_process_node=None, hx_on__after_request=None,
+         hx_on__after_settle=None, hx_on__after_swap=None,
+         hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+         hx_on__before_process_node=None, hx_on__before_request=None,
+         hx_on__before_swap=None, hx_on__before_send=None,
+         hx_on__before_transition=None, hx_on__config_request=None,
+         hx_on__confirm=None, hx_on__history_cache_error=None,
+         hx_on__history_cache_miss=None,
+         hx_on__history_cache_miss_error=None,
+         hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+         hx_on__before_history_save=None, hx_on__load=None,
+         hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+         hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+         hx_on__oob_error_no_target=None, hx_on__prompt=None,
+         hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+         hx_on__response_error=None, hx_on__send_abort=None,
+         hx_on__send_error=None, hx_on__sse_error=None,
+         hx_on__sse_open=None, hx_on__swap_error=None,
+         hx_on__target_error=None, hx_on__timeout=None,
+         hx_on__validation_validate=None, hx_on__validation_failed=None,
+         hx_on__validation_halted=None, hx_on__xhr_abort=None,
+         hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+         hx_on__xhr_progress=None, **kwargs)
+
+

An Input of type ‘hidden’

+
+

source

+
+
+

CheckboxX

+
+
 CheckboxX (checked:bool=False, label=None, value='1', id=None, name=None,
+            target_id=None, hx_vals=None, hx_target=None, cls=None,
+            title=None, style=None, accesskey=None, contenteditable=None,
+            dir=None, draggable=None, enterkeyhint=None, hidden=None,
+            inert=None, inputmode=None, lang=None, popover=None,
+            spellcheck=None, tabindex=None, translate=None, hx_get=None,
+            hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
+            hx_trigger=None, hx_swap=None, hx_swap_oob=None,
+            hx_include=None, hx_select=None, hx_select_oob=None,
+            hx_indicator=None, hx_push_url=None, hx_confirm=None,
+            hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,
+            hx_ext=None, hx_headers=None, hx_history=None,
+            hx_history_elt=None, hx_inherit=None, hx_params=None,
+            hx_preserve=None, hx_prompt=None, hx_request=None,
+            hx_sync=None, hx_validate=None, hx_on_blur=None,
+            hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+            hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+            hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+            hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+            hx_on_dblclick=None, hx_on_mousedown=None,
+            hx_on_mouseenter=None, hx_on_mouseleave=None,
+            hx_on_mousemove=None, hx_on_mouseout=None,
+            hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+            hx_on__abort=None, hx_on__after_on_load=None,
+            hx_on__after_process_node=None, hx_on__after_request=None,
+            hx_on__after_settle=None, hx_on__after_swap=None,
+            hx_on__before_cleanup_element=None,
+            hx_on__before_on_load=None, hx_on__before_process_node=None,
+            hx_on__before_request=None, hx_on__before_swap=None,
+            hx_on__before_send=None, hx_on__before_transition=None,
+            hx_on__config_request=None, hx_on__confirm=None,
+            hx_on__history_cache_error=None,
+            hx_on__history_cache_miss=None,
+            hx_on__history_cache_miss_error=None,
+            hx_on__history_cache_miss_load=None,
+            hx_on__history_restore=None, hx_on__before_history_save=None,
+            hx_on__load=None, hx_on__no_sse_source_error=None,
+            hx_on__on_load_error=None, hx_on__oob_after_swap=None,
+            hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,
+            hx_on__prompt=None, hx_on__pushed_into_history=None,
+            hx_on__replaced_in_history=None, hx_on__response_error=None,
+            hx_on__send_abort=None, hx_on__send_error=None,
+            hx_on__sse_error=None, hx_on__sse_open=None,
+            hx_on__swap_error=None, hx_on__target_error=None,
+            hx_on__timeout=None, hx_on__validation_validate=None,
+            hx_on__validation_failed=None, hx_on__validation_halted=None,
+            hx_on__xhr_abort=None, hx_on__xhr_loadend=None,
+            hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)
+
+

A Checkbox optionally inside a Label, preceded by a Hidden with matching name

+
+
show(CheckboxX(True, 'Check me out!'))
+
+ + +
+
+
+

source

+
+
+

Script

+
+
 Script (code:str='', id=None, cls=None, title=None, style=None,
+         attrmap=None, valmap=None, ft_cls=None, **kwargs)
+
+

A Script tag that doesn’t escape its code

+
+

source

+
+
+

Style

+
+
 Style (*c, id=None, cls=None, title=None, style=None, attrmap=None,
+        valmap=None, ft_cls=None, **kwargs)
+
+

A Style tag that doesn’t escape its code

+
+
+
+

Style and script templates

+
+

source

+
+

double_braces

+
+
 double_braces (s)
+
+

Convert single braces to double braces if next to special chars or newline

+
+

source

+
+
+

undouble_braces

+
+
 undouble_braces (s)
+
+

Convert double braces to single braces if next to special chars or newline

+
+

source

+
+
+

loose_format

+
+
 loose_format (s, **kw)
+
+

String format s using kw, without being strict about braces outside of template params

+
+

source

+
+
+

ScriptX

+
+
 ScriptX (fname, src=None, nomodule=None, type=None, _async=None,
+          defer=None, charset=None, crossorigin=None, integrity=None,
+          **kw)
+
+

A script element with contents read from fname

+
+

source

+
+
+

replace_css_vars

+
+
 replace_css_vars (css, pre='tpl', **kwargs)
+
+

Replace var(--) CSS variables with kwargs if name prefix matches pre

+
+

source

+
+
+

StyleX

+
+
 StyleX (fname, **kw)
+
+

A style element with contents read from fname and variables replaced from kw

+
+

source

+
+
+

Nbsp

+
+
 Nbsp ()
+
+

A non-breaking space

+
+
+
+

Surreal and JS

+
+

source

+
+

Surreal

+
+
 Surreal (code:str)
+
+

Wrap code in domReadyExecute and set m=me() and p=me('-')

+
+

source

+
+
+

On

+
+
 On (code:str, event:str='click', sel:str='', me=True)
+
+

An async surreal.js script block event handler for event on selector sel,p, making available parent p, event ev, and target e

+
+

source

+
+
+

Prev

+
+
 Prev (code:str, event:str='click')
+
+

An async surreal.js script block event handler for event on previous sibling, with same vars as On

+
+

source

+
+
+

Now

+
+
 Now (code:str, sel:str='')
+
+

An async surreal.js script block on selector me(sel)

+
+

source

+
+
+

AnyNow

+
+
 AnyNow (sel:str, code:str)
+
+

An async surreal.js script block on selector any(sel)

+
+

source

+
+
+

run_js

+
+
 run_js (js, id=None, **kw)
+
+

Run js script, auto-generating id based on name of caller if needed, and js-escaping any kw params

+
+

source

+
+
+

HtmxOn

+
+
 HtmxOn (eventname:str, code:str)
+
+
+

source

+
+
+

jsd

+
+
 jsd (org, repo, root, path, prov='gh', typ='script', ver=None, esm=False,
+      **kwargs)
+
+

jsdelivr Script or CSS Link tag, or URL

+
+
+
+

Other helpers

+
+

source

+
+

Fragment

+
+
 Fragment (*c)
+
+

An empty tag, used as a container

+
+
fts = Fragment(P('1st'), P('2nd'))
+print(to_xml(fts))
+
+
  <p>1st</p>
+  <p>2nd</p>
+
+
+
+
+

source

+
+
+

Titled

+
+
 Titled (title:str='FastHTML app', *args, cls='container', target_id=None,
+         hx_vals=None, hx_target=None, id=None, style=None,
+         accesskey=None, contenteditable=None, dir=None, draggable=None,
+         enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+         lang=None, popover=None, spellcheck=None, tabindex=None,
+         translate=None, hx_get=None, hx_post=None, hx_put=None,
+         hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
+         hx_swap_oob=None, hx_include=None, hx_select=None,
+         hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+         hx_confirm=None, hx_disable=None, hx_replace_url=None,
+         hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+         hx_history=None, hx_history_elt=None, hx_inherit=None,
+         hx_params=None, hx_preserve=None, hx_prompt=None,
+         hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,
+         hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,
+         hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,
+         hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,
+         hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,
+         hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,
+         hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,
+         hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,
+         hx_on__abort=None, hx_on__after_on_load=None,
+         hx_on__after_process_node=None, hx_on__after_request=None,
+         hx_on__after_settle=None, hx_on__after_swap=None,
+         hx_on__before_cleanup_element=None, hx_on__before_on_load=None,
+         hx_on__before_process_node=None, hx_on__before_request=None,
+         hx_on__before_swap=None, hx_on__before_send=None,
+         hx_on__before_transition=None, hx_on__config_request=None,
+         hx_on__confirm=None, hx_on__history_cache_error=None,
+         hx_on__history_cache_miss=None,
+         hx_on__history_cache_miss_error=None,
+         hx_on__history_cache_miss_load=None, hx_on__history_restore=None,
+         hx_on__before_history_save=None, hx_on__load=None,
+         hx_on__no_sse_source_error=None, hx_on__on_load_error=None,
+         hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,
+         hx_on__oob_error_no_target=None, hx_on__prompt=None,
+         hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,
+         hx_on__response_error=None, hx_on__send_abort=None,
+         hx_on__send_error=None, hx_on__sse_error=None,
+         hx_on__sse_open=None, hx_on__swap_error=None,
+         hx_on__target_error=None, hx_on__timeout=None,
+         hx_on__validation_validate=None, hx_on__validation_failed=None,
+         hx_on__validation_halted=None, hx_on__xhr_abort=None,
+         hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,
+         hx_on__xhr_progress=None, **kwargs)
+
+

An HTML partial containing a Title, and H1, and any provided children

+
+
show(Titled('my page', P('para')))
+
+my page +

my page

+

para

+
+
+
+
+

source

+
+
+

Socials

+
+
 Socials (title, site_name, description, image, url=None, w=1200, h=630,
+          twitter_site=None, creator=None, card='summary')
+
+

OG and Twitter social card headers

+
+

source

+
+
+

YouTubeEmbed

+
+
 YouTubeEmbed (video_id:str, width:int=560, height:int=315,
+               start_time:int=0, no_controls:bool=False,
+               title:str='YouTube video player', cls:str='', **kwargs)
+
+

Embed a YouTube video

+
+

source

+
+
+

Favicon

+
+
 Favicon (light_icon, dark_icon)
+
+

Light and dark favicon headers

+
+

source

+
+
+

clear

+
+
 clear (id)
+
+
+

source

+
+
+

with_sid

+
+
 with_sid (app, dest, path='/')
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/xtend.html.md b/docs/api/xtend.html.md new file mode 100644 index 0000000000000000000000000000000000000000..344a7ab16e547f4c49db3dbcb856c0e8e247c276 --- /dev/null +++ b/docs/api/xtend.html.md @@ -0,0 +1,683 @@ +# Component extensions + + + + +``` python +from pprint import pprint +``` + +------------------------------------------------------------------------ + +source + +### A + +> A (*c, hx_get=None, target_id=None, hx_swap=None, href='#', hx_vals=None, +> hx_target=None, id=None, cls=None, title=None, style=None, +> accesskey=None, contenteditable=None, dir=None, draggable=None, +> enterkeyhint=None, hidden=None, inert=None, inputmode=None, lang=None, +> popover=None, spellcheck=None, tabindex=None, translate=None, +> hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, +> hx_trigger=None, hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None, +> hx_history_elt=None, hx_inherit=None, hx_params=None, +> hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None, +> hx_validate=None, hx_on_blur=None, hx_on_change=None, +> hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None, +> hx_on_invalid=None, hx_on_reset=None, hx_on_select=None, +> hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None, +> hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None, +> hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, hx_on__sse_open=None, +> hx_on__swap_error=None, hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +*An A tag; `href` defaults to ‘\#’ for more concise use with HTMX* + +``` python +A('text', ht_get='/get', target_id='id') +``` + +``` html +text +``` + +------------------------------------------------------------------------ + +source + +### AX + +> AX (txt, hx_get=None, target_id=None, hx_swap=None, href='#', +> hx_vals=None, hx_target=None, id=None, cls=None, title=None, +> style=None, accesskey=None, contenteditable=None, dir=None, +> draggable=None, enterkeyhint=None, hidden=None, inert=None, +> inputmode=None, lang=None, popover=None, spellcheck=None, +> tabindex=None, translate=None, hx_post=None, hx_put=None, +> hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap_oob=None, +> hx_include=None, hx_select=None, hx_select_oob=None, +> hx_indicator=None, hx_push_url=None, hx_confirm=None, +> hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, +> hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None, +> hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, hx_on__sse_open=None, +> hx_on__swap_error=None, hx_on__target_error=None, +> hx_on__timeout=None, hx_on__validation_validate=None, +> hx_on__validation_failed=None, hx_on__validation_halted=None, +> hx_on__xhr_abort=None, hx_on__xhr_loadend=None, +> hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs) + +*An A tag with just one text child, allowing hx_get, target_id, and +hx_swap to be positional params* + +``` python +AX('text', '/get', 'id') +``` + +``` html +text +``` + +## Forms + +------------------------------------------------------------------------ + +source + +### Form + +> Form (*c, enctype='multipart/form-data', target_id=None, hx_vals=None, +> hx_target=None, id=None, cls=None, title=None, style=None, +> accesskey=None, contenteditable=None, dir=None, draggable=None, +> enterkeyhint=None, hidden=None, inert=None, inputmode=None, +> lang=None, popover=None, spellcheck=None, tabindex=None, +> translate=None, hx_get=None, hx_post=None, hx_put=None, +> hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None, +> hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, +> hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None, +> hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None, +> hx_on_invalid=None, hx_on_reset=None, hx_on_select=None, +> hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None, +> hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None, +> hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, +> hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, +> hx_on__after_on_load=None, hx_on__after_process_node=None, +> hx_on__after_request=None, hx_on__after_settle=None, +> hx_on__after_swap=None, hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +*A Form tag; identical to plain +[`ft_hx`](https://www.fastht.ml/docs/api/components.html#ft_hx) version +except default `enctype='multipart/form-data'`* + +------------------------------------------------------------------------ + +source + +### Hidden + +> Hidden (value:Any='', id:Any=None, target_id=None, hx_vals=None, +> hx_target=None, cls=None, title=None, style=None, accesskey=None, +> contenteditable=None, dir=None, draggable=None, +> enterkeyhint=None, hidden=None, inert=None, inputmode=None, +> lang=None, popover=None, spellcheck=None, tabindex=None, +> translate=None, hx_get=None, hx_post=None, hx_put=None, +> hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None, +> hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +*An Input of type ‘hidden’* + +------------------------------------------------------------------------ + +source + +### CheckboxX + +> CheckboxX (checked:bool=False, label=None, value='1', id=None, name=None, +> target_id=None, hx_vals=None, hx_target=None, cls=None, +> title=None, style=None, accesskey=None, contenteditable=None, +> dir=None, draggable=None, enterkeyhint=None, hidden=None, +> inert=None, inputmode=None, lang=None, popover=None, +> spellcheck=None, tabindex=None, translate=None, hx_get=None, +> hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, +> hx_trigger=None, hx_swap=None, hx_swap_oob=None, +> hx_include=None, hx_select=None, hx_select_oob=None, +> hx_indicator=None, hx_push_url=None, hx_confirm=None, +> hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, +> hx_ext=None, hx_headers=None, hx_history=None, +> hx_history_elt=None, hx_inherit=None, hx_params=None, +> hx_preserve=None, hx_prompt=None, hx_request=None, +> hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, +> hx_on_mouseenter=None, hx_on_mouseleave=None, +> hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, +> hx_on__before_on_load=None, hx_on__before_process_node=None, +> hx_on__before_request=None, hx_on__before_swap=None, +> hx_on__before_send=None, hx_on__before_transition=None, +> hx_on__config_request=None, hx_on__confirm=None, +> hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, +> hx_on__history_restore=None, hx_on__before_history_save=None, +> hx_on__load=None, hx_on__no_sse_source_error=None, +> hx_on__on_load_error=None, hx_on__oob_after_swap=None, +> hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None, +> hx_on__prompt=None, hx_on__pushed_into_history=None, +> hx_on__replaced_in_history=None, hx_on__response_error=None, +> hx_on__send_abort=None, hx_on__send_error=None, +> hx_on__sse_error=None, hx_on__sse_open=None, +> hx_on__swap_error=None, hx_on__target_error=None, +> hx_on__timeout=None, hx_on__validation_validate=None, +> hx_on__validation_failed=None, hx_on__validation_halted=None, +> hx_on__xhr_abort=None, hx_on__xhr_loadend=None, +> hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs) + +*A Checkbox optionally inside a Label, preceded by a +[`Hidden`](https://www.fastht.ml/docs/api/xtend.html#hidden) with +matching name* + +``` python +show(CheckboxX(True, 'Check me out!')) +``` + + + + +------------------------------------------------------------------------ + +source + +### Script + +> Script (code:str='', id=None, cls=None, title=None, style=None, +> attrmap=None, valmap=None, ft_cls=None, **kwargs) + +*A Script tag that doesn’t escape its code* + +------------------------------------------------------------------------ + +source + +### Style + +> Style (*c, id=None, cls=None, title=None, style=None, attrmap=None, +> valmap=None, ft_cls=None, **kwargs) + +*A Style tag that doesn’t escape its code* + +## Style and script templates + +------------------------------------------------------------------------ + +source + +### double_braces + +> double_braces (s) + +*Convert single braces to double braces if next to special chars or +newline* + +------------------------------------------------------------------------ + +source + +### undouble_braces + +> undouble_braces (s) + +*Convert double braces to single braces if next to special chars or +newline* + +------------------------------------------------------------------------ + +source + +### loose_format + +> loose_format (s, **kw) + +*String format `s` using `kw`, without being strict about braces outside +of template params* + +------------------------------------------------------------------------ + +source + +### ScriptX + +> ScriptX (fname, src=None, nomodule=None, type=None, _async=None, +> defer=None, charset=None, crossorigin=None, integrity=None, +> **kw) + +*A `script` element with contents read from `fname`* + +------------------------------------------------------------------------ + +source + +### replace_css_vars + +> replace_css_vars (css, pre='tpl', **kwargs) + +*Replace `var(--)` CSS variables with `kwargs` if name prefix matches +`pre`* + +------------------------------------------------------------------------ + +source + +### StyleX + +> StyleX (fname, **kw) + +*A `style` element with contents read from `fname` and variables +replaced from `kw`* + +------------------------------------------------------------------------ + +source + +### Nbsp + +> Nbsp () + +*A non-breaking space* + +## Surreal and JS + +------------------------------------------------------------------------ + +source + +### Surreal + +> Surreal (code:str) + +*Wrap `code` in `domReadyExecute` and set `m=me()` and `p=me('-')`* + +------------------------------------------------------------------------ + +source + +### On + +> On (code:str, event:str='click', sel:str='', me=True) + +*An async surreal.js script block event handler for `event` on selector +`sel,p`, making available parent `p`, event `ev`, and target `e`* + +------------------------------------------------------------------------ + +source + +### Prev + +> Prev (code:str, event:str='click') + +*An async surreal.js script block event handler for `event` on previous +sibling, with same vars as +[`On`](https://www.fastht.ml/docs/api/xtend.html#on)* + +------------------------------------------------------------------------ + +source + +### Now + +> Now (code:str, sel:str='') + +*An async surreal.js script block on selector `me(sel)`* + +------------------------------------------------------------------------ + +source + +### AnyNow + +> AnyNow (sel:str, code:str) + +*An async surreal.js script block on selector `any(sel)`* + +------------------------------------------------------------------------ + +source + +### run_js + +> run_js (js, id=None, **kw) + +*Run `js` script, auto-generating `id` based on name of caller if +needed, and js-escaping any `kw` params* + +------------------------------------------------------------------------ + +source + +### HtmxOn + +> HtmxOn (eventname:str, code:str) + +------------------------------------------------------------------------ + +source + +### jsd + +> jsd (org, repo, root, path, prov='gh', typ='script', ver=None, esm=False, +> **kwargs) + +*jsdelivr [`Script`](https://www.fastht.ml/docs/api/xtend.html#script) +or CSS `Link` tag, or URL* + +## Other helpers + +------------------------------------------------------------------------ + +source + +### Fragment + +> Fragment (*c) + +*An empty tag, used as a container* + +``` python +fts = Fragment(P('1st'), P('2nd')) +print(to_xml(fts)) +``` + +

1st

+

2nd

+ +------------------------------------------------------------------------ + +source + +### Titled + +> Titled (title:str='FastHTML app', *args, cls='container', target_id=None, +> hx_vals=None, hx_target=None, id=None, style=None, +> accesskey=None, contenteditable=None, dir=None, draggable=None, +> enterkeyhint=None, hidden=None, inert=None, inputmode=None, +> lang=None, popover=None, spellcheck=None, tabindex=None, +> translate=None, hx_get=None, hx_post=None, hx_put=None, +> hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None, +> hx_swap_oob=None, hx_include=None, hx_select=None, +> hx_select_oob=None, hx_indicator=None, hx_push_url=None, +> hx_confirm=None, hx_disable=None, hx_replace_url=None, +> hx_disabled_elt=None, hx_ext=None, hx_headers=None, +> hx_history=None, hx_history_elt=None, hx_inherit=None, +> hx_params=None, hx_preserve=None, hx_prompt=None, +> hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, +> hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, +> hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, +> hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, +> hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, +> hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, +> hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, +> hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, +> hx_on__abort=None, hx_on__after_on_load=None, +> hx_on__after_process_node=None, hx_on__after_request=None, +> hx_on__after_settle=None, hx_on__after_swap=None, +> hx_on__before_cleanup_element=None, hx_on__before_on_load=None, +> hx_on__before_process_node=None, hx_on__before_request=None, +> hx_on__before_swap=None, hx_on__before_send=None, +> hx_on__before_transition=None, hx_on__config_request=None, +> hx_on__confirm=None, hx_on__history_cache_error=None, +> hx_on__history_cache_miss=None, +> hx_on__history_cache_miss_error=None, +> hx_on__history_cache_miss_load=None, hx_on__history_restore=None, +> hx_on__before_history_save=None, hx_on__load=None, +> hx_on__no_sse_source_error=None, hx_on__on_load_error=None, +> hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, +> hx_on__oob_error_no_target=None, hx_on__prompt=None, +> hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, +> hx_on__response_error=None, hx_on__send_abort=None, +> hx_on__send_error=None, hx_on__sse_error=None, +> hx_on__sse_open=None, hx_on__swap_error=None, +> hx_on__target_error=None, hx_on__timeout=None, +> hx_on__validation_validate=None, hx_on__validation_failed=None, +> hx_on__validation_halted=None, hx_on__xhr_abort=None, +> hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, +> hx_on__xhr_progress=None, **kwargs) + +*An HTML partial containing a `Title`, and `H1`, and any provided +children* + +``` python +show(Titled('my page', P('para'))) +``` + +my page +

my page

+

para

+
+ +------------------------------------------------------------------------ + +source + +### Socials + +> Socials (title, site_name, description, image, url=None, w=1200, h=630, +> twitter_site=None, creator=None, card='summary') + +*OG and Twitter social card headers* + +------------------------------------------------------------------------ + +source + +### YouTubeEmbed + +> YouTubeEmbed (video_id:str, width:int=560, height:int=315, +> start_time:int=0, no_controls:bool=False, +> title:str='YouTube video player', cls:str='', **kwargs) + +*Embed a YouTube video* + +------------------------------------------------------------------------ + +source + +### Favicon + +> Favicon (light_icon, dark_icon) + +*Light and dark favicon headers* + +------------------------------------------------------------------------ + +source + +### clear + +> clear (id) + +------------------------------------------------------------------------ + +source + +### with_sid + +> with_sid (app, dest, path='/') diff --git a/docs/apilist.txt b/docs/apilist.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd377696d91e6a8250f880539f20cfdea3af6375 --- /dev/null +++ b/docs/apilist.txt @@ -0,0 +1,510 @@ +# fasthtml Module Documentation + +## fasthtml.authmw + +- `class BasicAuthMiddleware` + - `def __init__(self, app, cb, skip)` + - `def __call__(self, scope, receive, send)` + - `def authenticate(self, conn)` + +## fasthtml.cli + +- `@call_parse def railway_link()` + Link the current directory to the current project's Railway service + +- `@call_parse def railway_deploy(name, mount)` + Deploy a FastHTML app to Railway + +## fasthtml.components + +> `ft_html` and `ft_hx` functions to add some conveniences to `ft`, along with a full set of basic HTML components, and functions to work with forms and `FT` conversion + +- `def File(fname)` + Use the unescaped text in file `fname` directly + +- `def show(ft, *rest)` + Renders FT Components into HTML within a Jupyter notebook. + +- `def fill_form(form, obj)` + Fills named items in `form` using attributes in `obj` + +- `def fill_dataclass(src, dest)` + Modifies dataclass in-place and returns it + +- `def find_inputs(e, tags, **kw)` + Recursively find all elements in `e` with `tags` and attrs matching `kw` + +- `def html2ft(html, attr1st)` + Convert HTML to an `ft` expression + +- `def sse_message(elm, event)` + Convert element `elm` into a format suitable for SSE streaming + +## fasthtml.core + +> The `FastHTML` subclass of `Starlette`, along with the `RouterX` and `RouteX` classes it automatically uses. + +- `def parsed_date(s)` + Convert `s` to a datetime + +- `def snake2hyphens(s)` + Convert `s` from snake case to hyphenated and capitalised + +- `@dataclass class HtmxHeaders` + - `def __bool__(self)` + - `def __init__(self, boosted, current_url, history_restore_request, prompt, request, target, trigger_name, trigger)` + +- `@dataclass class HttpHeader` + - `def __init__(self, k, v)` + +- `@use_kwargs_dict(**htmx_resps) def HtmxResponseHeaders(**kwargs)` + HTMX response headers + +- `def form2dict(form)` + Convert starlette form data to a dict + +- `def parse_form(req)` + Starlette errors on empty multipart forms, so this checks for that situation + +- `class JSONResponse` + Same as starlette's version, but auto-stringifies non serializable types + + - `def render(self, content)` + +- `def flat_xt(lst)` + Flatten lists + +- `class Beforeware` + - `def __init__(self, f, skip)` + +- `def EventStream(s)` + Create a text/event-stream response from `s` + +- `def flat_tuple(o)` + Flatten lists + +- `def noop_body(c, req)` + Default Body wrap function which just returns the content + +- `def respond(req, heads, bdy)` + Default FT response creation function + +- `class Redirect` + Use HTMX or Starlette RedirectResponse as required to redirect to `loc` + + - `def __init__(self, loc)` + - `def __response__(self, req)` + +- `def qp(p, **kw)` + Add parameters kw to path p + +- `def def_hdrs(htmx, surreal)` + Default headers for a FastHTML app + +- `class FastHTML` + - `def __init__(self, debug, routes, middleware, title, exception_handlers, on_startup, on_shutdown, lifespan, hdrs, ftrs, exts, before, after, surreal, htmx, default_hdrs, sess_cls, secret_key, session_cookie, max_age, sess_path, same_site, sess_https_only, sess_domain, key_fname, body_wrap, htmlkw, nb_hdrs, canonical, **bodykw)` + - `def add_route(self, route)` + +- `@patch def ws(self, path, conn, disconn, name, middleware)` + Add a websocket route at `path` + +- `def nested_name(f)` + Get name of function `f` using '_' to join nested function names + +- `@patch def route(self, path, methods, name, include_in_schema, body_wrap)` + Add a route at `path` + +- `def serve(appname, app, host, port, reload, reload_includes, reload_excludes)` + Run the app in an async server, with live reload set as the default. + +- `class Client` + A simple httpx ASGI client that doesn't require `async` + + - `def __init__(self, app, url)` + +- `class RouteFuncs` + - `def __init__(self)` + - `def __setattr__(self, name, value)` + - `def __getattr__(self, name)` + - `def __dir__(self)` + +- `class APIRouter` + Add routes to an app + + - `def __init__(self, prefix, body_wrap)` + - `def __call__(self, path, methods, name, include_in_schema, body_wrap)` + Add a route at `path` + + - `def __getattr__(self, name)` + - `def to_app(self, app)` + Add routes to `app` + + - `def ws(self, path, conn, disconn, name, middleware)` + Add a websocket route at `path` + + +- `def cookie(key, value, max_age, expires, path, domain, secure, httponly, samesite)` + Create a 'set-cookie' `HttpHeader` + +- `@patch def static_route_exts(self, prefix, static_path, exts)` + Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()` + +- `@patch def static_route(self, ext, prefix, static_path)` + Add a static route at URL path `prefix` with files from `static_path` and single `ext` (including the '.') + +- `class MiddlewareBase` + - `def __call__(self, scope, receive, send)` + +- `class FtResponse` + Wrap an FT response with any Starlette `Response` + + - `def __init__(self, content, status_code, headers, cls, media_type, background)` + - `def __response__(self, req)` + +## fasthtml.fastapp + +> The `fast_app` convenience wrapper + +- `def fast_app(db_file, render, hdrs, ftrs, tbls, before, middleware, live, debug, title, routes, exception_handlers, on_startup, on_shutdown, lifespan, default_hdrs, pico, surreal, htmx, exts, canonical, secret_key, key_fname, session_cookie, max_age, sess_path, same_site, sess_https_only, sess_domain, htmlkw, bodykw, reload_attempts, reload_interval, static_path, body_wrap, nb_hdrs, **kwargs)` + Create a FastHTML or FastHTMLWithLiveReload app. + +## fasthtml.js + +> Basic external Javascript lib wrappers + +- `def light_media(css)` + Render light media for day mode views + +- `def dark_media(css)` + Render dark media for night mode views + +- `def MarkdownJS(sel)` + Implements browser-based markdown rendering. + +- `def HighlightJS(sel, langs, light, dark)` + Implements browser-based syntax highlighting. Usage example [here](/tutorials/quickstart_for_web_devs.html#code-highlighting). + +- `def MermaidJS(sel, theme)` + Implements browser-based Mermaid diagram rendering. + +## fasthtml.jupyter + +> Use FastHTML in Jupyter notebooks + +- `def nb_serve(app, log_level, port, host, **kwargs)` + Start a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level` + +- `def nb_serve_async(app, log_level, port, host, **kwargs)` + Async version of `nb_serve` + +- `def is_port_free(port, host)` + Check if `port` is free on `host` + +- `def wait_port_free(port, host, max_wait)` + Wait for `port` to be free on `host` + +- `class JupyUvi` + Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level` + + - `def __init__(self, app, log_level, host, port, start, **kwargs)` + - `def start(self)` + - `def start_async(self)` + - `def stop(self)` + +- `class JupyUviAsync` + Start and stop an async Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level` + + - `def __init__(self, app, log_level, host, port, **kwargs)` + - `def start(self)` + - `def stop(self)` + +- `def HTMX(path, app, host, port, height, link, iframe)` + An iframe which displays the HTMX application in a notebook. + +## fasthtml.live_reload + +- `class FastHTMLWithLiveReload` + `FastHTMLWithLiveReload` enables live reloading. + This means that any code changes saved on the server will automatically + trigger a reload of both the server and browser window. + + How does it work? + - a websocket is created at `/live-reload` + - a small js snippet `LIVE_RELOAD_SCRIPT` is injected into each webpage + - this snippet connects to the websocket at `/live-reload` and listens for an `onclose` event + - when the `onclose` event is detected the browser is reloaded + + Why do we listen for an `onclose` event? + When code changes are saved the server automatically reloads if the --reload flag is set. + The server reload kills the websocket connection. The `onclose` event serves as a proxy + for "developer has saved some changes". + + Usage + >>> from fasthtml.common import * + >>> app = FastHTMLWithLiveReload() + + Run: + serve() + + - `def __init__(self, *args, **kwargs)` + +## fasthtml.oauth + +> Basic scaffolding for handling OAuth + +- `class GoogleAppClient` + A `WebApplicationClient` for Google oauth2 + + - `def __init__(self, client_id, client_secret, code, scope, project_id, **kwargs)` + - `@classmethod def from_file(cls, fname, code, scope, **kwargs)` + +- `class GitHubAppClient` + A `WebApplicationClient` for GitHub oauth2 + + - `def __init__(self, client_id, client_secret, code, scope, **kwargs)` + +- `class HuggingFaceClient` + A `WebApplicationClient` for HuggingFace oauth2 + + - `def __init__(self, client_id, client_secret, code, scope, state, **kwargs)` + +- `class DiscordAppClient` + A `WebApplicationClient` for Discord oauth2 + + - `def __init__(self, client_id, client_secret, is_user, perms, scope, **kwargs)` + - `def login_link(self, redirect_uri, scope, state)` + - `def parse_response(self, code, redirect_uri)` + +- `class Auth0AppClient` + A `WebApplicationClient` for Auth0 OAuth2 + + - `def __init__(self, domain, client_id, client_secret, code, scope, redirect_uri, **kwargs)` + - `def login_link(self, req)` + +- `@patch def login_link(self, redirect_uri, scope, state, **kwargs)` + Get a login link for this client + +- `def redir_url(request, redir_path, scheme)` + Get the redir url for the host in `request` + +- `@patch def parse_response(self, code, redirect_uri)` + Get the token from the oauth2 server response + +- `@patch def get_info(self, token)` + Get the info for authenticated user + +- `@patch def retr_info(self, code, redirect_uri)` + Combines `parse_response` and `get_info` + +- `@patch def retr_id(self, code, redirect_uri)` + Call `retr_info` and then return id/subscriber value + +- `class OAuth` + - `def __init__(self, app, cli, skip, redir_path, error_path, logout_path, login_path, https, http_patterns)` + - `def redir_login(self, session)` + - `def redir_url(self, req)` + - `def login_link(self, req, scope, state)` + - `def check_invalid(self, req, session, auth)` + - `def logout(self, session)` + - `def get_auth(self, info, ident, session, state)` + +- `@patch() def consent_url(self, proj)` + Get Google OAuth consent screen URL + +- `@patch def save(self, fname)` + Save credentials to `fname` + +- `def load_creds(fname)` + Load credentials from `fname` + +- `@patch def creds(self)` + Create `Credentials` from the client, refreshing if needed + +## fasthtml.pico + +> Basic components for generating Pico CSS tags + +- `@delegates(ft_hx, keep=True) def Card(*c, **kwargs)` + A PicoCSS Card, implemented as an Article with optional Header and Footer + +- `@delegates(ft_hx, keep=True) def Group(*c, **kwargs)` + A PicoCSS Group, implemented as a Fieldset with role 'group' + +- `@delegates(ft_hx, keep=True) def Search(*c, **kwargs)` + A PicoCSS Search, implemented as a Form with role 'search' + +- `@delegates(ft_hx, keep=True) def Grid(*c, **kwargs)` + A PicoCSS Grid, implemented as child Divs in a Div with class 'grid' + +- `@delegates(ft_hx, keep=True) def DialogX(*c, **kwargs)` + A PicoCSS Dialog, with children inside a Card + +- `@delegates(ft_hx, keep=True) def Container(*args, **kwargs)` + A PicoCSS Container, implemented as a Main with class 'container' + +## fasthtml.stripe_otp + +- `def create_price(app_nm, amt, currency)` + Create a product and bind it to a price object. If product already exist just return the price list. + +- `def archive_price(app_nm)` + Archive a price - useful for cleanup if testing. + +- `class Payment` + +## fasthtml.svg + +> Simple SVG FT elements + +- `def Svg(*args, **kwargs)` + An SVG tag; xmlns is added automatically, and viewBox defaults to height and width if not provided + +- `@delegates(ft_hx) def ft_svg(tag, *c, **kwargs)` + Create a standard `FT` element with some SVG-specific attrs + +- `@delegates(ft_svg) def Rect(width, height, x, y, fill, stroke, stroke_width, rx, ry, **kwargs)` + A standard SVG `rect` element + +- `@delegates(ft_svg) def Circle(r, cx, cy, fill, stroke, stroke_width, **kwargs)` + A standard SVG `circle` element + +- `@delegates(ft_svg) def Ellipse(rx, ry, cx, cy, fill, stroke, stroke_width, **kwargs)` + A standard SVG `ellipse` element + +- `def transformd(translate, scale, rotate, skewX, skewY, matrix)` + Create an SVG `transform` kwarg dict + +- `@delegates(ft_svg) def Line(x1, y1, x2, y2, stroke, w, stroke_width, **kwargs)` + A standard SVG `line` element + +- `@delegates(ft_svg) def Polyline(*args, **kwargs)` + A standard SVG `polyline` element + +- `@delegates(ft_svg) def Polygon(*args, **kwargs)` + A standard SVG `polygon` element + +- `@delegates(ft_svg) def Text(*args, **kwargs)` + A standard SVG `text` element + +- `class PathFT` + - `def M(self, x, y)` + Move to. + + - `def L(self, x, y)` + Line to. + + - `def H(self, x)` + Horizontal line to. + + - `def V(self, y)` + Vertical line to. + + - `def Z(self)` + Close path. + + - `def C(self, x1, y1, x2, y2, x, y)` + Cubic Bézier curve. + + - `def S(self, x2, y2, x, y)` + Smooth cubic Bézier curve. + + - `def Q(self, x1, y1, x, y)` + Quadratic Bézier curve. + + - `def T(self, x, y)` + Smooth quadratic Bézier curve. + + - `def A(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y)` + Elliptical Arc. + + +- `def SvgOob(*args, **kwargs)` + Wraps an SVG shape as required for an HTMX OOB swap + +- `def SvgInb(*args, **kwargs)` + Wraps an SVG shape as required for an HTMX inband swap + +## fasthtml.xtend + +> Simple extensions to standard HTML components, such as adding sensible defaults + +- `@delegates(ft_hx, keep=True) def A(*c, **kwargs)` + An A tag; `href` defaults to '#' for more concise use with HTMX + +- `@delegates(ft_hx, keep=True) def AX(txt, hx_get, target_id, hx_swap, href, **kwargs)` + An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be positional params + +- `@delegates(ft_hx, keep=True) def Form(*c, **kwargs)` + A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'` + +- `@delegates(ft_hx, keep=True) def Hidden(value, id, **kwargs)` + An Input of type 'hidden' + +- `@delegates(ft_hx, keep=True) def CheckboxX(checked, label, value, id, name, **kwargs)` + A Checkbox optionally inside a Label, preceded by a `Hidden` with matching name + +- `@delegates(ft_html, keep=True) def Script(code, **kwargs)` + A Script tag that doesn't escape its code + +- `@delegates(ft_html, keep=True) def Style(*c, **kwargs)` + A Style tag that doesn't escape its code + +- `def double_braces(s)` + Convert single braces to double braces if next to special chars or newline + +- `def undouble_braces(s)` + Convert double braces to single braces if next to special chars or newline + +- `def loose_format(s, **kw)` + String format `s` using `kw`, without being strict about braces outside of template params + +- `def ScriptX(fname, src, nomodule, type, _async, defer, charset, crossorigin, integrity, **kw)` + A `script` element with contents read from `fname` + +- `def replace_css_vars(css, pre, **kwargs)` + Replace `var(--)` CSS variables with `kwargs` if name prefix matches `pre` + +- `def StyleX(fname, **kw)` + A `style` element with contents read from `fname` and variables replaced from `kw` + +- `def Nbsp()` + A non-breaking space + +- `def Surreal(code)` + Wrap `code` in `domReadyExecute` and set `m=me()` and `p=me('-')` + +- `def On(code, event, sel, me)` + An async surreal.js script block event handler for `event` on selector `sel,p`, making available parent `p`, event `ev`, and target `e` + +- `def Prev(code, event)` + An async surreal.js script block event handler for `event` on previous sibling, with same vars as `On` + +- `def Now(code, sel)` + An async surreal.js script block on selector `me(sel)` + +- `def AnyNow(sel, code)` + An async surreal.js script block on selector `any(sel)` + +- `def run_js(js, id, **kw)` + Run `js` script, auto-generating `id` based on name of caller if needed, and js-escaping any `kw` params + +- `def jsd(org, repo, root, path, prov, typ, ver, esm, **kwargs)` + jsdelivr `Script` or CSS `Link` tag, or URL + +- `class Fragment` + An empty tag, used as a container + + - `def __init__(self, *c)` + +- `@delegates(ft_hx, keep=True) def Titled(title, *args, **kwargs)` + An HTML partial containing a `Title`, and `H1`, and any provided children + +- `def Socials(title, site_name, description, image, url, w, h, twitter_site, creator, card)` + OG and Twitter social card headers + +- `def YouTubeEmbed(video_id, **kwargs)` + Embed a YouTube video + +- `def Favicon(light_icon, dark_icon)` + Light and dark favicon headers + diff --git a/docs/explains/CreateWebhook.png b/docs/explains/CreateWebhook.png new file mode 100644 index 0000000000000000000000000000000000000000..315190d4a1a2d892182d07d1313588d8987e30dd --- /dev/null +++ b/docs/explains/CreateWebhook.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc00255c1d2636a143401e11010c09fef484326c9b622b8ae88904179824f269 +size 594213 diff --git a/docs/explains/CreateWebhook2.png b/docs/explains/CreateWebhook2.png new file mode 100644 index 0000000000000000000000000000000000000000..59cd84cacb0edc55f90951181e8a6ca7980781cb --- /dev/null +++ b/docs/explains/CreateWebhook2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66ea8d813be5f44528a242bc24c7b42a4a623ac93a06a9147e0f6576438f4278 +size 661282 diff --git a/docs/explains/StripeDashboard_API_Key.png b/docs/explains/StripeDashboard_API_Key.png new file mode 100644 index 0000000000000000000000000000000000000000..c3207b6b28318e5c98c81747be612fcfb3c2b775 --- /dev/null +++ b/docs/explains/StripeDashboard_API_Key.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c6b40ee5de742a306c9287cbb2e0145a9d8ae2d5e833d5410e1406004d338dc +size 265685 diff --git a/docs/explains/StripePaymentPage.jpg b/docs/explains/StripePaymentPage.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9f0eb789193f59cce46082c1b76fcbefba072d2e --- /dev/null +++ b/docs/explains/StripePaymentPage.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77fc0f66b3acdad6990d3d1ea6c9203692d64d4fe51904adeaea368e32f14a6c +size 179411 diff --git a/docs/explains/SubscriptionEvents.png b/docs/explains/SubscriptionEvents.png new file mode 100644 index 0000000000000000000000000000000000000000..9acc1b553994617b54adf93816b1da49c3776133 Binary files /dev/null and b/docs/explains/SubscriptionEvents.png differ diff --git a/docs/explains/background_tasks.html b/docs/explains/background_tasks.html new file mode 100644 index 0000000000000000000000000000000000000000..3f1dc2b4f0605e4aacaa1c41f75e8afa0c0a3559 --- /dev/null +++ b/docs/explains/background_tasks.html @@ -0,0 +1,1097 @@ + + + + + + + + + + +Background Tasks – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Background Tasks

+
+ +
+
+ Background tasks are functions run after handlers return a response. +
+
+ + +
+ + + + +
+ + + +
+ + + +

Useful for operations where the user gets a response quickly but doesn’t need to wait for the operation to finish. Typical scenarios include:

+
    +
  • User setup in complex systems where you can inform the user and other people later in email that their account is complete
  • +
  • Batch processes that can take a significant amount of time (bulk email or API calls)
  • +
  • Any other process where the user can be notified later by email, websocket, webhook, or pop-up
  • +
+
+
+
+ +
+
+Note +
+
+
+

Background tasks in FastHTML are built on Starlette’s background tasks, with added sugar. Starlette’s background task design is an easy-to-use wrapper around Python’s async and threading libraries. Background tasks make apps snappier to the end user and generally improve an app’s speed.

+
+
+
+

A simple background task example

+

In this example we are attaching a task to FtResponse by assigning it via the background argument. When the page is visited, it will display ‘Simple Background Task Example’ almost instantly, while in the terminal it will slowly count upward from 0.

+
+
+
main.py
+
+
from fasthtml.common import *
+from starlette.background import BackgroundTask
+from time import sleep
+
+app, rt = fast_app()
+
+1def counter(loops:int):
+    "Slowly print integers to the terminal"
+    for i in range(loops):
+        print(i)
+        sleep(i)
+
+@rt
+def index():
+2    task = BackgroundTask(counter, loops=5)
+3    return Titled('Simple Background Task Example'), task
+
+serve()
+
+
+
1
+
+counter is our task function. There is nothing special about it, although it is a good practice for its arguments to be serializable as JSON +
+
2
+
+We use starlette.background.BackgroundTask to turn counter() into a background task +
+
3
+
+To add a background task to a handler, we add it to the return values at the top level of the response. +
+
+
+
+

A more realistic example

+

Let’s imagine that we are accessing a slow-to-process critical service. We don’t want our users to have to wait. While we could set up SSE to notify on completion, instead we decide to periodically check to see if the status of their record has changed.

+
+

Simulated Slow API Service

+

First, create a very simple slow timestamp API. All it does is stall requests for a few seconds before returning JSON containing timestamps.

+
# slow_api.py
+from fasthtml.common import *
+from time import sleep, time
+
+app, rt = fast_app()
+
+@rt('/slow')
+def slow(ts: int):
+1    sleep(3)
+2    return dict(request_time=ts, response_time=int(time()))
+
+serve(port=8123)
+
+
1
+
+This represents slow processing. +
+
2
+
+Returns both the task’s original timestamp and the time after completion +
+
+
+
+

Main FastHTML app

+

Now let’s create a user-facing app that uses this API to fetch the timestamp from the glacially slow service.

+
# main.py
+from fasthtml.common import *
+from starlette.background import BackgroundTask
+import time
+import httpx
+
+app, rt = fast_app()
+
+db = database(':memory:')
+
+1class TStamp: request_time: int; response_time: int
+
+tstamps = db.create(TStamp, pk='request_time')
+
+2def task_submit(request_time: int):
+    client = httpx.Client()
+3    response = client.post(f'http://127.0.0.1:8123/slow?ts={request_time}')
+4    tstamps.insert(**response.json())
+
+@rt
+def submit():
+    "Route that initiates a background task and returns immediately."
+    request_time = int(time.time())
+5    task = BackgroundTask(task_submit, request_time=request_time)
+6    return P(f'Request submitted at: {request_time}'), task
+
+@rt
+7def show_tstamps(): return Ul(map(Li, tstamps()))
+
+@rt
+def index():
+    return Titled('Background Task Dashboard',
+8        P(Button('Press to call slow service',
+            hx_post=submit, hx_target='#res')),
+        H2('Responses from Tasks'),
+        P('', id='res'),
+        Div(Ul(map(Li, tstamps())),
+9            hx_get=show_tstamps, hx_trigger='every 5s'),
+    )
+
+serve()
+
+
1
+
+Tracks when requests are sent and responses received +
+
2
+
+Task function calling slow service to be run in the background of a route handler. It is common but not necessary to prefix task functions with ‘task_’ +
+
3
+
+Call the slow API service (simulating a time-consuming operation) +
+
4
+
+Store both timestamps in our database +
+
5
+
+Create a background task by passing in the function to a BackgroundTask object, followed by any arguments. +
+
6
+
+In FtResponse, use the background keyword argument to set the task to be run after the HTTP response is generated. +
+
7
+
+Endpoint that displays all recorded timestamp pairs. +
+
8
+
+When this button is pressed, the ‘submit’ handler will respond instantly. The task_submit function will insert the slow API response into the db later. +
+
9
+
+Every 5 seconds get the tstamps stored in the DB. +
+
+
+
+
+ +
+
+Tip +
+
+
+

In the example above we use a synchronous background task function set in the FtResponse of a synchronous handler. However, we can also use asynchronous functions and handlers.

+
+
+
+
+
+

Multiple background tasks in a handler

+

It is possible to add multiple background tasks to an FtResponse.

+
+
+
+ +
+
+Warning +
+
+
+

Multiple background tasks on a background task are executed in order. In the case a task raises an exception, following tasks will not get the opportunity to be executed.

+
+
+
from starlette.background import BackgroundTasks
+
+@rt
+async def signup(email, username):
+    tasks = BackgroundTasks()
+    tasks.add_task(send_welcome_email, to_address=email)
+    tasks.add_task(send_admin_notification, username=username)
+    return Titled('Signup successful!'), tasks
+
+async def send_welcome_email(to_address):
+    ...
+
+async def send_admin_notification(username):
+    ...
+
+
+

Background tasks at scale

+

Background tasks enhance application performance both for users and apps by handling blocking processes asynchronously, even when defined as synchronous functions.

+

When FastHTML’s background tasks aren’t enough and your app runs slow on a server, manually offloading processes to the multiprocessing library is an option. By doing so you can leverage multiple cores and bypass the GIL, significantly improving speed and performance at the cost of added complexity.

+

Sometimes a server reaches its processing limits, and this is where distributed task queue systems like Celery and Dramatiq come into play. They are designed to distribute tasks across multiple servers, offering improved observability, retry mechanisms, and persistence, at the cost of substantially increased complexity.

+

However most applications work well with built-in background tasks like those in FastHTML, which we recommend trying first. Writing these functions with JSON-serializable arguments ensures straightforward conversion to other concurrency methods if needed.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/explains/background_tasks.html.md b/docs/explains/background_tasks.html.md new file mode 100644 index 0000000000000000000000000000000000000000..e18d418963a72bc379c26f04c32237626a353f71 --- /dev/null +++ b/docs/explains/background_tasks.html.md @@ -0,0 +1,254 @@ +# Background Tasks + + + + +Useful for operations where the user gets a response quickly but doesn’t +need to wait for the operation to finish. Typical scenarios include: + +- User setup in complex systems where you can inform the user and other + people later in email that their account is complete +- Batch processes that can take a significant amount of time (bulk email + or API calls) +- Any other process where the user can be notified later by email, + websocket, webhook, or pop-up + +
+ +> **Note** +> +> Background tasks in FastHTML are built on Starlette’s background +> tasks, with added sugar. Starlette’s background task design is an +> easy-to-use wrapper around Python’s async and threading libraries. +> Background tasks make apps snappier to the end user and generally +> improve an app’s speed. + +
+ +## A simple background task example + +In this example we are attaching a task to FtResponse by assigning it +via the background argument. When the page is visited, it will display +‘Simple Background Task Example’ almost instantly, while in the terminal +it will slowly count upward from 0. + +
+ +**main.py** + +``` python +from fasthtml.common import * +from starlette.background import BackgroundTask +from time import sleep + +app, rt = fast_app() + +def counter(loops:int): + "Slowly print integers to the terminal" + for i in range(loops): + print(i) + sleep(i) + +@rt +def index(): + task = BackgroundTask(counter, loops=5) + return Titled('Simple Background Task Example'), task + +serve() +``` + +
+ +Line 7 +`counter` is our task function. There is nothing special about it, +although it is a good practice for its arguments to be serializable as +JSON + +Line 15 +We use `starlette.background.BackgroundTask` to turn `counter()` into a +background task + +Line 16 +To add a background task to a handler, we add it to the return values at +the top level of the response. + +## A more realistic example + +Let’s imagine that we are accessing a slow-to-process critical service. +We don’t want our users to have to wait. While we could set up SSE to +notify on completion, instead we decide to periodically check to see if +the status of their record has changed. + +### Simulated Slow API Service + +First, create a very simple slow timestamp API. All it does is stall +requests for a few seconds before returning JSON containing timestamps. + +``` python +# slow_api.py +from fasthtml.common import * +from time import sleep, time + +app, rt = fast_app() + +@rt('/slow') +def slow(ts: int): + sleep(3) + return dict(request_time=ts, response_time=int(time())) + +serve(port=8123) +``` + +Line 9 +This represents slow processing. + +Line 10 +Returns both the task’s original timestamp and the time after completion + +### Main FastHTML app + +Now let’s create a user-facing app that uses this API to fetch the +timestamp from the glacially slow service. + +``` python +# main.py +from fasthtml.common import * +from starlette.background import BackgroundTask +import time +import httpx + +app, rt = fast_app() + +db = database(':memory:') + +class TStamp: request_time: int; response_time: int + +tstamps = db.create(TStamp, pk='request_time') + +def task_submit(request_time: int): + client = httpx.Client() + response = client.post(f'http://127.0.0.1:8123/slow?ts={request_time}') + tstamps.insert(**response.json()) + +@rt +def submit(): + "Route that initiates a background task and returns immediately." + request_time = int(time.time()) + task = BackgroundTask(task_submit, request_time=request_time) + return P(f'Request submitted at: {request_time}'), task + +@rt +def show_tstamps(): return Ul(map(Li, tstamps())) + +@rt +def index(): + return Titled('Background Task Dashboard', + P(Button('Press to call slow service', + hx_post=submit, hx_target='#res')), + H2('Responses from Tasks'), + P('', id='res'), + Div(Ul(map(Li, tstamps())), + hx_get=show_tstamps, hx_trigger='every 5s'), + ) + +serve() +``` + +Line 11 +Tracks when requests are sent and responses received + +Line 15 +Task function calling slow service to be run in the background of a +route handler. It is common but not necessary to prefix task functions +with ‘task\_’ + +Line 17 +Call the slow API service (simulating a time-consuming operation) + +Line 18 +Store both timestamps in our database + +Line 24 +Create a background task by passing in the function to a BackgroundTask +object, followed by any arguments. + +Line 25 +In FtResponse, use the background keyword argument to set the task to be +run after the HTTP response is generated. + +Line 28 +Endpoint that displays all recorded timestamp pairs. + +Line 33 +When this button is pressed, the ‘submit’ handler will respond +instantly. The task_submit function will insert the slow API response +into the db later. + +Line 38 +Every 5 seconds get the tstamps stored in the DB. + +
+ +> **Tip** +> +> In the example above we use a synchronous background task function set +> in the +> [`FtResponse`](https://www.fastht.ml/docs/api/core.html#ftresponse) of +> a synchronous handler. However, we can also use asynchronous functions +> and handlers. + +
+ +## Multiple background tasks in a handler + +It is possible to add multiple background tasks to an FtResponse. + +
+ +> **Warning** +> +> Multiple background tasks on a background task are executed in order. +> In the case a task raises an exception, following tasks will not get +> the opportunity to be executed. + +
+ +``` python +from starlette.background import BackgroundTasks + +@rt +async def signup(email, username): + tasks = BackgroundTasks() + tasks.add_task(send_welcome_email, to_address=email) + tasks.add_task(send_admin_notification, username=username) + return Titled('Signup successful!'), tasks + +async def send_welcome_email(to_address): + ... + +async def send_admin_notification(username): + ... +``` + +## Background tasks at scale + +Background tasks enhance application performance both for users and apps +by handling blocking processes asynchronously, even when defined as +synchronous functions. + +When FastHTML’s background tasks aren’t enough and your app runs slow on +a server, manually offloading processes to the `multiprocessing` library +is an option. By doing so you can leverage multiple cores and bypass the +GIL, significantly improving speed and performance at the cost of added +complexity. + +Sometimes a server reaches its processing limits, and this is where +distributed task queue systems like Celery and Dramatiq come into play. +They are designed to distribute tasks across multiple servers, offering +improved observability, retry mechanisms, and persistence, at the cost +of substantially increased complexity. + +However most applications work well with built-in background tasks like +those in FastHTML, which we recommend trying first. Writing these +functions with JSON-serializable arguments ensures straightforward +conversion to other concurrency methods if needed. diff --git a/docs/explains/explaining_xt_components.html b/docs/explains/explaining_xt_components.html new file mode 100644 index 0000000000000000000000000000000000000000..f71f8359ed575788d67d46fd66ef3c540ea4d523 --- /dev/null +++ b/docs/explains/explaining_xt_components.html @@ -0,0 +1,1029 @@ + + + + + + + + + + +FT Components – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

FT Components

+
+ +
+
+ FT components turn Python objects into HTML. +
+
+ + +
+ + + + +
+ + + +
+ + + +

FT, or ‘FastTags’, are the display components of FastHTML. In fact, the word “components” in the context of FastHTML is often synonymous with FT.

+

For example, when we look at a FastHTML app, in particular the views, as well as various functions and other objects, we see something like the code snippet below. It’s the return statement that we want to pay attention to:

+
+
from fasthtml.common import *
+
+def example():
+    # The code below is a set of ft components
+    return Div(
+            H1("FastHTML APP"),
+            P("Let's do this"),
+            cls="go"
+    )
+
+

Let’s go ahead and call our function and print the result:

+
+
example()
+
+
<div class="go">
+  <h1>FastHTML APP</h1>
+  <p>Let&#x27;s do this</p>
+</div>
+
+
+

As you can see, when returned to the user from a Python callable, like a function, the ft components are transformed into their string representations of XML or XML-like content such as HTML. More concisely, ft turns Python objects into HTML.

+

Now that we know what ft components look and behave like we can begin to understand them. At their most fundamental level, ft components:

+
    +
  1. Are Python callables, specifically functions, classes, methods of classes, lambda functions, and anything else called with parenthesis that returns a value.
  2. +
  3. Return a sequence of values which has three elements: +
      +
    1. The tag to be generated
    2. +
    3. The content of the tag, which is a tuple of strings/tuples. If a tuple, it is the three-element structure of an ft component
    4. +
    5. A dictionary of XML attributes and their values
    6. +
  4. +
  5. FastHTML’s default ft components words begin with an uppercase letter. Examples include Title(), Ul(), and Div() Custom components have included things like BlogPost and CityMap.
  6. +
+
+

How FastHTML names ft components

+

When it comes to naming ft components, FastHTML appears to break from PEP8. Specifically, PEP8 specifies that when naming variables, functions and instantiated classes we use the snake_case_pattern. That is to say, lowercase with words separated by underscores. However, FastHTML uses PascalCase for ft components.

+

There’s a couple of reasons for this:

+
    +
  1. ft components can be made from any callable type, so adhering to any one pattern doesn’t make much sense
  2. +
  3. It makes for easier reading of FastHTML code, as anything that is PascalCase is probably an ft component
  4. +
+
+
+

Default FT components

+

FastHTML has over 150 FT components designed to accelerate web development. Most of these mirror HTML tags such as <div>, <p>, <a>, <title>, and more. However, there are some extra tags added, including:

+
    +
  • Titled, a combination of the Title() and H1() tags
  • +
  • Socials, renders popular social media tags
  • +
+
+
+

The fasthtml.ft Namespace

+

Some people prefer to write code using namespaces while adhering to PEP8. If that’s a preference, projects can be coded using the fasthtml.ft namespace.

+
+
from fasthtml import ft
+
+ft.Ul(
+    ft.Li("one"),
+    ft.Li("two"),
+    ft.Li("three")
+)
+
+
<ul>
+  <li>one</li>
+  <li>two</li>
+  <li>three</li>
+</ul>
+
+
+
+
+

Attributes

+

This example demonstrates many important things to know about how ft components handle attributes.

+
#| echo: False
+1Label(
+    "Choose an option", 
+    Select(
+2        Option("one", value="1", selected=True),
+3        Option("two", value="2", selected=False),
+4        Option("three", value=3),
+5        cls="selector",
+6        _id="counter",
+7        **{'@click':"alert('Clicked');"},
+    ),
+8    _for="counter",
+)
+
+
1
+
+Line 2 demonstrates that FastHTML appreciates Labels surrounding their fields. +
+
2
+
+On line 5, we can see that attributes set to the boolean value of True are rendered with just the name of the attribute. +
+
3
+
+On line 6, we demonstrate that attributes set to the boolean value of False do not appear in the rendered output. +
+
4
+
+Line 7 is an example of how integers and other non-string values in the rendered output are converted to strings. +
+
5
+
+Line 8 is where we set the HTML class using the cls argument. We use cls here as class is a reserved word in Python. During the rendering process this will be converted to the word “class”. +
+
6
+
+Line 9 demonstrates that any named argument passed into an ft component will have the leading underscore stripped away before rendering. Useful for handling reserved words in Python. +
+
7
+
+On line 10 we have an attribute name that cannot be represented as a python variable. In cases like these, we can use an unpacked dict to represent these values. +
+
8
+
+The use of _for on line 12 is another demonstration of an argument having the leading underscore stripped during render. We can also use fr as that will be expanded to for. +
+
+

This renders the following HTML snippet:

+
+
Label(
+    "Choose an option", 
+    Select(
+        Option("one", value="1", selected=True),
+        Option("two", value="2", selected=False),
+        Option("three", value=3),  # <4>,
+        cls="selector",
+        _id="counter",
+        **{'@click':"alert('Clicked');"},
+    ),
+    _for="counter",
+)
+
+
<label for="counter">
+Choose an option
+  <select id="counter" @click="alert(&#x27;Clicked&#x27;);" class="selector" name="counter">
+    <option value="1" selected>one</option>
+    <option value="2" >two</option>
+    <option value="3">three</option>
+  </select>
+</label>
+
+
+
+
+

Defining new ft components

+

It is possible and sometimes useful to create your own ft components that generate non-standard tags that are not in the FastHTML library. FastHTML supports created and defining those new tags flexibly.

+

For more information, see the Defining new ft components reference page.

+
+
+

FT components and type hints

+

If you use type hints, we strongly suggest that FT components be treated as the Any type.

+

The reason is that FastHTML leverages python’s dynamic features to a great degree. Especially when it comes to FT components, which can evaluate out to be FT|str|None|tuple as well as anything that supports the __ft__, __html__, and __str__ method. That’s enough of the Python stack that assigning anything but Any to be the FT type will prove an exercise in frustation.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/explains/explaining_xt_components.html.md b/docs/explains/explaining_xt_components.html.md new file mode 100644 index 0000000000000000000000000000000000000000..d8eb6189772e91e4ca91c91f1878b3e43a682b30 --- /dev/null +++ b/docs/explains/explaining_xt_components.html.md @@ -0,0 +1,215 @@ +# **FT** Components + + + + +**FT**, or ‘FastTags’, are the display components of FastHTML. In fact, +the word “components” in the context of FastHTML is often synonymous +with **FT**. + +For example, when we look at a FastHTML app, in particular the views, as +well as various functions and other objects, we see something like the +code snippet below. It’s the `return` statement that we want to pay +attention to: + +``` python +from fasthtml.common import * + +def example(): + # The code below is a set of ft components + return Div( + H1("FastHTML APP"), + P("Let's do this"), + cls="go" + ) +``` + +Let’s go ahead and call our function and print the result: + +``` python +example() +``` + +``` xml +
+

FastHTML APP

+

Let's do this

+
+``` + +As you can see, when returned to the user from a Python callable, like a +function, the ft components are transformed into their string +representations of XML or XML-like content such as HTML. More concisely, +*ft turns Python objects into HTML*. + +Now that we know what ft components look and behave like we can begin to +understand them. At their most fundamental level, ft components: + +1. Are Python callables, specifically functions, classes, methods of + classes, lambda functions, and anything else called with parenthesis + that returns a value. +2. Return a sequence of values which has three elements: + 1. The tag to be generated + 2. The content of the tag, which is a tuple of strings/tuples. If a + tuple, it is the three-element structure of an ft component + 3. A dictionary of XML attributes and their values +3. FastHTML’s default ft components words begin with an uppercase + letter. Examples include `Title()`, `Ul()`, and `Div()` Custom + components have included things like `BlogPost` and `CityMap`. + +## How FastHTML names ft components + +When it comes to naming ft components, FastHTML appears to break from +PEP8. Specifically, PEP8 specifies that when naming variables, functions +and instantiated classes we use the `snake_case_pattern`. That is to +say, lowercase with words separated by underscores. However, FastHTML +uses `PascalCase` for ft components. + +There’s a couple of reasons for this: + +1. ft components can be made from any callable type, so adhering to any + one pattern doesn’t make much sense +2. It makes for easier reading of FastHTML code, as anything that is + PascalCase is probably an ft component + +## Default **FT** components + +FastHTML has over 150 **FT** components designed to accelerate web +development. Most of these mirror HTML tags such as `
`, `

`, +``, ``, and more. However, there are some extra tags added, +including: + +- [`Titled`](https://www.fastht.ml/docs/api/xtend.html#titled), a + combination of the `Title()` and `H1()` tags +- [`Socials`](https://www.fastht.ml/docs/api/xtend.html#socials), + renders popular social media tags + +## The `fasthtml.ft` Namespace + +Some people prefer to write code using namespaces while adhering to +PEP8. If that’s a preference, projects can be coded using the +`fasthtml.ft` namespace. + +``` python +from fasthtml import ft + +ft.Ul( + ft.Li("one"), + ft.Li("two"), + ft.Li("three") +) +``` + +``` xml +<ul> + <li>one</li> + <li>two</li> + <li>three</li> +</ul> +``` + +## Attributes + +This example demonstrates many important things to know about how ft +components handle attributes. + +``` python +#| echo: False +Label( + "Choose an option", + Select( + Option("one", value="1", selected=True), + Option("two", value="2", selected=False), + Option("three", value=3), + cls="selector", + _id="counter", + **{'@click':"alert('Clicked');"}, + ), + _for="counter", +) +``` + +Line 2 +Line 2 demonstrates that FastHTML appreciates `Label`s surrounding their +fields. + +Line 5 +On line 5, we can see that attributes set to the `boolean` value of +`True` are rendered with just the name of the attribute. + +Line 6 +On line 6, we demonstrate that attributes set to the `boolean` value of +`False` do not appear in the rendered output. + +Line 7 +Line 7 is an example of how integers and other non-string values in the +rendered output are converted to strings. + +Line 8 +Line 8 is where we set the HTML class using the `cls` argument. We use +`cls` here as `class` is a reserved word in Python. During the rendering +process this will be converted to the word “class”. + +Line 9 +Line 9 demonstrates that any named argument passed into an ft component +will have the leading underscore stripped away before rendering. Useful +for handling reserved words in Python. + +Line 10 +On line 10 we have an attribute name that cannot be represented as a +python variable. In cases like these, we can use an unpacked `dict` to +represent these values. + +Line 12 +The use of `_for` on line 12 is another demonstration of an argument +having the leading underscore stripped during render. We can also use +`fr` as that will be expanded to `for`. + +This renders the following HTML snippet: + +``` python +Label( + "Choose an option", + Select( + Option("one", value="1", selected=True), + Option("two", value="2", selected=False), + Option("three", value=3), # <4>, + cls="selector", + _id="counter", + **{'@click':"alert('Clicked');"}, + ), + _for="counter", +) +``` + +``` xml +<label for="counter"> +Choose an option + <select id="counter" @click="alert('Clicked');" class="selector" name="counter"> + <option value="1" selected>one</option> + <option value="2" >two</option> + <option value="3">three</option> + </select> +</label> +``` + +## Defining new ft components + +It is possible and sometimes useful to create your own ft components +that generate non-standard tags that are not in the FastHTML library. +FastHTML supports created and defining those new tags flexibly. + +For more information, see the [Defining new ft +components](../ref/defining_xt_component.html) reference page. + +## FT components and type hints + +If you use type hints, we strongly suggest that FT components be treated +as the `Any` type. + +The reason is that FastHTML leverages python’s dynamic features to a +great degree. Especially when it comes to `FT` components, which can +evaluate out to be `FT|str|None|tuple` as well as anything that supports +the `__ft__`, `__html__`, and `__str__` method. That’s enough of the +Python stack that assigning anything but `Any` to be the FT type will +prove an exercise in frustation. diff --git a/docs/explains/faq.html b/docs/explains/faq.html new file mode 100644 index 0000000000000000000000000000000000000000..e087add8fc42226d0861d79d2f25089d77db1fec --- /dev/null +++ b/docs/explains/faq.html @@ -0,0 +1,934 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head> + +<meta charset="utf-8"> +<meta name="generator" content="quarto-1.6.40"> + +<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> + +<meta name="description" content="Frequently Asked Questions"> + +<title>FAQ – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

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

FAQ

+
+ +
+
+ Frequently Asked Questions +
+
+ + +
+ + + + +
+ + + +
+ + + +
+

Why does my editor say that I have errors in my FastHTML code?

+

Many editors, including Visual Studio Code, use PyLance to provide error checking for Python. However, PyLance’s error checking is just a guess – it can’t actually know whether your code is correct or not. PyLance particularly struggles with FastHTML’s syntax, which leads to it often reporting false error messages in FastHTML projects.

+

To avoid these misleading error messages, it’s best to disable some PyLance error checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the same approach should also work in other editors based on vscode, such as Cursor and GitHub Codespaces):

+
    +
  1. Open your FastHTML project
  2. +
  3. Press Ctrl+Shift+P (or Cmd+Shift+P on Mac) to open the Command Palette
  4. +
  5. Type “Preferences: Open Workspace Settings (JSON)” and select it
  6. +
  7. In the JSON file that opens, add the following lines:
  8. +
+
{
+ "python.analysis.diagnosticSeverityOverrides": {
+      "reportGeneralTypeIssues": "none",
+      "reportOptionalMemberAccess": "none",
+      "reportWildcardImportFromLibrary": "none",
+      "reportRedeclaration": "none",
+      "reportAttributeAccessIssue": "none",
+      "reportInvalidTypeForm": "none",
+      "reportAssignmentType": "none",
+  }
+}
+
    +
  1. Save the file
  2. +
+

Even with PyLance diagnostics turned off, your FastHTML code will still run correctly. If you’re still seeing some false errors from PyLance, you can disable it entirely by adding this to your settings:

+
{
+  "python.analysis.ignore": [  "*"  ]
+}
+
+
+

Why the distinctive coding style?

+

FastHTML coding style is the fastai coding style.

+

If you are coming from a data science background the fastai coding style may already be your preferred style.

+

If you are coming from a PEP-8 background where the use of ruff is encouraged, there is a learning curve. However, once you get used to the fastai coding style you may discover yourself appreciating the concise nature of this style. It also encourages using more functional programming tooling, which is both productive and fun. Having said that, it’s entirely optional!

+
+
+

Why not JSX?

+

Many have asked! We think there’s no benefit… Python’s positional and kw args precisely 1:1 map already to html/xml children and attrs, so there’s no need for a new syntax.

+

We wrote some more thoughts on Why Python HTML components over Jinja2, Mako, or JSX here.

+
+
+

Why use import *

+

First, through the use of the __all__ attribute in our Python modules we control what actually gets imported. So there’s no risk of namespace pollution.

+

Second, our style lends itself to working in rather compact Jupyter notebooks and small Python modules. Hence we know about the source code whose libraries we import * from. This terseness means we can develop faster. We’re a small team, and any edge we can gain is important to us.

+

Third, for external libraries, be it core Python, SQLAlchemy, or other things we do tend to use explicit imports. In part to avoid namespace collisions, and also as reference to know where things are coming from.

+

We’ll finish by saying a lot of our users employ explicit imports. If that’s the path you want to take, we encourage the use of from fasthtml import common as fh. The acronym of fh makes it easy to recognize that a symbol is from the FastHTML library.

+
+
+

Can FastHTML be used for dashboards?

+

Yes it can. In fact, it excels at building dashboards. In addition to being great for building static dashboards, because of its foundation in ASGI and tech stack, FastHTML natively supports Websockets. That means using FastHTML we can create dashboards that autoupdate.

+
+
+

Why is FastHTML developed using notebooks?

+

Some people are under the impression that writing software in notebooks is bad.

+

Watch this video. We’ve used Jupyter notebooks exported via nbdev to write a wide range of “very serious” software projects over the last three years. This includes deep learning libraries, API clients, Python language extensions, terminal user interfaces, web frameworks, and more!

+

nbdev is a Jupyter-powered tool for writing software. Traditional programming environments throw away the result of your exploration in REPLs or notebooks. nbdev makes exploration an integral part of your workflow, all while promoting software engineering best practices.

+
+
+

Why not pyproject.toml for packaging?

+

FastHTML uses a setup.py module instead of a pyproject.toml file to configure itself for installation. The reason for this is pyproject.toml is not compatible with nbdev, which is what is used to write and build FastHTML.

+

The nbdev project spent around a year trying to move to pyproject.toml but there was insufficient functionality in the toml-based approach to complete the transition.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/explains/faq.html.md b/docs/explains/faq.html.md new file mode 100644 index 0000000000000000000000000000000000000000..b9bfa8329c6beff505793c8a883abca8b1da0a21 --- /dev/null +++ b/docs/explains/faq.html.md @@ -0,0 +1,133 @@ +# FAQ + + + + +## Why does my editor say that I have errors in my FastHTML code? + +Many editors, including Visual Studio Code, use PyLance to provide error +checking for Python. However, PyLance’s error checking is just a guess – +it can’t actually know whether your code is correct or not. PyLance +particularly struggles with FastHTML’s syntax, which leads to it often +reporting false error messages in FastHTML projects. + +To avoid these misleading error messages, it’s best to disable some +PyLance error checking in your FastHTML projects. Here’s how to do it in +Visual Studio Code (the same approach should also work in other editors +based on vscode, such as Cursor and GitHub Codespaces): + +1. Open your FastHTML project +2. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) to open the Command + Palette +3. Type “Preferences: Open Workspace Settings (JSON)” and select it +4. In the JSON file that opens, add the following lines: + +``` json +{ + "python.analysis.diagnosticSeverityOverrides": { + "reportGeneralTypeIssues": "none", + "reportOptionalMemberAccess": "none", + "reportWildcardImportFromLibrary": "none", + "reportRedeclaration": "none", + "reportAttributeAccessIssue": "none", + "reportInvalidTypeForm": "none", + "reportAssignmentType": "none", + } +} +``` + +5. Save the file + +Even with PyLance diagnostics turned off, your FastHTML code will still +run correctly. If you’re still seeing some false errors from PyLance, +you can disable it entirely by adding this to your settings: + +``` json +{ + "python.analysis.ignore": [ "*" ] +} +``` + +## Why the distinctive coding style? + +FastHTML coding style is the [fastai coding +style](https://fast.ai/docs/dev/style.html). + +If you are coming from a data science background the **fastai coding +style** may already be your preferred style. + +If you are coming from a PEP-8 background where the use of ruff is +encouraged, there is a learning curve. However, once you get used to the +**fastai coding style** you may discover yourself appreciating the +concise nature of this style. It also encourages using more functional +programming tooling, which is both productive and fun. Having said that, +it’s entirely optional! + +## Why not JSX? + +Many have asked! We think there’s no benefit… Python’s positional and kw +args precisely 1:1 map already to html/xml children and attrs, so +there’s no need for a new syntax. + +We wrote some more thoughts on Why Python HTML components over Jinja2, +Mako, or JSX +[here](https://www.answer.ai/posts/2024-08-03-fasthtml.html#why). + +## Why use `import *` + +First, through the use of the +[`__all__`](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package) +attribute in our Python modules we control what actually gets imported. +So there’s no risk of namespace pollution. + +Second, our style lends itself to working in rather compact Jupyter +notebooks and small Python modules. Hence we know about the source code +whose libraries we `import *` from. This terseness means we can develop +faster. We’re a small team, and any edge we can gain is important to us. + +Third, for external libraries, be it core Python, SQLAlchemy, or other +things we do tend to use explicit imports. In part to avoid namespace +collisions, and also as reference to know where things are coming from. + +We’ll finish by saying a lot of our users employ explicit imports. If +that’s the path you want to take, we encourage the use of +`from fasthtml import common as fh`. The acronym of `fh` makes it easy +to recognize that a symbol is from the FastHTML library. + +## Can FastHTML be used for dashboards? + +Yes it can. In fact, it excels at building dashboards. In addition to +being great for building static dashboards, because of its +[foundation](https://fastht.ml/about/foundation) in ASGI and [tech +stack](https://fastht.ml/about/tech), FastHTML natively supports +Websockets. That means using FastHTML we can create dashboards that +autoupdate. + +## Why is FastHTML developed using notebooks? + +Some people are under the impression that writing software in notebooks +is bad. + +[Watch this +video](https://www.youtube.com/watch?v=9Q6sLbz37gk&ab_channel=JeremyHoward). +We’ve used Jupyter notebooks exported via `nbdev` to write a wide range +of “very serious” software projects over the last three years. This +includes deep learning libraries, API clients, Python language +extensions, terminal user interfaces, web frameworks, and more! + +[nbdev](https://nbdev.fast.ai/) is a Jupyter-powered tool for writing +software. Traditional programming environments throw away the result of +your exploration in REPLs or notebooks. `nbdev` makes exploration an +integral part of your workflow, all while promoting software engineering +best practices. + +## Why not pyproject.toml for packaging? + +FastHTML uses a `setup.py` module instead of a `pyproject.toml` file to +configure itself for installation. The reason for this is +`pyproject.toml` is not compatible with [nbdev](https://nbdev.fast.ai/), +which is what is used to write and build FastHTML. + +The nbdev project spent around a year trying to move to pyproject.toml +but there was insufficient functionality in the toml-based approach to +complete the transition. diff --git a/docs/explains/imgs/gh-oauth.png b/docs/explains/imgs/gh-oauth.png new file mode 100644 index 0000000000000000000000000000000000000000..2ca616faea7c520639ed60f797a526eb8f7e9354 --- /dev/null +++ b/docs/explains/imgs/gh-oauth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a9e5976f056264a30e25c61a43cc766cae19bd81554d7424380bab83c47039e +size 122885 diff --git a/docs/explains/minidataapi.html b/docs/explains/minidataapi.html new file mode 100644 index 0000000000000000000000000000000000000000..84f348290bdfaee3e67e317290d5972b874435db --- /dev/null +++ b/docs/explains/minidataapi.html @@ -0,0 +1,1404 @@ + + + + + + + + + +MiniDataAPI Spec – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

MiniDataAPI Spec

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

The MiniDataAPI is a persistence API specification that designed to be small and relatively easy to implement across a wide range of datastores. While early implementations have been SQL-based, the specification can be quickly implemented in key/value stores, document databases, and more.

+
+
+
+ +
+
+Work in Progress +
+
+
+

The MiniData API spec is a work in progress, subject to change. While the majority of design is complete, expect there could be breaking changes.

+
+
+
+

Why?

+

The MiniDataAPI specification allows us to use the same API for many different database engines. Any application using the MiniDataAPI spec for interacting with its database requires no modification beyond import and configuration changes to switch database engines. For example, to convert an application from Fastlite running SQLite to FastSQL running PostgreSQL, should require only changing these two lines:

+
+
+

FastLite version

+
from fastlite import *
+db = database('test.db')
+
+

FastSQL version

+
from fastsql import *
+db = Database('postgres:...')
+
+
+

As both libraries adhere to the MiniDataAPI specification, the rest of the code in the application should remain the same. The advantage of the MiniDataAPI spec is that it allows people to use whatever datastores they have access to or prefer.

+
+
+
+ +
+
+Note +
+
+
+

Switching databases won’t migrate any existing data between databases.

+
+
+
+

Easy to learn, quick to implement

+

The MiniDataAPI specification is designed to be easy-to-learn and quick to implement. It focuses on straightforward Create, Read, Update, and Delete (CRUD) operations.

+

MiniDataAPI databases aren’t limited to just row-based systems. In fact, the specification is closer in design to a key/value store than a set of records. What’s exciting about this is we can write implementations for tools like Python dict stored as JSON, Redis, and even the venerable ZODB.

+
+
+

Limitations of the MiniDataAPI Specification

+
+

“Mini refers to the lightweightness of specification, not the data.”

+

– Jeremy Howard

+
+

The advantages of the MiniDataAPI come at a cost. The MiniDataAPI specification focuses a very small set of features compared to what can be found in full-fledged ORMs and query languages. It intentionally avoids nuances or sophisticated features.

+

This means the specification does not include joins or formal foreign keys. Complex data stored over multiple tables that require joins isn’t handled well. For this kind of scenario it’s probably for the best to use more sophisticated ORMs or even direct database queries.

+
+
+

Summary of the MiniDataAPI Design

+
    +
  • Easy-to-learn
  • +
  • Relative quick to implement for new database engines
  • +
  • An API for CRUD operations
  • +
  • For many different types of databases including row- and key/value-based designs
  • +
  • Intentionally small in terms of features: no joins, no foreign keys, no database specific features
  • +
  • Best for simpler designs, complex architectures will need more sophisticated tools.
  • +
+
+
+
+

Connect/construct the database

+

We connect or construct the database by passing in a string connecting to the database endpoint or a filepath representing the database’s location. While this example is for SQLite running in memory, other databases such as PostgreSQL, Redis, MongoDB, might instead use a URI pointing at the database’s filepath or endpoint. The method of connecting to a DB is not part of this API, but part of the underlying library. For instance, for fastlite:

+
+
db = database(':memory:')
+
+

Here’s a complete list of the available methods in the API, all documented below (assuming db is a database and t is a table):

+
    +
  • db.create
  • +
  • t.insert
  • +
  • t.delete
  • +
  • t.update
  • +
  • t[key]
  • +
  • t(...)
  • +
  • t.xtra
  • +
+
+
+

Tables

+

For the sake of expediency, this document uses a SQL example. However, tables can represent anything, not just the fundamental construct of a SQL databases. They might represent keys within a key/value structure or files on a hard-drive.

+
+

Creating tables

+

We use a create() method attached to Database object (db in our example) to create the tables.

+
+
class User: name:str; email: str; year_started:int
+users = db.create(User, pk='name')
+users
+
+
<Table user (name, email, year_started)>
+
+
+
+
class User: name:str; email: str; year_started:int
+users = db.create(User, pk='name')
+users
+
+
<Table user (name, email, year_started)>
+
+
+

If no pk is provided, id is assumed to be the primary key. Regardless of whether you mark a class as a dataclass or not, it will be turned into one – specifically into a flexiclass.

+
+
@dataclass
+class Todo: id: int; title: str; detail: str; status: str; name: str
+todos = db.create(Todo) 
+todos
+
+
<Table todo (id, title, detail, status, name)>
+
+
+
+
+

Compound primary keys

+

The MiniData API spec supports compound primary keys, where more than one column is used to identify records. We’ll also use this example to demonstrate creating a table using a dict of keyword arguments.

+
+
class Publication: authors: str; year: int; title: str
+publications = db.create(Publication, pk=('authors', 'year'))
+
+
+
+

Transforming tables

+

Depending on the database type, this method can include transforms - the ability to modify the tables. Let’s go ahead and add a password field for our table called pwd.

+
+
class User: name:str; email: str; year_started:int; pwd:str
+users = db.create(User, pk='name', transform=True)
+users
+
+
<Table user (name, email, year_started, pwd)>
+
+
+
+
+
+

Manipulating data

+

The specification is designed to provide as straightforward CRUD API (Create, Read, Update, and Delete) as possible. Additional features like joins are out of scope.

+
+

.insert()

+

Add a new record to the database. We want to support as many types as possible, for now we have tests for Python classes, dataclasses, and dicts. Returns an instance of the new record.

+

Here’s how to add a record using a Python class:

+
+
users.insert(User(name='Braden', email='b@example.com', year_started=2018))
+
+
User(name='Braden', email='b@example.com', year_started=2018, pwd=None)
+
+
+

We can also use keyword arguments directly:

+
+
users.insert(name='Alma', email='a@example.com', year_started=2019)
+
+
User(name='Alma', email='a@example.com', year_started=2019, pwd=None)
+
+
+

And now Charlie gets added via a Python dict.

+
+
users.insert({'name': 'Charlie', 'email': 'c@example.com', 'year_started': 2018})
+
+
User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)
+
+
+

And now TODOs. Note that the inserted row is returned:

+
+
todos.insert(Todo(title='Write MiniDataAPI spec', status='open', name='Braden'))
+todos.insert(title='Implement SSE in FastHTML', status='open', name='Alma')
+todo = todos.insert(dict(title='Finish development of FastHTML', status='closed', name='Charlie'))
+todo
+
+
Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')
+
+
+

Let’s do the same with the Publications table.

+
+
publications.insert(Publication(authors='Alma', year=2019, title='FastHTML'))
+publications.insert(authors='Alma', year=2030, title='FastHTML and beyond')
+publication= publications.insert((dict(authors='Alma', year=2035, title='FastHTML, the early years')))
+publication
+
+
Publication(authors='Alma', year=2035, title='FastHTML, the early years')
+
+
+
+ + +
+

.update()

+

Update an existing record of the database. Must accept Python dict, dataclasses, and standard classes. Uses the primary key for identifying the record to be changed. Returns an instance of the updated record.

+

Here’s with a normal Python class:

+
+
user
+
+
User(name='Alma', email='a@example.com', year_started=2019, pwd=None)
+
+
+
+
user.year_started = 2099
+users.update(user)
+
+
User(name='Alma', email='a@example.com', year_started=2099, pwd=None)
+
+
+

Or use a dict:

+
+
users.update(dict(name='Alma', year_started=2199, email='a@example.com'))
+
+
User(name='Alma', email='a@example.com', year_started=2199, pwd=None)
+
+
+

Or use kwargs:

+
+
users.update(name='Alma', year_started=2149)
+
+
User(name='Alma', email='a@example.com', year_started=2149, pwd=None)
+
+
+

If the primary key doesn’t match a record, raise a NotFoundError.

+

John hasn’t started with us yet so doesn’t get the chance yet to travel in time.

+
+
try: users.update(User(name='John', year_started=2024, email='j@example.com'))
+except NotFoundError: print('User not found')
+
+
User not found
+
+
+
+
+

.delete()

+

Delete a record of the database. Uses the primary key for identifying the record to be removed. Returns a table object.

+

Charlie decides to not travel in time. He exits our little group.

+
+
users.delete('Charlie')
+
+
<Table user (name, email, year_started, pwd)>
+
+
+

If the primary key value can’t be found, raises a NotFoundError.

+
+
try: users.delete('Charlies')
+except NotFoundError: print('User not found')
+
+
User not found
+
+
+

In John’s case, he isn’t time travelling with us yet so can’t be removed.

+
+
try: users.delete('John')
+except NotFoundError: print('User not found')
+
+
User not found
+
+
+

Deleting records with compound primary keys requires providing the entire key.

+
+
publications.delete(['Alma' , 2035])
+
+
<Table publication (authors, year, title)>
+
+
+
+
+

in keyword

+

Are Alma and John contained in the Users table? Or, to be technically precise, is the item with the specified primary key value in this table?

+
+
'Alma' in users, 'John' in users
+
+
(True, False)
+
+
+

Also works with compound primary keys, as shown below. You’ll note that the operation can be done with either a list or tuple.

+
+
['Alma', 2019] in  publications
+
+
True
+
+
+

And now for a False result, where John has no publications.

+
+
('John', 1967) in publications
+
+
False
+
+
+
+
+

.xtra()

+

If we set fields within the .xtra function to a particular value, then indexing is also filtered by those. This applies to every database method except for record creation. This makes it easier to limit users (or other objects) access to only things for which they have permission. This is a one-way operation, once set it can’t be undone for a particular table object.

+

For example, if we query all our records below without setting values via the .xtra function, we can see todos for everyone. Pay special attention to the id values of all three records, as we are about to filter most of them away.

+
+
todos()
+
+
[Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden'),
+ Todo(id=2, title='Implement SSE in FastHTML', detail=None, status='open', name='Alma'),
+ Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]
+
+
+

Let’s use .xtra to constrain results just to Charlie. We set the name field in Todos, but it could be any field defined for this table.

+
+
todos.xtra(name='Charlie')
+
+

We’ve now set a field to a value with .xtra, if we loop over all the records again, only those assigned to records with a name of Charlie will be displayed.

+
+
todos()
+
+
[Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]
+
+
+

The in keyword is also affected. Only records with a name of Charlie will evaluate to be True. Let’s demonstrate by testing it with a Charlie record:

+
+
ct = todos[3]
+ct
+
+
Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')
+
+
+

Charlie’s record has an ID of 3. Here we demonstrate that Charlie’s TODO can be found in the list of todos:

+
+
ct.id in todos
+
+
True
+
+
+

If we try in with the other IDs the query fails because the filtering is now set to just records with a name of Charlie.

+
+
1 in todos, 2 in todos
+
+
(False, False)
+
+
+
+
try: todos[2]
+except NotFoundError: print('Record not found')
+
+
Record not found
+
+
+

We are also constrained by what records we can update. In the following example we try to update a TODO not named ‘Charlie’. Because the name is wrong, the .update function will raise a NotFoundError.

+
+
try: todos.update(Todo(id=1, title='Finish MiniDataAPI Spec', status='closed', name='Braden'))
+except NotFoundError as e: print('Record not updated')
+
+
Record not updated
+
+
+

Unlike poor Braden, Charlie isn’t filtered out. Let’s update his TODO.

+
+
todos.update(Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie'))
+
+
Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')
+
+
+

Finally, once constrained by .xtra, only records with Charlie as the name can be deleted.

+
+
try: todos.delete(1)
+except NotFoundError as e: print('Record not updated')
+
+
Record not updated
+
+
+

Charlie’s TODO was to finish development of FastHTML. While the framework will stabilize, like any good project it will see new features added and the odd bug corrected for many years to come. Therefore, Charlie’s TODO is nonsensical. Let’s delete it.

+
+
todos.delete(ct.id)
+
+
<Table todo (id, title, detail, status, name)>
+
+
+

When a TODO is inserted, the xtra fields are automatically set. This ensures that we don’t accidentally, for instance, insert items for others users. Note that here we don’t set the name field, but it’s still included in the resultant row:

+
+
ct = todos.insert(Todo(title='Rewrite personal site in FastHTML', status='open'))
+ct
+
+
Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')
+
+
+

If we try to change the username to someone else, the change is ignored, due to xtra:

+
+
ct.name = 'Braden'
+todos.update(ct)
+
+
Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')
+
+
+
+
+
+

SQL-first design

+
+
users = None
+User = None
+
+
+
users = db.t.user
+users
+
+
<Table user (name, email, year_started, pwd)>
+
+
+

(This section needs to be documented properly.)

+

From the table objects we can extract a Dataclass version of our tables. Usually this is given an singular uppercase version of our table name, which in this case is User.

+
+
User = users.dataclass()
+
+
+
User(name='Braden', email='b@example.com', year_started=2018)
+
+
User(name='Braden', email='b@example.com', year_started=2018, pwd=UNSET)
+
+
+
+
+

Implementations

+
+

Implementing MiniDataAPI for a new datastore

+

For creating new implementations, the code examples in this specification are the test case for the API. New implementations should pass the tests in order to be compliant with the specification.

+
+
+

Implementations

+
    +
  • fastlite - The original implementation, only for Sqlite
  • +
  • fastsql - An SQL database agnostic implementation based on the excellent SQLAlchemy library.
  • +
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/explains/minidataapi.html.md b/docs/explains/minidataapi.html.md new file mode 100644 index 0000000000000000000000000000000000000000..0259fc3ba3471c211237e76f21bdf3c4f8a552c1 --- /dev/null +++ b/docs/explains/minidataapi.html.md @@ -0,0 +1,668 @@ +# MiniDataAPI Spec + + + + +The `MiniDataAPI` is a persistence API specification that designed to be +small and relatively easy to implement across a wide range of +datastores. While early implementations have been SQL-based, the +specification can be quickly implemented in key/value stores, document +databases, and more. + +
+ +> **Work in Progress** +> +> The MiniData API spec is a work in progress, subject to change. While +> the majority of design is complete, expect there could be breaking +> changes. + +
+ +## Why? + +The MiniDataAPI specification allows us to use the same API for many +different database engines. Any application using the MiniDataAPI spec +for interacting with its database requires no modification beyond import +and configuration changes to switch database engines. For example, to +convert an application from Fastlite running SQLite to FastSQL running +PostgreSQL, should require only changing these two lines: + +
+ +
+ +FastLite version + +``` python +from fastlite import * +db = database('test.db') +``` + +
+ +
+ +FastSQL version + +``` python +from fastsql import * +db = Database('postgres:...') +``` + +
+ +
+ +As both libraries adhere to the MiniDataAPI specification, the rest of +the code in the application should remain the same. The advantage of the +MiniDataAPI spec is that it allows people to use whatever datastores +they have access to or prefer. + +
+ +> **Note** +> +> Switching databases won’t migrate any existing data between databases. + +
+ +### Easy to learn, quick to implement + +The MiniDataAPI specification is designed to be easy-to-learn and quick +to implement. It focuses on straightforward Create, Read, Update, and +Delete (CRUD) operations. + +MiniDataAPI databases aren’t limited to just row-based systems. In fact, +the specification is closer in design to a key/value store than a set of +records. What’s exciting about this is we can write implementations for +tools like Python dict stored as JSON, Redis, and even the venerable +ZODB. + +### Limitations of the MiniDataAPI Specification + +> “Mini refers to the lightweightness of specification, not the data.” +> +> – Jeremy Howard + +The advantages of the MiniDataAPI come at a cost. The MiniDataAPI +specification focuses a very small set of features compared to what can +be found in full-fledged ORMs and query languages. It intentionally +avoids nuances or sophisticated features. + +This means the specification does not include joins or formal foreign +keys. Complex data stored over multiple tables that require joins isn’t +handled well. For this kind of scenario it’s probably for the best to +use more sophisticated ORMs or even direct database queries. + +### Summary of the MiniDataAPI Design + +- Easy-to-learn +- Relative quick to implement for new database engines +- An API for CRUD operations +- For many different types of databases including row- and + key/value-based designs +- Intentionally small in terms of features: no joins, no foreign keys, + no database specific features +- Best for simpler designs, complex architectures will need more + sophisticated tools. + +## Connect/construct the database + +We connect or construct the database by passing in a string connecting +to the database endpoint or a filepath representing the database’s +location. While this example is for SQLite running in memory, other +databases such as PostgreSQL, Redis, MongoDB, might instead use a URI +pointing at the database’s filepath or endpoint. The method of +connecting to a DB is *not* part of this API, but part of the underlying +library. For instance, for fastlite: + +``` python +db = database(':memory:') +``` + +Here’s a complete list of the available methods in the API, all +documented below (assuming `db` is a database and `t` is a table): + +- `db.create` +- `t.insert` +- `t.delete` +- `t.update` +- `t[key]` +- `t(...)` +- `t.xtra` + +## Tables + +For the sake of expediency, this document uses a SQL example. However, +tables can represent anything, not just the fundamental construct of a +SQL databases. They might represent keys within a key/value structure or +files on a hard-drive. + +### Creating tables + +We use a `create()` method attached to `Database` object (`db` in our +example) to create the tables. + +``` python +class User: name:str; email: str; year_started:int +users = db.create(User, pk='name') +users +``` + + + +``` python +class User: name:str; email: str; year_started:int +users = db.create(User, pk='name') +users +``` + +
+ +If no `pk` is provided, `id` is assumed to be the primary key. +Regardless of whether you mark a class as a dataclass or not, it will be +turned into one – specifically into a +[`flexiclass`](https://fastcore.fast.ai/xtras.html#flexiclass). + +``` python +@dataclass +class Todo: id: int; title: str; detail: str; status: str; name: str +todos = db.create(Todo) +todos +``` + +
+ +### Compound primary keys + +The MiniData API spec supports compound primary keys, where more than +one column is used to identify records. We’ll also use this example to +demonstrate creating a table using a dict of keyword arguments. + +``` python +class Publication: authors: str; year: int; title: str +publications = db.create(Publication, pk=('authors', 'year')) +``` + +### Transforming tables + +Depending on the database type, this method can include transforms - the +ability to modify the tables. Let’s go ahead and add a password field +for our table called `pwd`. + +``` python +class User: name:str; email: str; year_started:int; pwd:str +users = db.create(User, pk='name', transform=True) +users +``` + +
+ +## Manipulating data + +The specification is designed to provide as straightforward CRUD API +(Create, Read, Update, and Delete) as possible. Additional features like +joins are out of scope. + +### .insert() + +Add a new record to the database. We want to support as many types as +possible, for now we have tests for Python classes, dataclasses, and +dicts. Returns an instance of the new record. + +Here’s how to add a record using a Python class: + +``` python +users.insert(User(name='Braden', email='b@example.com', year_started=2018)) +``` + + User(name='Braden', email='b@example.com', year_started=2018, pwd=None) + +We can also use keyword arguments directly: + +``` python +users.insert(name='Alma', email='a@example.com', year_started=2019) +``` + + User(name='Alma', email='a@example.com', year_started=2019, pwd=None) + +And now Charlie gets added via a Python dict. + +``` python +users.insert({'name': 'Charlie', 'email': 'c@example.com', 'year_started': 2018}) +``` + + User(name='Charlie', email='c@example.com', year_started=2018, pwd=None) + +And now TODOs. Note that the inserted row is returned: + +``` python +todos.insert(Todo(title='Write MiniDataAPI spec', status='open', name='Braden')) +todos.insert(title='Implement SSE in FastHTML', status='open', name='Alma') +todo = todos.insert(dict(title='Finish development of FastHTML', status='closed', name='Charlie')) +todo +``` + + Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie') + +Let’s do the same with the `Publications` table. + +``` python +publications.insert(Publication(authors='Alma', year=2019, title='FastHTML')) +publications.insert(authors='Alma', year=2030, title='FastHTML and beyond') +publication= publications.insert((dict(authors='Alma', year=2035, title='FastHTML, the early years'))) +publication +``` + + Publication(authors='Alma', year=2035, title='FastHTML, the early years') + +### Square bracket search \[\] + +Get a single record by entering a primary key into a table object within +square brackets. Let’s see if we can find Alma. + +``` python +user = users['Alma'] +user +``` + + User(name='Alma', email='a@example.com', year_started=2019, pwd=None) + +If no record is found, a `NotFoundError` error is raised. Here we look +for David, who hasn’t yet been added to our users table. + +``` python +try: users['David'] +except NotFoundError: print(f'User not found') +``` + + User not found + +Here’s a demonstration of a ticket search, demonstrating how this works +with non-string primary keys. + +``` python +todos[1] +``` + + Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden') + +Compound primary keys can be supplied in lists or tuples, in the order +they were defined. In this case it is the `authors` and `year` columns. + +Here’s a query by compound primary key done with a `list`: + +``` python +publications[['Alma', 2019]] +``` + + Publication(authors='Alma', year=2019, title='FastHTML') + +Here’s the same query done directly with index args. + +``` python +publications['Alma', 2030] +``` + + Publication(authors='Alma', year=2030, title='FastHTML and beyond') + +### Parentheses search () + +Get zero to many records by entering values with parentheses searches. +If nothing is in the parentheses, then everything is returned. + +``` python +users() +``` + + [User(name='Braden', email='b@example.com', year_started=2018, pwd=None), + User(name='Alma', email='a@example.com', year_started=2019, pwd=None), + User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)] + +We can order the results. + +``` python +users(order_by='name') +``` + + [User(name='Alma', email='a@example.com', year_started=2019, pwd=None), + User(name='Braden', email='b@example.com', year_started=2018, pwd=None), + User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)] + +We can filter on the results: + +``` python +users(where="name='Alma'") +``` + + [User(name='Alma', email='a@example.com', year_started=2019, pwd=None)] + +Generally you probably want to use placeholders, to avoid SQL injection +attacks: + +``` python +users("name=?", ('Alma',)) +``` + + [User(name='Alma', email='a@example.com', year_started=2019, pwd=None)] + +We can limit results with the `limit` keyword: + +``` python +users(limit=1) +``` + + [User(name='Braden', email='b@example.com', year_started=2018, pwd=None)] + +If we’re using the `limit` keyword, we can also use the `offset` keyword +to start the query later. + +``` python +users(limit=5, offset=1) +``` + + [User(name='Alma', email='a@example.com', year_started=2019, pwd=None), + User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)] + +### .update() + +Update an existing record of the database. Must accept Python dict, +dataclasses, and standard classes. Uses the primary key for identifying +the record to be changed. Returns an instance of the updated record. + +Here’s with a normal Python class: + +``` python +user +``` + + User(name='Alma', email='a@example.com', year_started=2019, pwd=None) + +``` python +user.year_started = 2099 +users.update(user) +``` + + User(name='Alma', email='a@example.com', year_started=2099, pwd=None) + +Or use a dict: + +``` python +users.update(dict(name='Alma', year_started=2199, email='a@example.com')) +``` + + User(name='Alma', email='a@example.com', year_started=2199, pwd=None) + +Or use kwargs: + +``` python +users.update(name='Alma', year_started=2149) +``` + + User(name='Alma', email='a@example.com', year_started=2149, pwd=None) + +If the primary key doesn’t match a record, raise a `NotFoundError`. + +John hasn’t started with us yet so doesn’t get the chance yet to travel +in time. + +``` python +try: users.update(User(name='John', year_started=2024, email='j@example.com')) +except NotFoundError: print('User not found') +``` + + User not found + +### .delete() + +Delete a record of the database. Uses the primary key for identifying +the record to be removed. Returns a table object. + +Charlie decides to not travel in time. He exits our little group. + +``` python +users.delete('Charlie') +``` + +
+ +If the primary key value can’t be found, raises a `NotFoundError`. + +``` python +try: users.delete('Charlies') +except NotFoundError: print('User not found') +``` + + User not found + +In John’s case, he isn’t time travelling with us yet so can’t be +removed. + +``` python +try: users.delete('John') +except NotFoundError: print('User not found') +``` + + User not found + +Deleting records with compound primary keys requires providing the +entire key. + +``` python +publications.delete(['Alma' , 2035]) +``` + +
+ +### `in` keyword + +Are `Alma` and `John` contained `in` the Users table? Or, to be +technically precise, is the item with the specified primary key value +`in` this table? + +``` python +'Alma' in users, 'John' in users +``` + + (True, False) + +Also works with compound primary keys, as shown below. You’ll note that +the operation can be done with either a `list` or `tuple`. + +``` python +['Alma', 2019] in publications +``` + + True + +And now for a `False` result, where John has no publications. + +``` python +('John', 1967) in publications +``` + + False + +### .xtra() + +If we set fields within the `.xtra` function to a particular value, then +indexing is also filtered by those. This applies to every database +method except for record creation. This makes it easier to limit users +(or other objects) access to only things for which they have permission. +This is a one-way operation, once set it can’t be undone for a +particular table object. + +For example, if we query all our records below without setting values +via the `.xtra` function, we can see todos for everyone. Pay special +attention to the `id` values of all three records, as we are about to +filter most of them away. + +``` python +todos() +``` + + [Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden'), + Todo(id=2, title='Implement SSE in FastHTML', detail=None, status='open', name='Alma'), + Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')] + +Let’s use `.xtra` to constrain results just to Charlie. We set the +`name` field in Todos, but it could be any field defined for this table. + +``` python +todos.xtra(name='Charlie') +``` + +We’ve now set a field to a value with `.xtra`, if we loop over all the +records again, only those assigned to records with a `name` of `Charlie` +will be displayed. + +``` python +todos() +``` + + [Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')] + +The `in` keyword is also affected. Only records with a `name` of Charlie +will evaluate to be `True`. Let’s demonstrate by testing it with a +Charlie record: + +``` python +ct = todos[3] +ct +``` + + Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie') + +Charlie’s record has an ID of 3. Here we demonstrate that Charlie’s TODO +can be found in the list of todos: + +``` python +ct.id in todos +``` + + True + +If we try `in` with the other IDs the query fails because the filtering +is now set to just records with a name of Charlie. + +``` python +1 in todos, 2 in todos +``` + + (False, False) + +``` python +try: todos[2] +except NotFoundError: print('Record not found') +``` + + Record not found + +We are also constrained by what records we can update. In the following +example we try to update a TODO not named ‘Charlie’. Because the name is +wrong, the `.update` function will raise a `NotFoundError`. + +``` python +try: todos.update(Todo(id=1, title='Finish MiniDataAPI Spec', status='closed', name='Braden')) +except NotFoundError as e: print('Record not updated') +``` + + Record not updated + +Unlike poor Braden, Charlie isn’t filtered out. Let’s update his TODO. + +``` python +todos.update(Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')) +``` + + Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie') + +Finally, once constrained by `.xtra`, only records with Charlie as the +name can be deleted. + +``` python +try: todos.delete(1) +except NotFoundError as e: print('Record not updated') +``` + + Record not updated + +Charlie’s TODO was to finish development of FastHTML. While the +framework will stabilize, like any good project it will see new features +added and the odd bug corrected for many years to come. Therefore, +Charlie’s TODO is nonsensical. Let’s delete it. + +``` python +todos.delete(ct.id) +``` + +
+ +When a TODO is inserted, the `xtra` fields are automatically set. This +ensures that we don’t accidentally, for instance, insert items for +others users. Note that here we don’t set the `name` field, but it’s +still included in the resultant row: + +``` python +ct = todos.insert(Todo(title='Rewrite personal site in FastHTML', status='open')) +ct +``` + + Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie') + +If we try to change the username to someone else, the change is ignored, +due to `xtra`: + +``` python +ct.name = 'Braden' +todos.update(ct) +``` + + Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie') + +## SQL-first design + +``` python +users = None +User = None +``` + +``` python +users = db.t.user +users +``` + +
+ +(This section needs to be documented properly.) + +From the table objects we can extract a Dataclass version of our tables. +Usually this is given an singular uppercase version of our table name, +which in this case is `User`. + +``` python +User = users.dataclass() +``` + +``` python +User(name='Braden', email='b@example.com', year_started=2018) +``` + + User(name='Braden', email='b@example.com', year_started=2018, pwd=UNSET) + +## Implementations + +### Implementing MiniDataAPI for a new datastore + +For creating new implementations, the code examples in this +specification are the test case for the API. New implementations should +pass the tests in order to be compliant with the specification. + +### Implementations + +- [fastlite](https://github.com/AnswerDotAI/fastlite) - The original + implementation, only for Sqlite +- [fastsql](https://github.com/AnswerDotAI/fastsql) - An SQL database + agnostic implementation based on the excellent SQLAlchemy library. diff --git a/docs/explains/oauth.html b/docs/explains/oauth.html new file mode 100644 index 0000000000000000000000000000000000000000..fcefa3439d1a4c9206331aa353845b97feb71b51 --- /dev/null +++ b/docs/explains/oauth.html @@ -0,0 +1,1123 @@ + + + + + + + + + +OAuth – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

OAuth

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

OAuth is an open standard for ‘access delegation’, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. It is the mechanism that enables “Log in with Google” on many sites, saving you from having to remember and manage yet another password. Like many auth-related topics, there’s a lot of depth and complexity to the OAuth standard, but once you understand the basic usage it can be a very convenient alternative to managing your own user accounts.

+

On this page you’ll see how to use OAuth with FastHTML to implement some common pieces of functionality.

+
+

Creating an Client

+

FastHTML has Client classes for managing settings and state for different OAuth providers. Currently implemented are: GoogleAppClient, GitHubAppClient, HuggingFaceClient and DiscordAppClient - see the source if you need to add other providers. You’ll need a client_id and client_secret from the provider (see the from-scratch example later in this page for an example of registering with GitHub) to create the client. We recommend storing these in environment variables, rather than hardcoding them in your code.

+
+
import os
+from fasthtml.oauth import GoogleAppClient
+client = GoogleAppClient(os.getenv("AUTH_CLIENT_ID"),
+                         os.getenv("AUTH_CLIENT_SECRET"))
+
+

The client is used to obtain a login link and to manage communications between your app and the OAuth provider (client.login_link(redirect_uri="/redirect")).

+
+
+

Using the OAuth class

+

Once you’ve set up a client, adding OAuth to a FastHTML app can be as simple as:

+
+
from fasthtml.oauth import OAuth
+from fasthtml.common import FastHTML, RedirectResponse
+
+class Auth(OAuth):
+    def get_auth(self, info, ident, session, state):
+        email = info.email or ''
+        if info.email_verified and email.split('@')[-1]=='answer.ai':
+            return RedirectResponse('/', status_code=303)
+
+app = FastHTML()
+oauth = Auth(app, client)
+
+@app.get('/')
+def home(auth): return P('Logged in!'), A('Log out', href='/logout')
+
+@app.get('/login')
+def login(req): return Div(P("Not logged in"), A('Log in', href=oauth.login_link(req)))
+
+

There’s a fair bit going on here, so let’s unpack what’s happening in that code:

+
    +
  • OAuth (and by extension our custom Auth class) has a number of default arguments, including some key URLs: redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login'. It will create and handle the redirect and logout paths, and it’s up to you to handle /login (where unsuccessful login attempts will be redirected) and /error (for oauth errors).
  • +
  • When we run oauth = Auth(app, client) it adds the redirect and logout paths to the app and also adds some beforeware. This beforeware runs on any requests (apart from any specified with the skip parameter).
  • +
+

The added beforeware specifies some app behaviour:

+
    +
  • If someone who isn’t logged in attempts to visit our homepage (/) here, they will be redirected to /login.
  • +
  • If they are logged in, it calls a check_invalid method. This defaults to False, which let’s the user continue to the page they requested. The behaviour can be modified by defining your own check_invalid method in the Auth class - for example, you could have this forcibly log out users who have recently been banned.
  • +
+

So how does someone log in? If they visit (or are redirected to) the login page at /login, we show them a login link. This sends them to the OAuth provider, where they’ll go through the steps of selecting their account, giving permissions etc. Once done they will be redirected back to /redirect. Behind the scenes a code that comes as part of their request gets turned into user info, which is then passed to the key function get_auth(self, info, ident, session, state). Here is where you’d handle looking up or adding a user in a database, checking for some condition (for example, this code checks if the email is an answer.ai email address) or choosing the destination based on state. The arguments are:

+
    +
  • self: the Auth object, which you can use to access the client (self.cli)
  • +
  • info: the information provided by the OAuth provider, typically including a unique user id, email address, username and other metadata.
  • +
  • ident: a unique identifier for this user. What this looks like varies between providers. This is useful for managing a database of users, for example.
  • +
  • session: the current session, that you can store information in securely
  • +
  • state: you can optionally pass in some state when creating the login link. This persists and is returned after the user goes through the Oath steps, which is useful for returning them to the same page they left. It can also be used as added security against CSRF attacks.
  • +
+

In our example, we check the email in info (we use a GoogleAppClient, not all providers will include an email). If we aren’t happy, and get_auth returns False or nothing (as in the case here for non-answerai people) then the user is redirected back to the login page. But if everything looks good we return a redirect to the homepage, and an auth key is added to the session and the scope containing the users identity ident. So, for example, in the homepage route we could use auth to look up this particular user’s profile info and customize the page accordingly. This auth will persist in their session until they clear the browser cache, so by default they’ll stay logged in. To log them out, remove it ( session.pop('auth', None)) or send them to /logout which will do that for you.

+
+
+

Explaining OAuth with a from-scratch implementation

+

Hopefully the example above is enough to get you started. You can also check out the (fairly minimal) source code where this is implemented, and the examples here.

+

If you’re wanting to learn more about how this works, and to see where you might add additional functionality, the rest of this page will walk through some examples without the OAuth convenience class, to illustrate the concepts. This was written before said OAuth class was available, and is kept here for educational purposes - we recommend you stick with the new approach shown above in most cases.

+
+
+

A Minimal Login Flow (GitHub)

+

Let’s begin by building a minimal ‘Sign in with GitHub’ flow. This will demonstrate the basic steps of OAuth.

+

OAuth requires a “provider” (in this case, GitHub) to authenticate the user. So the first step when setting up our app is to register with GitHub to set things up.

+

Go to https://github.com/settings/developers and click “New OAuth App”. Fill in the form with the following values, then click ‘Register application’.

+
    +
  • Application name: Your app name
  • +
  • Homepage URL: http://localhost:8000 (or whatever URL you’re using - you can change this later)
  • +
  • Authorization callback URL: http://localhost:8000/auth_redirect (you can modify this later too)
  • +
+
+Setting up an OAuth app in GitHub +
+

After you register, you’ll see a screen where you can view the client ID and generate a client secret. Store these values in a safe place. You’ll use them to create a GitHubAppClient object in FastHTML.

+

This client object is responsible for handling the parts of the OAuth flow which depend on direct communication between your app and GitHub, as opposed to interactions which go through the user’s browser via redirects.

+

Here is how to setup the client object:

+
client = GitHubAppClient(
+    client_id="your_client_id",
+    client_secret="your_client_secret"
+)
+

You should also save the path component of the authorization callback URL which you provided on registration.

+

This route is where GitHub will redirect the user’s browser in order to send an authorization code to your app. You should save only the URL’s path component rather than the entire URL because you want your code to work automatically in deployment, when the host and port part of the URL change from localhost:8000 to your real DNS name.

+

Save the special authorization callback path under an obvious name:

+
auth_callback_path = "/auth_redirect"
+
+
+
+ +
+
+Note +
+
+
+

It’s recommended to store the client ID, and secret, in environment variables, rather than hardcoding them in your code.

+
+
+

When the user visit a normal page of your app, if they are not already logged in, then you’ll want to redirect them to your app’s login page, which will live at the /login path. We accomplish that by using this piece of “beforeware”, which defines logic which runs before other work for all routes except ones we specify to be skipped:

+
def before(req, session):
+    auth = req.scope['auth'] = session.get('user_id', None)
+    if not auth: return RedirectResponse('/login', status_code=303)
+    counts.xtra(name=auth)
+bware = Beforeware(before, skip=['/login', auth_callback_path])
+

We configure the beforeware to skip /login because that’s where the user goes to login, and we also skip the special authorization callback path because that is used by OAuth itself to receive information from GitHub.

+

It’s only at your login page that we start the OAuth flow. To start the OAuth flow, you need to give the user a link to GitHub’s login for your app. You’ll need the client object to generate that link, and the client object will in turn need the full authorization callback URL, which we need to build from the authorization callback path, so it is a multi-step process to produce this GitHub login link.

+

Here is an implementation of your own /login route handler. It generates the GitHub login link and presents it to the user:

+
@app.get('/login')
+def login(request)
+    redir = redir_url(request,auth_callback_path)
+    login_link = client.login_link(redir)
+    return P(A('Login with GitHub', href=login_link))    
+

Once the user follows that link, GitHub will ask them to grant permission to your app to access their GitHub account. If they agree, GitHub will redirect them back to your app’s authorization callback URL, carrying an authorization code which your app can use to generate an access token. To receive this code, you need to set up a route in FastHTML that listens for requests at the authorization callback path. For example:

+
@app.get(auth_callback_path)
+def auth_redirect(code:str):
+    return P(f"code: {code}")
+

This authorization code is temporary, and is used by your app to directly ask the provider for user information like an access token.

+

To recap, you can think of the exchange so far as:

+
    +
  • User to us: “I want to log in with you, app.”
  • +
  • Us to User: “Okay but first, here’s a special link to log in with GitHub”
  • +
  • User to GitHub: “I want to log in with you, GitHub, to use this app.”
  • +
  • GitHub to User: “OK, redirecting you back to the app’s URL (with an auth code)”
  • +
  • User to Us: “Hi again, app. Here’s the GitHub auth code you need to ask GitHub for info about me” (delivered via /auth_redirect?code=...)
  • +
+

The final steps we need to implement are as follows:

+
    +
  • Us to GitHUb: “A user just gave me this auth code. May I have the user info (e.g., an access token)?”
  • +
  • GitHub to us: “Since you have an auth code, here’s the user info”
  • +
+

It’s critical for us to derive the user info from the auth code immediately in the authorization callback, because the auth code may be used only once. So we use it that once in order to get information like an access token, which will remain valid for longer.

+

To go from the auth code to user info, you use info = client.retr_info(code,redirect_uri). From the user info, you can extract the user_id, which is a unique identifier for the user:

+
@app.get(auth_callback_path)
+def auth_redirect(code:str, request):
+    redir = redir_url(request, auth_callback_path)
+    user_info = client.retr_info(code, redir)
+    user_id = info[client.id_key]
+    return P(f"User id: {user_id}")
+

But we want the user ID not to print it but to remember the user.

+

So let us store it in the session object, to remember who is logged in:

+
@app.get(auth_callback_path)
+def auth_redirect(code:str, request, session):
+    redir = redir_url(request, auth_callback_path)
+    user_info = client.retr_info(code, redir)
+    user_id = user_info[client.id_key] # get their ID
+    session['user_id'] = user_id # save ID in the session
+    return RedirectResponse('/', status_code=303)
+

The session object is derived from values visible to the user’s browser, but it is cryptographically signed so the user can’t read it themselves. This makes it safe to store even information we don’t want to expose to the user.

+

For larger quantities of data, we’d want to save that information in a database and use the session to hold keys to lookup information from that database.

+

Here’s a minimal app that puts all these pieces together. It uses the user info to get the user_id. It stores that in the session object. It then uses the user_id as a key into a database, which tracks how frequently every user has hit an increment button.

+
import os
+from fasthtml.common import *
+from fasthtml.oauth import GitHubAppClient, redir_url
+
+db = database('data/counts.db')
+counts = db.t.counts
+if counts not in db.t: counts.create(dict(name=str, count=int), pk='name')
+Count = counts.dataclass()
+
+# Auth client setup for GitHub
+client = GitHubAppClient(os.getenv("AUTH_CLIENT_ID"), 
+                         os.getenv("AUTH_CLIENT_SECRET"))
+auth_callback_path = "/auth_redirect"
+
+def before(req, session):
+    # if not logged in, we send them to our login page
+    # logged in means:
+    # - 'user_id' in the session object, 
+    # - 'auth' in the request object
+    auth = req.scope['auth'] = session.get('user_id', None)
+    if not auth: return RedirectResponse('/login', status_code=303)
+    counts.xtra(name=auth)
+bware = Beforeware(before, skip=['/login', auth_callback_path])
+
+app = FastHTML(before=bware)
+
+# User asks us to Login
+@app.get('/login')
+def login(request):
+    redir = redir_url(request,auth_callback_path)
+    login_link = client.login_link(redir)
+    # we tell user to login at github
+    return P(A('Login with GitHub', href=login_link))    
+
+# User comes back to us with an auth code from Github
+@app.get(auth_callback_path)
+def auth_redirect(code:str, request, session):
+    redir = redir_url(request, auth_callback_path)
+    user_info = client.retr_info(code, redir)
+    user_id = user_info[client.id_key] # get their ID
+    session['user_id'] = user_id # save ID in the session
+    # create a db entry for the user
+    if user_id not in counts: counts.insert(name=user_id, count=0)
+    return RedirectResponse('/', status_code=303)
+
+@app.get('/')
+def home(auth):
+    return Div(
+        P("Count demo"),
+        P(f"Count: ", Span(counts[auth].count, id='count')),
+        Button('Increment', hx_get='/increment', hx_target='#count'),
+        P(A('Logout', href='/logout'))
+    )
+
+@app.get('/increment')
+def increment(auth):
+    c = counts[auth]
+    c.count += 1
+    return counts.upsert(c).count
+
+@app.get('/logout')
+def logout(session):
+    session.pop('user_id', None)
+    return RedirectResponse('/login', status_code=303)
+
+serve()
+

Some things to note:

+
    +
  • The before function is used to check if the user is authenticated. If not, they are redirected to the login page.
  • +
  • To log the user out, we remove the user ID from the session.
  • +
  • Calling counts.xtra(name=auth) ensures that only the row corresponding to the current user is accessible when responding to a request. This is often nicer than trying to remember to filter the data in every route, and lowers the risk of accidentally leaking data.
  • +
  • In the auth_redirect route, we store the user ID in the session and create a new row in the user_counts table if it doesn’t already exist.
  • +
+

You can find more heavily-commented version of this code in the oauth directory in fasthtml-example, along with an even more minimal example. More examples may be added in the future.

+
+

Revoking Tokens (Google)

+

When the user in the example above logs out, we remove their user ID from the session. However, the user is still logged in to GitHub. If they click ‘Login with GitHub’ again, they’ll be redirected back to our site without having to log in again. This is because GitHub remembers that they’ve already granted our app permission to access their account. Most of the time this is convenient, but for testing or security purposes you may want a way to revoke this permission.

+

As a user, you can usually revoke access to an app from the provider’s website (for example, https://github.com/settings/applications). But as a developer, you can also revoke access programmatically - at least with some providers. This requires keeping track of the access token (stored in client.token["access_token"] after you call retr_info), and sending a request to the provider’s revoke URL:

+
auth_revoke_url = "https://accounts.google.com/o/oauth2/revoke"
+def revoke_token(token):
+    response = requests.post(auth_revoke_url, params={"token": token})
+    return response.status_code == 200 # True if successful
+

Not all providers support token revocation, and it is not built into FastHTML clients at the moment.

+
+
+

Using State (Hugging Face)

+

Imagine a user (not logged in) comes to your AI image editing site, starts testing things out, and then realizes they need to sign in before they can click “Run (Pro)” on the edit they’re working on. They click “Sign in with Hugging Face”, log in, and are redirected back to your site. But now they’ve lost their in-progress edit and are left just looking at the homepage! This is an example of a case where you might want to keep track of some additional state. Another strong use case for being able to pass some uniqie state through the OAuth flow is to prevent something called a CSRF attack. To add a state string to the OAuth flow, include a state argument when creating the login link:

+
# in login page:
+link = A('Login with GitHub', href=client.login_link(state='current_prompt: add a unicorn'))
+
+# in auth_redirect:
+@app.get('/auth_redirect')
+def auth_redirect(code:str, session, state:str=None):
+    print(f"state: {state}") # Use as needed
+    ...
+

The state string is passed through the OAuth flow and back to your site.

+
+
+

A Work in Progress

+

This page (and OAuth support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!

+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/explains/oauth.html.md b/docs/explains/oauth.html.md new file mode 100644 index 0000000000000000000000000000000000000000..118f5803366b66905f285586ea4483eb834a06a9 --- /dev/null +++ b/docs/explains/oauth.html.md @@ -0,0 +1,480 @@ +# OAuth + + + + +OAuth is an open standard for ‘access delegation’, commonly used as a +way for Internet users to grant websites or applications access to their +information on other websites but without giving them the passwords. It +is the mechanism that enables “Log in with Google” on many sites, saving +you from having to remember and manage yet another password. Like many +auth-related topics, there’s a lot of depth and complexity to the OAuth +standard, but once you understand the basic usage it can be a very +convenient alternative to managing your own user accounts. + +On this page you’ll see how to use OAuth with FastHTML to implement some +common pieces of functionality. + +## Creating an Client + +FastHTML has Client classes for managing settings and state for +different OAuth providers. Currently implemented are: GoogleAppClient, +GitHubAppClient, HuggingFaceClient and DiscordAppClient - see the +[source](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/api/08_oauth.ipynb) +if you need to add other providers. You’ll need a `client_id` and +`client_secret` from the provider (see the from-scratch example later in +this page for an example of registering with GitHub) to create the +client. We recommend storing these in environment variables, rather than +hardcoding them in your code. + +``` python +import os +from fasthtml.oauth import GoogleAppClient +client = GoogleAppClient(os.getenv("AUTH_CLIENT_ID"), + os.getenv("AUTH_CLIENT_SECRET")) +``` + +The client is used to obtain a login link and to manage communications +between your app and the OAuth provider +(`client.login_link(redirect_uri="/redirect")`). + +## Using the OAuth class + +Once you’ve set up a client, adding OAuth to a FastHTML app can be as +simple as: + +``` python +from fasthtml.oauth import OAuth +from fasthtml.common import FastHTML, RedirectResponse + +class Auth(OAuth): + def get_auth(self, info, ident, session, state): + email = info.email or '' + if info.email_verified and email.split('@')[-1]=='answer.ai': + return RedirectResponse('/', status_code=303) + +app = FastHTML() +oauth = Auth(app, client) + +@app.get('/') +def home(auth): return P('Logged in!'), A('Log out', href='/logout') + +@app.get('/login') +def login(req): return Div(P("Not logged in"), A('Log in', href=oauth.login_link(req))) +``` + +There’s a fair bit going on here, so let’s unpack what’s happening in +that code: + +- OAuth (and by extension our custom Auth class) has a number of default + arguments, including some key URLs: + `redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login'`. + It will create and handle the redirect and logout paths, and it’s up + to you to handle `/login` (where unsuccessful login attempts will be + redirected) and `/error` (for oauth errors). +- When we run `oauth = Auth(app, client)` it adds the redirect and + logout paths to the app and also adds some beforeware. This beforeware + runs on any requests (apart from any specified with the `skip` + parameter). + +The added beforeware specifies some app behaviour: + +- If someone who isn’t logged in attempts to visit our homepage (`/`) + here, they will be redirected to `/login`. +- If they are logged in, it calls a `check_invalid` method. This + defaults to False, which let’s the user continue to the page they + requested. The behaviour can be modified by defining your own + `check_invalid` method in the Auth class - for example, you could have + this forcibly log out users who have recently been banned. + +So how does someone log in? If they visit (or are redirected to) the +login page at `/login`, we show them a login link. This sends them to +the OAuth provider, where they’ll go through the steps of selecting +their account, giving permissions etc. Once done they will be redirected +back to `/redirect`. Behind the scenes a code that comes as part of +their request gets turned into user info, which is then passed to the +key function `get_auth(self, info, ident, session, state)`. Here is +where you’d handle looking up or adding a user in a database, checking +for some condition (for example, this code checks if the email is an +answer.ai email address) or choosing the destination based on state. The +arguments are: + +- `self`: the Auth object, which you can use to access the client + (`self.cli`) +- `info`: the information provided by the OAuth provider, typically + including a unique user id, email address, username and other + metadata. +- `ident`: a unique identifier for this user. What this looks like + varies between providers. This is useful for managing a database of + users, for example. +- `session`: the current session, that you can store information in + securely +- `state`: you can optionally pass in some state when creating the login + link. This persists and is returned after the user goes through the + Oath steps, which is useful for returning them to the same page they + left. It can also be used as added security against CSRF attacks. + +In our example, we check the email in `info` (we use a GoogleAppClient, +not all providers will include an email). If we aren’t happy, and +get_auth returns False or nothing (as in the case here for non-answerai +people) then the user is redirected back to the login page. But if +everything looks good we return a redirect to the homepage, and an +`auth` key is added to the session and the scope containing the users +identity `ident`. So, for example, in the homepage route we could use +`auth` to look up this particular user’s profile info and customize the +page accordingly. This auth will persist in their session until they +clear the browser cache, so by default they’ll stay logged in. To log +them out, remove it ( `session.pop('auth', None)`) or send them to +`/logout` which will do that for you. + +## Explaining OAuth with a from-scratch implementation + +Hopefully the example above is enough to get you started. You can also +check out the (fairly minimal) [source +code](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/api/08_oauth.ipynb) +where this is implemented, and the [examples +here](https://github.com/AnswerDotAI/fasthtml-example/blob/main/oauth_example). + +If you’re wanting to learn more about how this works, and to see where +you might add additional functionality, the rest of this page will walk +through some examples **without** the OAuth convenience class, to +illustrate the concepts. This was written before said OAuth class was +available, and is kept here for educational purposes - we recommend you +stick with the new approach shown above in most cases. + +## A Minimal Login Flow (GitHub) + +Let’s begin by building a minimal ‘Sign in with GitHub’ flow. This will +demonstrate the basic steps of OAuth. + +OAuth requires a “provider” (in this case, GitHub) to authenticate the +user. So the first step when setting up our app is to register with +GitHub to set things up. + +Go to https://github.com/settings/developers and click “New OAuth App”. +Fill in the form with the following values, then click ‘Register +application’. + +- Application name: Your app name +- Homepage URL: http://localhost:8000 (or whatever URL you’re using - + you can change this later) +- Authorization callback URL: http://localhost:8000/auth_redirect (you + can modify this later too) + +
+ +Setting up an OAuth app in GitHub + +
+ +After you register, you’ll see a screen where you can view the client ID +and generate a client secret. Store these values in a safe place. You’ll +use them to create a +[`GitHubAppClient`](https://www.fastht.ml/docs/api/oauth.html#githubappclient) +object in FastHTML. + +This `client` object is responsible for handling the parts of the OAuth +flow which depend on direct communication between your app and GitHub, +as opposed to interactions which go through the user’s browser via +redirects. + +Here is how to setup the client object: + +``` python +client = GitHubAppClient( + client_id="your_client_id", + client_secret="your_client_secret" +) +``` + +You should also save the path component of the authorization callback +URL which you provided on registration. + +This route is where GitHub will redirect the user’s browser in order to +send an authorization code to your app. You should save only the URL’s +path component rather than the entire URL because you want your code to +work automatically in deployment, when the host and port part of the URL +change from `localhost:8000` to your real DNS name. + +Save the special authorization callback path under an obvious name: + +``` python +auth_callback_path = "/auth_redirect" +``` + +
+ +> **Note** +> +> It’s recommended to store the client ID, and secret, in environment +> variables, rather than hardcoding them in your code. + +
+ +When the user visit a normal page of your app, if they are not already +logged in, then you’ll want to redirect them to your app’s login page, +which will live at the `/login` path. We accomplish that by using this +piece of “beforeware”, which defines logic which runs before other work +for all routes except ones we specify to be skipped: + +``` python +def before(req, session): + auth = req.scope['auth'] = session.get('user_id', None) + if not auth: return RedirectResponse('/login', status_code=303) + counts.xtra(name=auth) +bware = Beforeware(before, skip=['/login', auth_callback_path]) +``` + +We configure the beforeware to skip `/login` because that’s where the +user goes to login, and we also skip the special authorization callback +path because that is used by OAuth itself to receive information from +GitHub. + +It’s only at your login page that we start the OAuth flow. To start the +OAuth flow, you need to give the user a link to GitHub’s login for your +app. You’ll need the `client` object to generate that link, and the +client object will in turn need the full authorization callback URL, +which we need to build from the authorization callback path, so it is a +multi-step process to produce this GitHub login link. + +Here is an implementation of your own `/login` route handler. It +generates the GitHub login link and presents it to the user: + +``` python +@app.get('/login') +def login(request) + redir = redir_url(request,auth_callback_path) + login_link = client.login_link(redir) + return P(A('Login with GitHub', href=login_link)) +``` + +Once the user follows that link, GitHub will ask them to grant +permission to your app to access their GitHub account. If they agree, +GitHub will redirect them back to your app’s authorization callback URL, +carrying an authorization code which your app can use to generate an +access token. To receive this code, you need to set up a route in +FastHTML that listens for requests at the authorization callback path. +For example: + +``` python +@app.get(auth_callback_path) +def auth_redirect(code:str): + return P(f"code: {code}") +``` + +This authorization code is temporary, and is used by your app to +directly ask the provider for user information like an access token. + +To recap, you can think of the exchange so far as: + +- User to us: “I want to log in with you, app.” +- Us to User: “Okay but first, here’s a special link to log in with + GitHub” +- User to GitHub: “I want to log in with you, GitHub, to use this app.” +- GitHub to User: “OK, redirecting you back to the app’s URL (with an + auth code)” +- User to Us: “Hi again, app. Here’s the GitHub auth code you need to + ask GitHub for info about me” (delivered via + `/auth_redirect?code=...`) + +The final steps we need to implement are as follows: + +- Us to GitHUb: “A user just gave me this auth code. May I have the user + info (e.g., an access token)?” +- GitHub to us: “Since you have an auth code, here’s the user info” + +It’s critical for us to derive the user info from the auth code +immediately in the authorization callback, because the auth code may be +used only once. So we use it that once in order to get information like +an access token, which will remain valid for longer. + +To go from the auth code to user info, you use +`info = client.retr_info(code,redirect_uri)`. From the user info, you +can extract the `user_id`, which is a unique identifier for the user: + +``` python +@app.get(auth_callback_path) +def auth_redirect(code:str, request): + redir = redir_url(request, auth_callback_path) + user_info = client.retr_info(code, redir) + user_id = info[client.id_key] + return P(f"User id: {user_id}") +``` + +But we want the user ID not to print it but to remember the user. + +So let us store it in the `session` object, to remember who is logged +in: + +``` python +@app.get(auth_callback_path) +def auth_redirect(code:str, request, session): + redir = redir_url(request, auth_callback_path) + user_info = client.retr_info(code, redir) + user_id = user_info[client.id_key] # get their ID + session['user_id'] = user_id # save ID in the session + return RedirectResponse('/', status_code=303) +``` + +The session object is derived from values visible to the user’s browser, +but it is cryptographically signed so the user can’t read it themselves. +This makes it safe to store even information we don’t want to expose to +the user. + +For larger quantities of data, we’d want to save that information in a +database and use the session to hold keys to lookup information from +that database. + +Here’s a minimal app that puts all these pieces together. It uses the +user info to get the user_id. It stores that in the session object. It +then uses the user_id as a key into a database, which tracks how +frequently every user has hit an increment button. + +``` python +import os +from fasthtml.common import * +from fasthtml.oauth import GitHubAppClient, redir_url + +db = database('data/counts.db') +counts = db.t.counts +if counts not in db.t: counts.create(dict(name=str, count=int), pk='name') +Count = counts.dataclass() + +# Auth client setup for GitHub +client = GitHubAppClient(os.getenv("AUTH_CLIENT_ID"), + os.getenv("AUTH_CLIENT_SECRET")) +auth_callback_path = "/auth_redirect" + +def before(req, session): + # if not logged in, we send them to our login page + # logged in means: + # - 'user_id' in the session object, + # - 'auth' in the request object + auth = req.scope['auth'] = session.get('user_id', None) + if not auth: return RedirectResponse('/login', status_code=303) + counts.xtra(name=auth) +bware = Beforeware(before, skip=['/login', auth_callback_path]) + +app = FastHTML(before=bware) + +# User asks us to Login +@app.get('/login') +def login(request): + redir = redir_url(request,auth_callback_path) + login_link = client.login_link(redir) + # we tell user to login at github + return P(A('Login with GitHub', href=login_link)) + +# User comes back to us with an auth code from Github +@app.get(auth_callback_path) +def auth_redirect(code:str, request, session): + redir = redir_url(request, auth_callback_path) + user_info = client.retr_info(code, redir) + user_id = user_info[client.id_key] # get their ID + session['user_id'] = user_id # save ID in the session + # create a db entry for the user + if user_id not in counts: counts.insert(name=user_id, count=0) + return RedirectResponse('/', status_code=303) + +@app.get('/') +def home(auth): + return Div( + P("Count demo"), + P(f"Count: ", Span(counts[auth].count, id='count')), + Button('Increment', hx_get='/increment', hx_target='#count'), + P(A('Logout', href='/logout')) + ) + +@app.get('/increment') +def increment(auth): + c = counts[auth] + c.count += 1 + return counts.upsert(c).count + +@app.get('/logout') +def logout(session): + session.pop('user_id', None) + return RedirectResponse('/login', status_code=303) + +serve() +``` + +Some things to note: + +- The [`before`](https://www.fastht.ml/docs/explains/stripe.html#before) + function is used to check if the user is authenticated. If not, they + are redirected to the login page. +- To log the user out, we remove the user ID from the session. +- Calling `counts.xtra(name=auth)` ensures that only the row + corresponding to the current user is accessible when responding to a + request. This is often nicer than trying to remember to filter the + data in every route, and lowers the risk of accidentally leaking data. +- In the `auth_redirect` route, we store the user ID in the session and + create a new row in the `user_counts` table if it doesn’t already + exist. + +You can find more heavily-commented version of this code in the [oauth +directory in +fasthtml-example](https://github.com/AnswerDotAI/fasthtml-example/tree/main/oauth_example), +along with an even more minimal example. More examples may be added in +the future. + +### Revoking Tokens (Google) + +When the user in the example above logs out, we remove their user ID +from the session. However, the user is still logged in to GitHub. If +they click ‘Login with GitHub’ again, they’ll be redirected back to our +site without having to log in again. This is because GitHub remembers +that they’ve already granted our app permission to access their account. +Most of the time this is convenient, but for testing or security +purposes you may want a way to revoke this permission. + +As a user, you can usually revoke access to an app from the provider’s +website (for example, ). But +as a developer, you can also revoke access programmatically - at least +with some providers. This requires keeping track of the access token +(stored in `client.token["access_token"]` after you call `retr_info`), +and sending a request to the provider’s revoke URL: + +``` python +auth_revoke_url = "https://accounts.google.com/o/oauth2/revoke" +def revoke_token(token): + response = requests.post(auth_revoke_url, params={"token": token}) + return response.status_code == 200 # True if successful +``` + +Not all providers support token revocation, and it is not built into +FastHTML clients at the moment. + +### Using State (Hugging Face) + +Imagine a user (not logged in) comes to your AI image editing site, +starts testing things out, and then realizes they need to sign in before +they can click “Run (Pro)” on the edit they’re working on. They click +“Sign in with Hugging Face”, log in, and are redirected back to your +site. But now they’ve lost their in-progress edit and are left just +looking at the homepage! This is an example of a case where you might +want to keep track of some additional state. Another strong use case for +being able to pass some uniqie state through the OAuth flow is to +prevent something called a [CSRF +attack](https://en.wikipedia.org/wiki/Cross-site_request_forgery). To +add a state string to the OAuth flow, include a `state` argument when +creating the login link: + +``` python +# in login page: +link = A('Login with GitHub', href=client.login_link(state='current_prompt: add a unicorn')) + +# in auth_redirect: +@app.get('/auth_redirect') +def auth_redirect(code:str, session, state:str=None): + print(f"state: {state}") # Use as needed + ... +``` + +The state string is passed through the OAuth flow and back to your site. + +### A Work in Progress + +This page (and OAuth support in FastHTML) is a work in progress. +Questions, PRs, and feedback are welcome! diff --git a/docs/explains/refund.png b/docs/explains/refund.png new file mode 100644 index 0000000000000000000000000000000000000000..6b3c2178c90fe17b8097621164feaf342b064ce1 --- /dev/null +++ b/docs/explains/refund.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4590ebc54f2e558ffca0b3514f96c9b9e8ceebbb628afa1163dbdb04d53edb4e +size 228169 diff --git a/docs/explains/routes.html b/docs/explains/routes.html new file mode 100644 index 0000000000000000000000000000000000000000..e0e3635fad1a8e9a511a53462e6b40ef6c23a67f --- /dev/null +++ b/docs/explains/routes.html @@ -0,0 +1,1029 @@ + + + + + + + + + +Routes – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Routes

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

Behaviour in FastHTML apps is defined by routes. The syntax is largely the same as the wonderful FastAPI (which is what you should be using instead of this if you’re creating a JSON service. FastHTML is mainly for making HTML web apps, not APIs).

+
+
+
+ +
+
+Unfinished +
+
+
+

We haven’t yet written complete documentation of all of FastHTML’s routing features – until we add that, the best place to see all the available functionality is to look over the tests

+
+
+

Note that you need to include the types of your parameters, so that FastHTML knows what to pass to your function. Here, we’re just expecting a string:

+
+
from fasthtml.common import *
+
+
+
app = FastHTML()
+
+@app.get('/user/{nm}')
+def get_nm(nm:str): return f"Good day to you, {nm}!"
+
+

Normally you’d save this into a file such as main.py, and then run it in uvicorn using:

+
uvicorn main:app
+

However, for testing, we can use Starlette’s TestClient to try it out:

+
+
from starlette.testclient import TestClient
+
+
+
client = TestClient(app)
+r = client.get('/user/Jeremy')
+r
+
+
<Response [200 OK]>
+
+
+

TestClient uses httpx behind the scenes, so it returns a httpx.Response, which has a text attribute with our response body:

+
+
r.text
+
+
'Good day to you, Jeremy!'
+
+
+

In the previous example, the function name (get_nm) didn’t actually matter – we could have just called it _, for instance, since we never actually call it directly. It’s just called through HTTP. In fact, we often do call our functions _ when using this style of route, since that’s one less thing we have to worry about, naming.

+

An alternative approach to creating a route is to use app.route instead, in which case, you make the function name the HTTP method you want. Since this is such a common pattern, you might like to give a shorter name to app.route – we normally use rt:

+
+
rt = app.route
+
+@rt('/')
+def post(): return "Going postal!"
+
+client.post('/').text
+
+
'Going postal!'
+
+
+
+

Route-specific functionality

+

FastHTML supports custom decorators for adding specific functionality to routes. This allows you to implement authentication, authorization, middleware, or other custom behaviors for individual routes.

+

Here’s an example of a basic authentication decorator:

+
+
from functools import wraps
+
+def basic_auth(f):
+    @wraps(f)
+    async def wrapper(req, *args, **kwargs):
+        token = req.headers.get("Authorization")
+        if token == 'abc123':
+            return await f(req, *args, **kwargs)
+        return Response('Not Authorized', status_code=401)
+    return wrapper
+
+@app.get("/protected")
+@basic_auth
+async def protected(req):
+    return "Protected Content"
+
+client.get('/protected', headers={'Authorization': 'abc123'}).text
+
+
'Protected Content'
+
+
+

The decorator intercepts the request before the route function executes. If the decorator allows the request to proceed, it calls the original route function, passing along the request and any other arguments.

+

One of the key advantages of this approach is the ability to apply different behaviors to different routes. You can also stack multiple decorators on a single route for combined functionality.

+
+
def app_beforeware():
+    print('App level beforeware')
+
+app = FastHTML(before=Beforeware(app_beforeware))
+client = TestClient(app)
+
+def route_beforeware(f):
+    @wraps(f)
+    async def decorator(*args, **kwargs):
+        print('Route level beforeware')
+        return await f(*args, **kwargs)
+    return decorator
+    
+def second_route_beforeware(f):
+    @wraps(f)
+    async def decorator(*args, **kwargs):
+        print('Second route level beforeware')
+        return await f(*args, **kwargs)
+    return decorator
+
+@app.get("/users")
+@route_beforeware
+@second_route_beforeware
+async def users():
+    return "Users Page"
+
+client.get('/users').text
+
+
App level beforeware
+Route level beforeware
+Second route level beforeware
+
+
+
'Users Page'
+
+
+

This flexiblity allows for granular control over route behaviour, enabling you to tailor each endpoint’s functionality as needed. While app-level beforeware remains useful for global operations, decorators provide a powerful tool for route-specific customization.

+
+
+

Combining Routes

+

Sometimes a FastHTML project can grow so weildy that putting all the routes into main.py becomes unweildy. Or, we install a FastHTML- or Starlette-based package that requires us to add routes.

+

First let’s create a books.py module, that represents all the user-related views:

+
+
# books.py
+books_app, rt = fast_app()
+
+books = ['A Guide to FastHTML', 'FastHTML Cookbook', 'FastHTML in 24 Hours']
+
+@rt("/", name="list")
+def get():
+    return Titled("Books", *[P(book) for book in books])
+
+

Let’s mount it in our main module:

+
from books import books_app
+
+1app, rt = fast_app(routes=[Mount("/books", books_app, name="books")])
+
+@rt("/")
+def get():
+    return Titled("Dashboard",
+2        P(A(href="/books")("Books")),
+        Hr(),
+3        P(A(link=uri("books:list"))("Books")),
+    )
+
+serve()
+
+
1
+
+We use starlette.Mount to add the route to our routes list. We provide the name of books to make discovery and management of the links easier. More on that in items 2 and 3 of this annotations list +
+
2
+
+This example link to the books list view is hand-crafted. Obvious in purpose, it makes changing link patterns in the future harder +
+
3
+
+This example link uses the named URL route for the books. The advantage of this approach is it makes management of large numbers of link items easier. +
+
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/explains/routes.html.md b/docs/explains/routes.html.md new file mode 100644 index 0000000000000000000000000000000000000000..b4bfa63077b823110d73d1c94d217d938881ac26 --- /dev/null +++ b/docs/explains/routes.html.md @@ -0,0 +1,216 @@ +# Routes + + + + +Behaviour in FastHTML apps is defined by routes. The syntax is largely +the same as the wonderful [FastAPI](https://fastapi.tiangolo.com/) +(which is what you should be using instead of this if you’re creating a +JSON service. FastHTML is mainly for making HTML web apps, not APIs). + +
+ +> **Unfinished** +> +> We haven’t yet written complete documentation of all of FastHTML’s +> routing features – until we add that, the best place to see all the +> available functionality is to look over [the +> tests](../api/core.html#tests) + +
+ +Note that you need to include the types of your parameters, so that +[`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml) knows +what to pass to your function. Here, we’re just expecting a string: + +``` python +from fasthtml.common import * +``` + +``` python +app = FastHTML() + +@app.get('/user/{nm}') +def get_nm(nm:str): return f"Good day to you, {nm}!" +``` + +Normally you’d save this into a file such as main.py, and then run it in +`uvicorn` using: + + uvicorn main:app + +However, for testing, we can use Starlette’s `TestClient` to try it out: + +``` python +from starlette.testclient import TestClient +``` + +``` python +client = TestClient(app) +r = client.get('/user/Jeremy') +r +``` + + + +TestClient uses `httpx` behind the scenes, so it returns a +`httpx.Response`, which has a `text` attribute with our response body: + +``` python +r.text +``` + + 'Good day to you, Jeremy!' + +In the previous example, the function name (`get_nm`) didn’t actually +matter – we could have just called it `_`, for instance, since we never +actually call it directly. It’s just called through HTTP. In fact, we +often do call our functions `_` when using this style of route, since +that’s one less thing we have to worry about, naming. + +An alternative approach to creating a route is to use `app.route` +instead, in which case, you make the function name the HTTP method you +want. Since this is such a common pattern, you might like to give a +shorter name to `app.route` – we normally use `rt`: + +``` python +rt = app.route + +@rt('/') +def post(): return "Going postal!" + +client.post('/').text +``` + + 'Going postal!' + +### Route-specific functionality + +FastHTML supports custom decorators for adding specific functionality to +routes. This allows you to implement authentication, authorization, +middleware, or other custom behaviors for individual routes. + +Here’s an example of a basic authentication decorator: + +``` python +from functools import wraps + +def basic_auth(f): + @wraps(f) + async def wrapper(req, *args, **kwargs): + token = req.headers.get("Authorization") + if token == 'abc123': + return await f(req, *args, **kwargs) + return Response('Not Authorized', status_code=401) + return wrapper + +@app.get("/protected") +@basic_auth +async def protected(req): + return "Protected Content" + +client.get('/protected', headers={'Authorization': 'abc123'}).text +``` + + 'Protected Content' + +The decorator intercepts the request before the route function executes. +If the decorator allows the request to proceed, it calls the original +route function, passing along the request and any other arguments. + +One of the key advantages of this approach is the ability to apply +different behaviors to different routes. You can also stack multiple +decorators on a single route for combined functionality. + +``` python +def app_beforeware(): + print('App level beforeware') + +app = FastHTML(before=Beforeware(app_beforeware)) +client = TestClient(app) + +def route_beforeware(f): + @wraps(f) + async def decorator(*args, **kwargs): + print('Route level beforeware') + return await f(*args, **kwargs) + return decorator + +def second_route_beforeware(f): + @wraps(f) + async def decorator(*args, **kwargs): + print('Second route level beforeware') + return await f(*args, **kwargs) + return decorator + +@app.get("/users") +@route_beforeware +@second_route_beforeware +async def users(): + return "Users Page" + +client.get('/users').text +``` + + App level beforeware + Route level beforeware + Second route level beforeware + + 'Users Page' + +This flexiblity allows for granular control over route behaviour, +enabling you to tailor each endpoint’s functionality as needed. While +app-level beforeware remains useful for global operations, decorators +provide a powerful tool for route-specific customization. + +## Combining Routes + +Sometimes a FastHTML project can grow so weildy that putting all the +routes into `main.py` becomes unweildy. Or, we install a FastHTML- or +Starlette-based package that requires us to add routes. + +First let’s create a `books.py` module, that represents all the +user-related views: + +``` python +# books.py +books_app, rt = fast_app() + +books = ['A Guide to FastHTML', 'FastHTML Cookbook', 'FastHTML in 24 Hours'] + +@rt("/", name="list") +def get(): + return Titled("Books", *[P(book) for book in books]) +``` + +Let’s mount it in our main module: + +``` python +from books import books_app + +app, rt = fast_app(routes=[Mount("/books", books_app, name="books")]) + +@rt("/") +def get(): + return Titled("Dashboard", + P(A(href="/books")("Books")), + Hr(), + P(A(link=uri("books:list"))("Books")), + ) + +serve() +``` + +Line 3 +We use `starlette.Mount` to add the route to our routes list. We provide +the name of `books` to make discovery and management of the links +easier. More on that in items 2 and 3 of this annotations list + +Line 8 +This example link to the books list view is hand-crafted. Obvious in +purpose, it makes changing link patterns in the future harder + +Line 10 +This example link uses the named URL route for the books. The advantage +of this approach is it makes management of large numbers of link items +easier. diff --git a/docs/explains/stripe.html b/docs/explains/stripe.html new file mode 100644 index 0000000000000000000000000000000000000000..0cca2f08a28fc70e9760991663e7d7d62b56978b --- /dev/null +++ b/docs/explains/stripe.html @@ -0,0 +1,1297 @@ + + + + + + + + + +Stripe – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Stripe

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

This guide will walk through a minimal example of working with a Stripe one-time payment link and webhook for secure reconciliation of payments.

+

To get started we can import the stripe library and authenticate with a Stripe API key that you can get from the stripe web UI.

+
+
+Exported source +
from fasthtml.common import *
+import os
+
+
+
+

Stripe Authentication

+

You can install stripe python sdk directly from pypi:

+
pip install stripe
+

Additionally, you need to install the stripe cli. You can find how to install it on your specific system in their docs here

+
+
# uncomment and execute if needed
+#!pip install stripe
+
+
+
+Exported source +
import stripe
+
+
+
+
+Exported source +
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY")
+DOMAIN_URL = os.environ.get("DOMAIN_URL", "http://localhost:5001")
+
+
+

You can get this API key from the Stripe Dashboard by going to this url.

+
+
+
+ +
+
+Note +
+
+
+

Note: Make sure you have Test mode turned on in the dashboard.

+
+
+

+

Make sure you are using a test key for this tutorial

+
+
assert 'test_' in stripe.api_key
+
+
+
+

Pre-app setup

+
+
+
+ +
+
+Tip +
+
+
+

Everything in the pre-app setup sections is a run once and not to be included in your web-app.

+
+
+
+

Create a product

+

You can run this to programatically create a Stripe Product with a Price. Typically, this is not something you do dynamically in your FastHTML app, but rather something you set up one time. You can also optionally do this on the Stripe Dashboard UI.

+
+
+Exported source +
def _search_app(app_nm:str, limit=1): 
+    "Checks for product based on app_nm and returns the product if it exists"
+    return stripe.Product.search(query=f"name:'{app_nm}' AND active:'True'", limit=limit).data
+
+def create_price(app_nm:str, amt:int, currency="usd") -> list[stripe.Price]:
+    "Create a product and bind it to a price object. If product already exist just return the price list."
+    existing_product = _search_app(app_nm)
+    if existing_product: 
+        return stripe.Price.list(product=existing_product[0].id).data
+    else:
+        product = stripe.Product.create(name=f"{app_nm}")
+        return [stripe.Price.create(product=product.id, unit_amount=amt, currency=currency)]
+
+def archive_price(app_nm:str):
+    "Archive a price - useful for cleanup if testing."
+    existing_products = _search_app(app_nm, limit=50)
+    for product in existing_products:
+        for price in stripe.Price.list(product=product.id).data: 
+            stripe.Price.modify(price.id, active=False)
+        stripe.Product.modify(product.id, active=False)
+
+
+
+
+
+ +
+
+Tip +
+
+
+

To do recurring payment, you would use recurring={"interval": "year"} or recurring={"interval": "month"} when creating your stripe price.

+
+
+
+
+Exported source +
app_nm = "[FastHTML Docs] Demo Product"
+price_list = create_price(app_nm, amt=1999)
+assert len(price_list) == 1, 'For this tutorial, we only have one price bound to our product.'
+price = price_list[0]
+
+
+
+
print(f"Price ID = {price.id}")
+
+
Price ID = price_1R1ZzcFrdmWPkpOp9M28ykjy
+
+
+
+
+

Create a webook

+

A webhook is simply a URL where your app listens for messages from Stripe. It provides a way for Stripe, the payment processor, to notify your application when something happens with a payment. Think of it like a delivery notification: when a customer completes a payment, Stripe needs to tell your application so you can update your records, send confirmation emails, or provide access to purchased content. It is simply a URL,

+

But your app needs to be sure every webhook event is actually coming from Stripe. That is, it needs to authenticate the notification. To do that, your app will need a webhook signing secret, which it uses to confirm that the notifications were signed by Stripe.

+

This secret is different from your Stripe API key. The Stripe API key lets you prove who you are to Stripe. The webhook signing secret lets you be sure messages from Stripe are coming from Stripe.

+

You will need a webhook signing secret whether your app is is running locally in test mode, or whether it is a real production app on running on a server. Here is how you get the webhook signing secret in these two cases.

+
+

Local Webhook

+

When your application runs locally during development it can be reached only from your computer, so Stripe can’t make an HTTP request against the webhook. To workaround this in development, the Stripe CLI tool creates a secure tunnel which forwards these webhook notifications from Stripe’s servers to your local application.

+

Run this command to start that tunnel:

+
stripe listen --forward-to http://localhost:5001/webhook
+

On success, that command will also tell you the webhook signing secret. Take the secret it gives you and set it as an environment variable.

+
export STRIPE_LOCAL_TEST_WEBHOOK_SECRET=<your-secret>
+
+
+

Production Webhook

+

For a deployed app, you configure a permanent webhook connection in your Stripe Dashboard. This establishes an official notification channel where Stripe will send real-time updates about payments to your application’s /webhook URL.

+

On the dashboard, you can configure which specific payment event notifications will go to this webhook (e.g., completed checkouts, successful payments, failed payments, etc..). Your app provides the webhook signing secret to the Stripe library, to authenticate that these notifications come from the Stripe service. This is essential for production environments where your app needs to automatically respond to payment activities without manual intervention.

+

To configure the permanent webhook connection, you need to do the following steps:

+
    +
  1. Make sure you are in Test mode like before

  2. +
  3. Go to https://dashboard.stripe.com/test/webhooks

  4. +
  5. Click “+ Add endpoint” to create create a new webhook (or, if that is missing, click “Create an event destination”).

  6. +
  7. On the primary screen shown below, “Listen to Stripe events”, fill out the details. Your Endpoint URL will be https://YOURDOMAIN/webhook

  8. +
  9. Save your webhook signing scret. On the “Listen to Stripe events” screen, you can find it in the app sample code on the right hand side as the “endpoint secret”. You can also retrieve it later from the dashboard.

  10. +
+

+

You also need to configure which events should generate webhook notifications:

+
    +
  1. Click “+ Select events” to open the secondary control screen, “Select events to send”, which is shown below. In on our case we want to listen for checkout.session.completed.

  2. +
  3. Click the “Add Events” button, to confirm which events to send.

  4. +
+

+
+
+
+ +
+
+Tip +
+
+
+

For subscriptions you may also want to enable additional events for your webhook such as: customer.subscription.created, customer.subscription.deleted, and others based on your use-case.

+

+
+
+

Finally, click “Add Endpoint”, to finish configuring the endpoint.

+
+
+
+
+

App

+
+
+
+ +
+
+Tip +
+
+
+

Everything after this point is going to be included in your actual application. The application created in this tutorial can be found here

+
+
+
+

Setup to have the right information

+

In order to accept a payment, you need to know who is making the payment.

+

There are many ways to accomplish this, for example using oauth or a form. For this example we will start by hardcoding an email address into a session to simulate what it would look like with oauth.

+

We save the email address into the session object, under the key auth. By putting this logic into beforeware, which runs before every request is processed, we ensure that every route handler will be able to read that address from the session object.

+
+
+Exported source +
def before(sess): sess['auth'] = 'hamel@hamel.com'
+bware = Beforeware(before, skip=['/webhook'])
+app, rt = fast_app(before=bware)
+
+
+

We will need our webhook secret that was created. For this tutorial, we will be using the local development environment variable that was created above. For your deployed production environment, you will need to get the secret for your webhook from the Stripe Dashboard.

+
+
+Exported source +
WEBHOOK_SECRET = os.getenv("STRIPE_LOCAL_TEST_WEBHOOK_SECRET")
+
+
+
+
+

Payment Setup

+

We need 2 things first:

+
    +
  1. A button for users to click to pay
  2. +
  3. A route that gives stripe the information it needs to process the payment
  4. +
+
+
+Exported source +
@rt("/")
+def home(sess):
+    auth = sess['auth']
+    return Titled(
+        "Buy Now", 
+        Div(H2("Demo Product - $19.99"),
+            P(f"Welcome, {auth}"),
+            Button("Buy Now", hx_post="/create-checkout-session", hx_swap="none"),
+            A("View Account", href="/account")))
+
+
+

We are only allowing card payments (payment_method_types=['card']). For additional options see the Stripe docs.

+
+
+Exported source +
@rt("/create-checkout-session", methods=["POST"])
+async def create_checkout_session(sess):
+    checkout_session = stripe.checkout.Session.create(
+        line_items=[{'price': price.id, 'quantity': 1}],
+        mode='payment',
+        payment_method_types=['card'],
+        customer_email=sess['auth'],
+        metadata={'app_name': app_nm, 
+                  'AnyOther': 'Metadata',},
+        # CHECKOUT_SESSION_ID is a special variable Stripe fills in for you
+        success_url=DOMAIN_URL + '/success?checkout_sid={CHECKOUT_SESSION_ID}',
+        cancel_url=DOMAIN_URL + '/cancel')
+    return Redirect(checkout_session.url)
+
+
+
+
+
+ +
+
+Tip +
+
+
+

For subscriptions the mode would typically be subscription instead of payment

+
+
+

This section creates two key components: a simple webpage with a “Buy Now” button, and a function that handles what happens when that button is clicked.

+

When a customer clicks “Buy Now,” the app creates a Stripe checkout session (essentially a payment page) with product details, price, and customer information. Stripe then takes over the payment process, showing the customer a secure payment form. After payment is completed or canceled, Stripe redirects the customer back to your app using the success or cancel URLs you specified. This approach keeps sensitive payment details off your server, as Stripe handles the actual transaction.

+
+
+

Post-Payment Processing

+

After a customer initiates payment, there are two parallel processes:

+
    +
  1. User Experience Flow: The customer is redirected to Stripe’s checkout page, completes payment, and is then redirected back to your application (either the success or cancel page).

  2. +
  3. Backend Processing Flow: Stripe sends webhook notifications to your server about payment events, allowing your application to update records, provision access, or trigger other business logic.

  4. +
+

This dual-track approach ensures both a smooth user experience and reliable payment processing.

+

The webhook notification is critical as it’s a reliable way to confirm payment completion.

+
+

Backend Processing Flow

+

Create a database schema with the information you’d like to store.

+
+
+Exported source +
# Database Table
+class Payment:
+    checkout_session_id: str  # Stripe checkout session ID (primary key)
+    email: str
+    amount: int  # Amount paid in cents
+    payment_status: str  # paid, pending, failed
+    created_at: int # Unix timestamp
+    metadata: str  # Additional payment metadata as JSON
+
+
+

Connect to the database

+
+
+Exported source +
db = Database("stripe_payments.db")
+payments = db.create(Payment, pk='checkout_session_id', transform=True)
+
+
+

In our webhook we can execute any business logic and database updating we need to.

+
+
+Exported source +
@rt("/webhook")
+async def post(req):
+    payload = await req.body()
+    # Verify the event came from Stripe
+    try:
+        event = stripe.Webhook.construct_event(
+            payload, req.headers.get("stripe-signature"), WEBHOOK_SECRET)
+    except Exception as e:
+        print(f"Webhook error: {e}")
+        return
+    if event and event.type == "checkout.session.completed":
+        event_data = event.data.object
+        if event_data.metadata.get('app_name') == app_nm:
+            payment = Payment(
+                checkout_session_id=event_data.id,
+                email=event_data.customer_email,
+                amount=event_data.amount_total,
+                payment_status=event_data.payment_status,
+                created_at=event_data.created,
+                metadata=str(event_data.metadata))
+            payments.insert(payment)
+            print(f"Payment recorded for user: {event_data.customer_email}")
+            
+    # Do not worry about refunds yet, we will cover how to do this later in the tutorial
+    elif event and event.type == "charge.refunded":
+        event_data = event.data.object
+        payment_intent_id = event_data.payment_intent
+        sessions = stripe.checkout.Session.list(payment_intent=payment_intent_id)
+        if sessions and sessions.data:
+            checkout_sid = sessions.data[0].id
+            payments.update(Payment(checkout_session_id= checkout_sid, payment_status="refunded"))
+            print(f"Refund recorded for payment: {checkout_sid}")
+
+
+

The webhook route is where Stripe sends automated notifications about payment events. When a payment is completed, Stripe sends a secure notification to this endpoint. The code verifies this notification is legitimate using the webhook secret, then processes the event data - extracting information like the customer’s email and payment status. This allows your application to automatically update user accounts, trigger fulfillment processes, or record transaction details without manual intervention.

+

Note that in this route, our code extracts the user’s email address from the Stripe event, not from the session object. That is the because this route will be hit by a request from Stripe’s servers, not from the user’s browser.

+
+
+
+ +
+
+Tip +
+
+
+

When doing a subscription, often you would add additional event types in an if statement to update your database appropriately with the subscription status.

+
if event.type == "payment_intent.succeeded":
+    ...
+elif event.type == "customer.subscription.created":
+    ...
+elif event.type == "customer.subscription.deleted":
+    ...
+
+
+
+
+

User Experience Flow

+

The /success route is where Stripe will redirect the user after the payment completes successfully, which will also be after Stripe has called the webhook to inform your app of the transaction.

+

Stripe knows to send the user here, because you provided Stripe with this route when you created a checkout session.

+

But you want to verify this is the case. So in this route, you should verify the user’s payment status, by checking your database for the entry which your app saved when it received that webhook notification.

+
+
+Exported source +
@rt("/success")
+def success(sess, checkout_sid:str):    
+    # Get payment record from database (saved in the webhook)
+    payment = payments[checkout_sid]
+
+    if not payment or payment.payment_status != 'paid': 
+        return Titled("Error", P("Payment not found"))
+
+    return Titled(
+        "Success",
+        Div(H2("Payment Successful!"),
+            P(f"Thank you for your purchase, {sess['auth']}"),
+            P(f"Amount Paid: ${payment.amount / 100:.2f}"),
+            P(f"Status: {payment.payment_status}"),
+            P(f"Transaction ID: {payment.checkout_session_id}"),
+            A("Back to Home", href="/")))
+
+
+

There is also a /cancel route, where Stripe will redirect the user if they canceled the checkout.

+
+
+Exported source +
@rt("/cancel")
+def cancel():
+    return Titled(
+        "Cancelled",
+        Div(H2("Payment Cancelled"),
+            P("Your payment was cancelled."),
+            A("Back to Home", href="/")))
+
+
+

This image shows Stripe’s payment page that customers see after clicking the “Buy Now” button. When your app redirects to the Stripe checkout URL, Stripe displays this secure payment form where customers enter their card details. For testing purposes, you can use Stripe’s test card number (4242 4242 4242 4242) with any future expiration date and any 3-digit CVC code. This test card will successfully process payments in test mode without charging real money. The form shows the product name and price that were configured in your Stripe session, providing a seamless transition from your app to the payment processor and back again after completion.

+

+

Once you have processed the payments you can see each record in the sqlite database that was stored in the webhook.

+

Next, we can see how to add the refund route

+

In order to use a refund capability we need an account management page where users can request refunds for their payments.

+

When you initiate a refund, you can see the status of the refund in your Stripe dasbhoard at https://dashboard.stripe.com/payments, or https://dashboard.stripe.com/test/payments if you are in Test mode

+

It will look like this with a Refunded icon:

+

+
+
+
+
+

Recap

+

In this tutorial, we learned how to implement and test a complete Stripe payment flow including:

+
    +
  1. Creating test products and prices
  2. +
  3. Setting up a payment page and checkout session
  4. +
  5. Webhook handling for secure payment verification
  6. +
  7. Building success/cancel pages for the user experience
  8. +
  9. Adding refund functionality
  10. +
  11. Creating an account management page to view payment history
  12. +
+

When migrating this payment system to production, you’ll need to create actual products, prices and webhooks in your Stripe dashboard rather than test ones. You’ll also need to replace your test API keys with live Stripe API keys.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/explains/stripe.html.md b/docs/explains/stripe.html.md new file mode 100644 index 0000000000000000000000000000000000000000..723942ae511bdbcbdb0fa48547dbc0f07d214607 --- /dev/null +++ b/docs/explains/stripe.html.md @@ -0,0 +1,608 @@ +# Stripe + + + + +This guide will walk through a minimal example of working with a Stripe +one-time payment link and webhook for secure reconciliation of payments. + +To get started we can import the stripe library and authenticate with a +**Stripe API key** that you can get from the stripe web UI. + +
+Exported source + +``` python +from fasthtml.common import * +import os +``` + +
+ +## Stripe Authentication + +You can install stripe python sdk directly from pypi: + +``` sh +pip install stripe +``` + +Additionally, you need to install the stripe cli. You can find how to +install it on your specific system in their docs +[here](https://docs.stripe.com/get-started/development-environment?lang=python#setup-cli) + +``` python +# uncomment and execute if needed +#!pip install stripe +``` + +
+Exported source + +``` python +import stripe +``` + +
+ +
+Exported source + +``` python +stripe.api_key = os.environ.get("STRIPE_SECRET_KEY") +DOMAIN_URL = os.environ.get("DOMAIN_URL", "http://localhost:5001") +``` + +
+ +You can get this API key from the Stripe Dashboard by going to [this +url](https://dashboard.stripe.com/test/apikeys). + +
+ +> **Note** +> +> Note: Make sure you have `Test mode` turned on in the dashboard. + +
+ +![](StripeDashboard_API_Key.png) + +Make sure you are using a test key for this tutorial + +``` python +assert 'test_' in stripe.api_key +``` + +## Pre-app setup + +
+ +> **Tip** +> +> Everything in the pre-app setup sections is a run once and not to be +> included in your web-app. + +
+ +### Create a product + +You can run this to programatically create a Stripe **Product** with a +**Price**. Typically, this is not something you do dynamically in your +FastHTML app, but rather something you set up one time. You can also +optionally do this on the Stripe Dashboard UI. + +
+Exported source + +``` python +def _search_app(app_nm:str, limit=1): + "Checks for product based on app_nm and returns the product if it exists" + return stripe.Product.search(query=f"name:'{app_nm}' AND active:'True'", limit=limit).data + +def create_price(app_nm:str, amt:int, currency="usd") -> list[stripe.Price]: + "Create a product and bind it to a price object. If product already exist just return the price list." + existing_product = _search_app(app_nm) + if existing_product: + return stripe.Price.list(product=existing_product[0].id).data + else: + product = stripe.Product.create(name=f"{app_nm}") + return [stripe.Price.create(product=product.id, unit_amount=amt, currency=currency)] + +def archive_price(app_nm:str): + "Archive a price - useful for cleanup if testing." + existing_products = _search_app(app_nm, limit=50) + for product in existing_products: + for price in stripe.Price.list(product=product.id).data: + stripe.Price.modify(price.id, active=False) + stripe.Product.modify(product.id, active=False) +``` + +
+ +
+ +> **Tip** +> +> To do recurring payment, you would use +> `recurring={"interval": "year"}` or `recurring={"interval": "month"}` +> when creating your stripe price. + +
+ +
+Exported source + +``` python +app_nm = "[FastHTML Docs] Demo Product" +price_list = create_price(app_nm, amt=1999) +assert len(price_list) == 1, 'For this tutorial, we only have one price bound to our product.' +price = price_list[0] +``` + +
+ +``` python +print(f"Price ID = {price.id}") +``` + + Price ID = price_1R1ZzcFrdmWPkpOp9M28ykjy + +### Create a webook + +A webhook is simply a URL where your app listens for messages from +Stripe. It provides a way for Stripe, the payment processor, to notify +your application when something happens with a payment. Think of it like +a delivery notification: when a customer completes a payment, Stripe +needs to tell your application so you can update your records, send +confirmation emails, or provide access to purchased content. It is +simply a URL, + +But your app needs to be sure every webhook event is actually coming +from Stripe. That is, it needs to authenticate the notification. To do +that, your app will need a **webhook signing secret**, which it uses to +confirm that the notifications were signed by Stripe. + +This secret is different from your Stripe API key. The Stripe API key +lets you prove who you are to Stripe. The webhook signing secret lets +you be sure messages from Stripe are coming from Stripe. + +You will need a webhook signing secret whether your app is is running +locally in test mode, or whether it is a real production app on running +on a server. Here is how you get the webhook signing secret in these two +cases. + +#### Local Webhook + +When your application runs locally during development it can be reached +only from your computer, so Stripe can’t make an HTTP request against +the webhook. To workaround this in development, the Stripe CLI tool +creates a secure tunnel which forwards these webhook notifications from +Stripe’s servers to your local application. + +Run this command to start that tunnel: + +``` bash +stripe listen --forward-to http://localhost:5001/webhook +``` + +On success, that command will also tell you the webhook signing secret. +Take the secret it gives you and set it as an environment variable. + +``` bash +export STRIPE_LOCAL_TEST_WEBHOOK_SECRET= +``` + +#### Production Webhook + +For a deployed app, you configure a permanent webhook connection in your +Stripe Dashboard. This establishes an official notification channel +where Stripe will send real-time updates about payments to your +application’s `/webhook` URL. + +On the dashboard, you can configure which specific payment event +notifications will go to this webhook (e.g., completed checkouts, +successful payments, failed payments, etc..). Your app provides the +webhook signing secret to the Stripe library, to authenticate that these +notifications come from the Stripe service. This is essential for +production environments where your app needs to automatically respond to +payment activities without manual intervention. + +To configure the permanent webhook connection, you need to do the +following steps: + +1. Make sure you are in Test mode like before + +2. Go to https://dashboard.stripe.com/test/webhooks + +3. Click “+ Add endpoint” to create create a new webhook (or, if that + is missing, click “Create an event destination”). + +4. On the primary screen shown below, “Listen to Stripe events”, fill + out the details. Your Endpoint URL will be + `https://YOURDOMAIN/webhook` + +5. Save your webhook signing scret. On the “Listen to Stripe events” + screen, you can find it in the app sample code on the right hand + side as the “endpoint secret”. You can also retrieve it later from + the dashboard. + +![](CreateWebhook.png) + +You also need to configure which events should generate webhook +notifications: + +1. Click “+ Select events” to open the secondary control screen, + “Select events to send”, which is shown below. In on our case we + want to listen for `checkout.session.completed`. + +2. Click the “Add Events” button, to confirm which events to send. + +![](CreateWebhook2.png) + +
+ +> **Tip** +> +> For subscriptions you may also want to enable additional events for +> your webhook such as: `customer.subscription.created`, +> `customer.subscription.deleted`, and others based on your use-case. +> +> ![](SubscriptionEvents.png) + +
+ +Finally, click “Add Endpoint”, to finish configuring the endpoint. + +## App + +
+ +> **Tip** +> +> Everything after this point is going to be included in your actual +> application. The application created in this tutorial can be found +> [here](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/explains/stripe_otp.py) + +
+ +### Setup to have the right information + +In order to accept a payment, you need to know who is making the +payment. + +There are many ways to accomplish this, for example using +[oauth](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/explains/oauth.ipynb) +or a form. For this example we will start by hardcoding an email address +into a session to simulate what it would look like with oauth. + +We save the email address into the session object, under the key `auth`. +By putting this logic into beforeware, which runs before every request +is processed, we ensure that every route handler will be able to read +that address from the session object. + +
+Exported source + +``` python +def before(sess): sess['auth'] = 'hamel@hamel.com' +bware = Beforeware(before, skip=['/webhook']) +app, rt = fast_app(before=bware) +``` + +
+ +We will need our webhook secret that was created. For this tutorial, we +will be using the local development environment variable that was +created above. For your deployed production environment, you will need +to get the secret for your webhook from the Stripe Dashboard. + +
+Exported source + +``` python +WEBHOOK_SECRET = os.getenv("STRIPE_LOCAL_TEST_WEBHOOK_SECRET") +``` + +
+ +### Payment Setup + +We need 2 things first: + +1. A button for users to click to pay +2. A route that gives stripe the information it needs to process the + payment + +
+Exported source + +``` python +@rt("/") +def home(sess): + auth = sess['auth'] + return Titled( + "Buy Now", + Div(H2("Demo Product - $19.99"), + P(f"Welcome, {auth}"), + Button("Buy Now", hx_post="/create-checkout-session", hx_swap="none"), + A("View Account", href="/account"))) +``` + +
+ +We are only allowing card payments (`payment_method_types=['card']`). +For additional options see the [Stripe docs](https://docs.stripe.com/). + +
+Exported source + +``` python +@rt("/create-checkout-session", methods=["POST"]) +async def create_checkout_session(sess): + checkout_session = stripe.checkout.Session.create( + line_items=[{'price': price.id, 'quantity': 1}], + mode='payment', + payment_method_types=['card'], + customer_email=sess['auth'], + metadata={'app_name': app_nm, + 'AnyOther': 'Metadata',}, + # CHECKOUT_SESSION_ID is a special variable Stripe fills in for you + success_url=DOMAIN_URL + '/success?checkout_sid={CHECKOUT_SESSION_ID}', + cancel_url=DOMAIN_URL + '/cancel') + return Redirect(checkout_session.url) +``` + +
+ +
+ +> **Tip** +> +> For subscriptions the mode would typically be `subscription` instead +> of `payment` + +
+ +This section creates two key components: a simple webpage with a “Buy +Now” button, and a function that handles what happens when that button +is clicked. + +When a customer clicks “Buy Now,” the app creates a Stripe checkout +session (essentially a payment page) with product details, price, and +customer information. Stripe then takes over the payment process, +showing the customer a secure payment form. After payment is completed +or canceled, Stripe redirects the customer back to your app using the +success or cancel URLs you specified. This approach keeps sensitive +payment details off your server, as Stripe handles the actual +transaction. + +### Post-Payment Processing + +After a customer initiates payment, there are two parallel processes: + +1. **User Experience Flow**: The customer is redirected to Stripe’s + checkout page, completes payment, and is then redirected back to + your application (either the success or cancel page). + +2. **Backend Processing Flow**: Stripe sends webhook notifications to + your server about payment events, allowing your application to + update records, provision access, or trigger other business logic. + +This dual-track approach ensures both a smooth user experience and +reliable payment processing. + +The webhook notification is critical as it’s a reliable way to confirm +payment completion. + +#### Backend Processing Flow + +Create a database schema with the information you’d like to store. + +
+Exported source + +``` python +# Database Table +class Payment: + checkout_session_id: str # Stripe checkout session ID (primary key) + email: str + amount: int # Amount paid in cents + payment_status: str # paid, pending, failed + created_at: int # Unix timestamp + metadata: str # Additional payment metadata as JSON +``` + +
+ +Connect to the database + +
+Exported source + +``` python +db = Database("stripe_payments.db") +payments = db.create(Payment, pk='checkout_session_id', transform=True) +``` + +
+ +In our webhook we can execute any business logic and database updating +we need to. + +
+Exported source + +``` python +@rt("/webhook") +async def post(req): + payload = await req.body() + # Verify the event came from Stripe + try: + event = stripe.Webhook.construct_event( + payload, req.headers.get("stripe-signature"), WEBHOOK_SECRET) + except Exception as e: + print(f"Webhook error: {e}") + return + if event and event.type == "checkout.session.completed": + event_data = event.data.object + if event_data.metadata.get('app_name') == app_nm: + payment = Payment( + checkout_session_id=event_data.id, + email=event_data.customer_email, + amount=event_data.amount_total, + payment_status=event_data.payment_status, + created_at=event_data.created, + metadata=str(event_data.metadata)) + payments.insert(payment) + print(f"Payment recorded for user: {event_data.customer_email}") + + # Do not worry about refunds yet, we will cover how to do this later in the tutorial + elif event and event.type == "charge.refunded": + event_data = event.data.object + payment_intent_id = event_data.payment_intent + sessions = stripe.checkout.Session.list(payment_intent=payment_intent_id) + if sessions and sessions.data: + checkout_sid = sessions.data[0].id + payments.update(Payment(checkout_session_id= checkout_sid, payment_status="refunded")) + print(f"Refund recorded for payment: {checkout_sid}") +``` + +
+ +The webhook route is where Stripe sends automated notifications about +payment events. When a payment is completed, Stripe sends a secure +notification to this endpoint. The code verifies this notification is +legitimate using the webhook secret, then processes the event data - +extracting information like the customer’s email and payment status. +This allows your application to automatically update user accounts, +trigger fulfillment processes, or record transaction details without +manual intervention. + +Note that in this route, our code extracts the user’s email address from +the Stripe event, *not from the session object*. That is the because +this route will be hit by a request from Stripe’s servers, not from the +user’s browser. + +
+ +> **Tip** +> +> When doing a subscription, often you would add additional event types +> in an if statement to update your database appropriately with the +> subscription status. +> +> ``` python +> if event.type == "payment_intent.succeeded": +> ... +> elif event.type == "customer.subscription.created": +> ... +> elif event.type == "customer.subscription.deleted": +> ... +> ``` + +
+ +#### User Experience Flow + +The `/success` route is where Stripe will redirect the user *after* the +payment completes successfully, which will also be after Stripe has +called the webhook to inform your app of the transaction. + +Stripe knows to send the user here, because you provided Stripe with +this route when you created a checkout session. + +But you want to verify this is the case. So in this route, you should +verify the user’s payment status, by checking your database for the +entry which your app saved when it received that webhook notification. + +
+Exported source + +``` python +@rt("/success") +def success(sess, checkout_sid:str): + # Get payment record from database (saved in the webhook) + payment = payments[checkout_sid] + + if not payment or payment.payment_status != 'paid': + return Titled("Error", P("Payment not found")) + + return Titled( + "Success", + Div(H2("Payment Successful!"), + P(f"Thank you for your purchase, {sess['auth']}"), + P(f"Amount Paid: ${payment.amount / 100:.2f}"), + P(f"Status: {payment.payment_status}"), + P(f"Transaction ID: {payment.checkout_session_id}"), + A("Back to Home", href="/"))) +``` + +
+ +There is also a `/cancel` route, where Stripe will redirect the user if +they canceled the checkout. + +
+Exported source + +``` python +@rt("/cancel") +def cancel(): + return Titled( + "Cancelled", + Div(H2("Payment Cancelled"), + P("Your payment was cancelled."), + A("Back to Home", href="/"))) +``` + +
+ +This image shows Stripe’s payment page that customers see after clicking +the “Buy Now” button. When your app redirects to the Stripe checkout +URL, Stripe displays this secure payment form where customers enter +their card details. For testing purposes, you can use Stripe’s test card +number (4242 4242 4242 4242) with any future expiration date and any +3-digit CVC code. This test card will successfully process payments in +test mode without charging real money. The form shows the product name +and price that were configured in your Stripe session, providing a +seamless transition from your app to the payment processor and back +again after completion. + +![](StripePaymentPage.jpg) + +Once you have processed the payments you can see each record in the +sqlite database that was stored in the webhook. + +Next, we can see how to add the refund route + +In order to use a refund capability we need an account management page +where users can request refunds for their payments. + +When you initiate a refund, you can see the status of the refund in your +Stripe dasbhoard at +[`https://dashboard.stripe.com/payments`](https://dashboard.stripe.com/payments), +or +[`https://dashboard.stripe.com/test/payments`](https://dashboard.stripe.com/test/payments) +if you are in `Test mode` + +It will look like this with a `Refunded icon`: + +![](refund.png) + +# Recap + +In this tutorial, we learned how to implement and test a complete Stripe +payment flow including: + +1. Creating test products and prices +2. Setting up a payment page and checkout session +3. Webhook handling for secure payment verification +4. Building success/cancel pages for the user experience +5. Adding refund functionality +6. Creating an account management page to view payment history + +When migrating this payment system to production, you’ll need to create +actual products, prices and webhooks in your Stripe dashboard rather +than test ones. You’ll also need to replace your test API keys with live +Stripe API keys. diff --git a/docs/explains/websockets.html b/docs/explains/websockets.html new file mode 100644 index 0000000000000000000000000000000000000000..22ec1241cc20880fe82c73e403fce1d10b4ac021 --- /dev/null +++ b/docs/explains/websockets.html @@ -0,0 +1,985 @@ + + + + + + + + + +WebSockets – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

WebSockets

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

Websockets are a protocol for two-way, persistent communication between a client and server. This is different from HTTP, which uses a request/response model where the client sends a request and the server responds. With websockets, either party can send messages at any time, and the other party can respond.

+

This allows for different applications to be built, including things like chat apps, live-updating dashboards, and real-time collaborative tools, which would require constant polling of the server for updates with HTTP.

+

In FastHTML, you can create a websocket route using the @app.ws decorator. This decorator takes a route path, and optional conn and disconn parameters representing the on_connect and on_disconnect callbacks in websockets, respectively. The function decorated by @app.ws is the main function that is called when a message is received.

+

Here’s an example of a basic websocket route:

+
@app.ws('/ws', conn=on_conn, disconn=on_disconn)
+async def on_message(msg:str, send):
+    await send(Div('Hello ' + msg, id='notifications'))
+    await send(Div('Goodbye ' + msg, id='notifications'))
+

The on_message function is the main function that is called when a message is received and can be named however you like. Similar to standard routes, the arguments to on_message are automatically parsed from the websocket payload for you, so you don’t need to manually parse the message content. However, certain argument names are reserved for special purposes. Here are the most important ones:

+
    +
  • send is a function that can be used to send text data to the client.
  • +
  • data is a dictionary containing the data sent by the client.
  • +
  • ws is a reference to the websocket object.
  • +
+

For example, we can send a message to the client that just connected like this:

+
async def on_conn(send):
+    await send(Div('Hello, world!'))
+

Or if we receive a message from the client, we can send a message back to them:

+
@app.ws('/ws', conn=on_conn, disconn=on_disconn)
+async def on_message(msg:str, send):
+    await send(Div('You said: ' + msg, id='notifications'))
+    # or...
+    return Div('You said: ' + msg, id='notifications')
+

On the client side, we can use HTMX’s websocket extension to open a websocket connection and send/receive messages. For example:

+
from fasthtml.common import *
+
+app = FastHTML(exts='ws')
+
+@app.get('/')
+def home():
+    cts = Div(
+        Div(id='notifications'),
+        Form(Input(id='msg'), id='form', ws_send=True),
+        hx_ext='ws', ws_connect='/ws')
+    return Titled('Websocket Test', cts)
+

This will create a websocket connection to the server on route /ws, and send any form submissions to the server via the websocket. The server will then respond by sending a message back to the client. The client will then update the message div with the message from the server using Out of Band Swaps, which means that the content is swapped with the same id without reloading the page.

+
+
+
+ +
+
+Note +
+
+
+

Make sure you set exts='ws' when creating your FastHTML object if you want to use websockets so the extension is loaded.

+
+
+

Putting it all together, the code for the client and server should look like this:

+
from fasthtml.common import *
+
+app = FastHTML(exts='ws')
+rt = app.route
+
+@rt('/')
+def get():
+    cts = Div(
+        Div(id='notifications'),
+        Form(Input(id='msg'), id='form', ws_send=True),
+        hx_ext='ws', ws_connect='/ws')
+    return Titled('Websocket Test', cts)
+
+@app.ws('/ws')
+async def ws(msg:str, send):
+    await send(Div('Hello ' + msg, id='notifications'))
+
+serve()
+

This is a fairly simple example and could be done just as easily with standard HTTP requests, but it illustrates the basic idea of how websockets work. Let’s look at a more complex example next.

+
+

Session data in Websockets

+

Session data is shared between standard HTTP routes and Websockets. This means you can access, for example, logged in user ID inside websocket handler:

+
from fasthtml.common import *
+
+app = FastHTML(exts='ws')
+rt = app.route
+
+@rt('/login')
+def get(session):
+    session["person"] = "Bob"
+    return "ok"
+
+@app.ws('/ws')
+async def ws(msg:str, send, session):
+    await send(Div(f'Hello {session.get("person")}' + msg, id='notifications'))
+
+serve()
+
+
+

Real-Time Chat App

+

Let’s put our new websocket knowledge to use by building a simple chat app. We will create a chat app where multiple users can send and receive messages in real time.

+

Let’s start by defining the app and the home page:

+
from fasthtml.common import *
+
+app = FastHTML(exts='ws')
+rt = app.route
+
+msgs = []
+@rt('/')
+def home(): return Div(
+    Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
+    Form(Input(id='msg'), id='form', ws_send=True),
+    hx_ext='ws', ws_connect='/ws')
+

Now, let’s handle the websocket connection. We’ll add a new route for this along with an on_conn and on_disconn function to keep track of the users currently connected to the websocket. Finally, we will handle the logic for sending messages to all connected users.

+
users = {}
+def on_conn(ws, send): users[str(id(ws))] = send
+def on_disconn(ws): users.pop(str(id(ws)), None)
+
+@app.ws('/ws', conn=on_conn, disconn=on_disconn)
+async def ws(msg:str):
+    msgs.append(msg)
+    # Use associated `send` function to send message to each user
+    for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list'))
+
+serve()
+

We can now run this app with python chat_ws.py and open multiple browser tabs to http://localhost:5001. You should be able to send messages in one tab and see them appear in the other tabs.

+
+

A Work in Progress

+

This page (and Websocket support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!

+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/explains/websockets.html.md b/docs/explains/websockets.html.md new file mode 100644 index 0000000000000000000000000000000000000000..8865d7aa82cd727410d56eaee4e0b752dd2ebafd --- /dev/null +++ b/docs/explains/websockets.html.md @@ -0,0 +1,197 @@ +# WebSockets + + + + +Websockets are a protocol for two-way, persistent communication between +a client and server. This is different from HTTP, which uses a +request/response model where the client sends a request and the server +responds. With websockets, either party can send messages at any time, +and the other party can respond. + +This allows for different applications to be built, including things +like chat apps, live-updating dashboards, and real-time collaborative +tools, which would require constant polling of the server for updates +with HTTP. + +In FastHTML, you can create a websocket route using the `@app.ws` +decorator. This decorator takes a route path, and optional `conn` and +`disconn` parameters representing the `on_connect` and `on_disconnect` +callbacks in websockets, respectively. The function decorated by +`@app.ws` is the main function that is called when a message is +received. + +Here’s an example of a basic websocket route: + +``` python +@app.ws('/ws', conn=on_conn, disconn=on_disconn) +async def on_message(msg:str, send): + await send(Div('Hello ' + msg, id='notifications')) + await send(Div('Goodbye ' + msg, id='notifications')) +``` + +The `on_message` function is the main function that is called when a +message is received and can be named however you like. Similar to +standard routes, the arguments to `on_message` are automatically parsed +from the websocket payload for you, so you don’t need to manually parse +the message content. However, certain argument names are reserved for +special purposes. Here are the most important ones: + +- `send` is a function that can be used to send text data to the client. +- `data` is a dictionary containing the data sent by the client. +- `ws` is a reference to the websocket object. + +For example, we can send a message to the client that just connected +like this: + +``` python +async def on_conn(send): + await send(Div('Hello, world!')) +``` + +Or if we receive a message from the client, we can send a message back +to them: + +``` python +@app.ws('/ws', conn=on_conn, disconn=on_disconn) +async def on_message(msg:str, send): + await send(Div('You said: ' + msg, id='notifications')) + # or... + return Div('You said: ' + msg, id='notifications') +``` + +On the client side, we can use HTMX’s websocket extension to open a +websocket connection and send/receive messages. For example: + +``` python +from fasthtml.common import * + +app = FastHTML(exts='ws') + +@app.get('/') +def home(): + cts = Div( + Div(id='notifications'), + Form(Input(id='msg'), id='form', ws_send=True), + hx_ext='ws', ws_connect='/ws') + return Titled('Websocket Test', cts) +``` + +This will create a websocket connection to the server on route `/ws`, +and send any form submissions to the server via the websocket. The +server will then respond by sending a message back to the client. The +client will then update the message div with the message from the server +using Out of Band Swaps, which means that the content is swapped with +the same id without reloading the page. + +
+ +> **Note** +> +> Make sure you set `exts='ws'` when creating your +> [`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml) object +> if you want to use websockets so the extension is loaded. + +
+ +Putting it all together, the code for the client and server should look +like this: + +``` python +from fasthtml.common import * + +app = FastHTML(exts='ws') +rt = app.route + +@rt('/') +def get(): + cts = Div( + Div(id='notifications'), + Form(Input(id='msg'), id='form', ws_send=True), + hx_ext='ws', ws_connect='/ws') + return Titled('Websocket Test', cts) + +@app.ws('/ws') +async def ws(msg:str, send): + await send(Div('Hello ' + msg, id='notifications')) + +serve() +``` + +This is a fairly simple example and could be done just as easily with +standard HTTP requests, but it illustrates the basic idea of how +websockets work. Let’s look at a more complex example next. + +## Session data in Websockets + +Session data is shared between standard HTTP routes and Websockets. This +means you can access, for example, logged in user ID inside websocket +handler: + +``` python +from fasthtml.common import * + +app = FastHTML(exts='ws') +rt = app.route + +@rt('/login') +def get(session): + session["person"] = "Bob" + return "ok" + +@app.ws('/ws') +async def ws(msg:str, send, session): + await send(Div(f'Hello {session.get("person")}' + msg, id='notifications')) + +serve() +``` + +## Real-Time Chat App + +Let’s put our new websocket knowledge to use by building a simple chat +app. We will create a chat app where multiple users can send and receive +messages in real time. + +Let’s start by defining the app and the home page: + +``` python +from fasthtml.common import * + +app = FastHTML(exts='ws') +rt = app.route + +msgs = [] +@rt('/') +def home(): return Div( + Div(Ul(*[Li(m) for m in msgs], id='msg-list')), + Form(Input(id='msg'), id='form', ws_send=True), + hx_ext='ws', ws_connect='/ws') +``` + +Now, let’s handle the websocket connection. We’ll add a new route for +this along with an `on_conn` and `on_disconn` function to keep track of +the users currently connected to the websocket. Finally, we will handle +the logic for sending messages to all connected users. + +``` python +users = {} +def on_conn(ws, send): users[str(id(ws))] = send +def on_disconn(ws): users.pop(str(id(ws)), None) + +@app.ws('/ws', conn=on_conn, disconn=on_disconn) +async def ws(msg:str): + msgs.append(msg) + # Use associated `send` function to send message to each user + for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list')) + +serve() +``` + +We can now run this app with `python chat_ws.py` and open multiple +browser tabs to `http://localhost:5001`. You should be able to send +messages in one tab and see them appear in the other tabs. + +### A Work in Progress + +This page (and Websocket support in FastHTML) is a work in progress. +Questions, PRs, and feedback are welcome! diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..82ef3bab5996bc30a77e3fe0901081eed5eb8c61 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000000000000000000000000000000000000..c8988b54367ffd07e022462506a20be9b3020f11 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,975 @@ + + + + + + + + + + +FastHTML – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

FastHTML

+
+ +
+
+ The fastest, most powerful way to create an HTML app +
+
+ + +
+ + + + +
+ + + +
+ + + +

Welcome to the official FastHTML documentation.

+

FastHTML is a new next-generation web framework for fast, scalable web applications with minimal, compact code. It’s designed to be:

+
    +
  • Powerful and expressive enough to build the most advanced, interactive web apps you can imagine.
  • +
  • Fast and lightweight, so you can write less code and get more done.
  • +
  • Easy to learn and use, with a simple, intuitive syntax that makes it easy to build complex apps quickly.
  • +
+

FastHTML apps are just Python code, so you can use FastHTML with the full power of the Python language and ecosystem. FastHTML’s functionality maps 1:1 directly to HTML and HTTP, but allows them to be encapsulated using good software engineering practices—so you’ll need to understand these foundations to use this library fully. To understand how and why this works, please read this first: fastht.ml/about.

+
+

Installation

+

Since fasthtml is a Python library, you can install it with:

+
pip install python-fasthtml
+

In the near future, we hope to add component libraries that can likewise be installed via pip.

+
+
+

Usage

+

For a minimal app, create a file “main.py” as follows:

+
+
+
main.py
+
+
from fasthtml.common import *
+
+app,rt = fast_app()
+
+@rt('/')
+def get(): return Div(P('Hello World!'), hx_get="/change")
+
+serve()
+
+

Running the app with python main.py prints out a link to your running app: http://localhost:5001. Visit that link in your browser and you should see a page with the text “Hello World!”. Congratulations, you’ve just created your first FastHTML app!

+

Adding interactivity is surprisingly easy, thanks to HTMX. Modify the file to add this function:

+
+
+
main.py
+
+
@rt('/change')
+def get(): return P('Nice to be here!')
+
+

You now have a page with a clickable element that changes the text when clicked. When clicking on this link, the server will respond with an “HTML partial”—that is, just a snippet of HTML which will be inserted into the existing page. In this case, the returned element will replace the original P element (since that’s the default behavior of HTMX) with the new version returned by the second route.

+

This “hypermedia-based” approach to web development is a powerful way to build web applications.

+
+

Getting help from AI

+

Because FastHTML is newer than most LLMs, AI systems like Cursor, ChatGPT, Claude, and Copilot won’t give useful answers about it. To fix that problem, we’ve provided an LLM-friendly guide that teaches them how to use FastHTML. To use it, add this link for your AI helper to use:

+ +

This example is in a format based on recommendations from Anthropic for use with Claude Projects. This works so well that we’ve actually found that Claude can provide even better information than our own documentation! For instance, read through this annotated Claude chat for some great getting-started information, entirely generated from a project using the above text file as context.

+

If you use Cursor, type @doc then choose “Add new doc”, and use the /llms-ctx.txt link above. The context file is auto-generated from our llms.txt (our proposed standard for providing AI-friendly information)—you can generate alternative versions suitable for other models as needed.

+
+
+
+

Next Steps

+

Start with the official sources to learn more about FastHTML:

+
    +
  • About: Learn about the core ideas behind FastHTML
  • +
  • Documentation: Learn from examples how to write FastHTML code
  • +
  • Idiomatic app: Heavily commented source code walking through a complete application, including custom authentication, JS library connections, and database use.
  • +
+

We also have a 1-hour intro video:

+
+

The capabilities of FastHTML are vast and growing, and not all the features and patterns have been documented yet. Be prepared to invest time into studying and modifying source code, such as the main FastHTML repo’s notebooks and the official FastHTML examples repo:

+ +

Then explore the small but growing third-party ecosystem of FastHTML tutorials, notebooks, libraries, and components:

+ +

Finally, join the FastHTML community to ask questions, share your work, and learn from others:

+ +
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/index.html.md b/docs/index.html.md new file mode 100644 index 0000000000000000000000000000000000000000..8a328d69f67455fb824673d5dd9d4fd81cb1f3c8 --- /dev/null +++ b/docs/index.html.md @@ -0,0 +1,203 @@ +# FastHTML + + + + +Welcome to the official FastHTML documentation. + +FastHTML is a new next-generation web framework for fast, scalable web +applications with minimal, compact code. It’s designed to be: + +- Powerful and expressive enough to build the most advanced, interactive + web apps you can imagine. +- Fast and lightweight, so you can write less code and get more done. +- Easy to learn and use, with a simple, intuitive syntax that makes it + easy to build complex apps quickly. + +FastHTML apps are just Python code, so you can use FastHTML with the +full power of the Python language and ecosystem. FastHTML’s +functionality maps 1:1 directly to HTML and HTTP, but allows them to be +encapsulated using good software engineering practices—so you’ll need to +understand these foundations to use this library fully. To understand +how and why this works, please read this first: +[fastht.ml/about](https://fastht.ml/about). + +## Installation + +Since `fasthtml` is a Python library, you can install it with: + +``` sh +pip install python-fasthtml +``` + +In the near future, we hope to add component libraries that can likewise +be installed via `pip`. + +## Usage + +For a minimal app, create a file “main.py” as follows: + +
+ +**main.py** + +``` python +from fasthtml.common import * + +app,rt = fast_app() + +@rt('/') +def get(): return Div(P('Hello World!'), hx_get="/change") + +serve() +``` + +
+ +Running the app with `python main.py` prints out a link to your running +app: `http://localhost:5001`. Visit that link in your browser and you +should see a page with the text “Hello World!”. Congratulations, you’ve +just created your first FastHTML app! + +Adding interactivity is surprisingly easy, thanks to HTMX. Modify the +file to add this function: + +
+ +**main.py** + +``` python +@rt('/change') +def get(): return P('Nice to be here!') +``` + +
+ +You now have a page with a clickable element that changes the text when +clicked. When clicking on this link, the server will respond with an +“HTML partial”—that is, just a snippet of HTML which will be inserted +into the existing page. In this case, the returned element will replace +the original P element (since that’s the default behavior of HTMX) with +the new version returned by the second route. + +This “hypermedia-based” approach to web development is a powerful way to +build web applications. + +### Getting help from AI + +Because FastHTML is newer than most LLMs, AI systems like Cursor, +ChatGPT, Claude, and Copilot won’t give useful answers about it. To fix +that problem, we’ve provided an LLM-friendly guide that teaches them how +to use FastHTML. To use it, add this link for your AI helper to use: + +- [/llms-ctx.txt](https://www.fastht.ml/docs/llms-ctx.txt) + +This example is in a format based on recommendations from Anthropic for +use with [Claude +Projects](https://support.anthropic.com/en/articles/9517075-what-are-projects). +This works so well that we’ve actually found that Claude can provide +even better information than our own documentation! For instance, read +through [this annotated Claude +chat](https://gist.github.com/jph00/9559b0a563f6a370029bec1d1cc97b74) +for some great getting-started information, entirely generated from a +project using the above text file as context. + +If you use Cursor, type `@doc` then choose “*Add new doc*”, and use the +/llms-ctx.txt link above. The context file is auto-generated from our +[`llms.txt`](https://llmstxt.org/) (our proposed standard for providing +AI-friendly information)—you can generate alternative versions suitable +for other models as needed. + +## Next Steps + +Start with the official sources to learn more about FastHTML: + +- [About](https://fastht.ml/about): Learn about the core ideas behind + FastHTML +- [Documentation](https://www.fastht.ml/docs): Learn from examples how + to write FastHTML code +- [Idiomatic + app](https://github.com/AnswerDotAI/fasthtml/blob/main/examples/adv_app.py): + Heavily commented source code walking through a complete application, + including custom authentication, JS library connections, and database + use. + +We also have a 1-hour intro video: + + + +The capabilities of FastHTML are vast and growing, and not all the +features and patterns have been documented yet. Be prepared to invest +time into studying and modifying source code, such as the main FastHTML +repo’s notebooks and the official FastHTML examples repo: + +- [FastHTML Examples Repo on + GitHub](https://github.com/AnswerDotAI/fasthtml-example) +- [FastHTML Repo on GitHub](https://github.com/AnswerDotAI/fasthtml) + +Then explore the small but growing third-party ecosystem of FastHTML +tutorials, notebooks, libraries, and components: + +- [FastHTML Gallery](https://fastht.ml/gallery): Learn from minimal + examples of components (ie chat bubbles, click-to-edit, infinite + scroll, etc) +- [Creating Custom FastHTML Tags for Markdown + Rendering](https://isaac-flath.github.io/website/posts/boots/FasthtmlTutorial.html) + by Isaac Flath +- [How to Build a Simple Login System in + FastHTML](https://blog.mariusvach.com/posts/login-fasthtml) by Marius + Vach +- Your tutorial here! + +Finally, join the FastHTML community to ask questions, share your work, +and learn from others: + +- [Discord](https://discord.gg/qcXvcxMhdP) + +## Other languages and related projects + +If you’re not a Python user, or are keen to try out a new language, +we’ll list here other projects that have a similar approach to FastHTML. +(Please reach out if you know of any other projects that you’d like to +see added.) + +- [htmgo](https://htmgo.dev/) (Go): “*htmgo is a lightweight pure go way + to build interactive websites / web applications using go & htmx. By + combining the speed & simplicity of go + hypermedia attributes (htmx) + to add interactivity to websites, all conveniently wrapped in pure go, + you can build simple, fast, interactive websites without touching + javascript. All compiled to a single deployable binary*” + +If you’re just interested in functional HTML components, rather than a +full HTMX server solution, consider: + +- [fastcore.xml.FT](https://fastcore.fast.ai/xml.html): This is actually + what FastHTML uses behind the scenes +- [htpy](https://htpy.dev/): Similar to + [`fastcore.xml.FT`](https://fastcore.fast.ai/xml.html#ft), but with a + somewhat different syntax +- [elm-html](https://package.elm-lang.org/packages/elm/html/latest/): + Elm’s built-in HTML library with a type-safe functional approach +- [hiccup](https://github.com/weavejester/hiccup): Popular library for + representing HTML in Clojure using vectors +- [hiccl](https://github.com/garlic0x1/hiccl): HTML generation library + for Common Lisp inspired by Clojure’s Hiccup +- [Falco.Markup](https://github.com/pimbrouwers/Falco): F# HTML DSL and + web framework with type-safe HTML generation +- [Lucid](https://github.com/chrisdone/lucid): Type-safe HTML generation + for Haskell using monad transformers +- [dream-html](https://github.com/aantron/dream): Part of the Dream web + framework for OCaml, provides type-safe HTML templating + +For other hypermedia application platforms, not based on HTMX, take a +look at: + +- [Hotwire/Turbo](https://turbo.hotwired.dev/): Rails-oriented framework + that similarly uses HTML-over-the-wire +- [LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html): + Phoenix framework’s solution for building interactive web apps with + minimal JavaScript +- [Unpoly](https://unpoly.com/): Another HTML-over-the-wire framework + with progressive enhancement +- [Livewire](https://laravel-livewire.com/): Laravel’s take on building + dynamic interfaces with minimal JavaScript diff --git a/docs/listings.json b/docs/listings.json new file mode 100644 index 0000000000000000000000000000000000000000..a6559156aa5d205680d86d59f140667809bfdf3b --- /dev/null +++ b/docs/listings.json @@ -0,0 +1,12 @@ +[ + { + "listing": "/tutorials/index.html", + "items": [ + "/tutorials/by_example.html", + "/tutorials/quickstart_for_web_devs.html", + "/tutorials/e2e.html", + "/tutorials/best_practice.html", + "/tutorials/jupyter_and_fasthtml.html" + ] + } +] \ No newline at end of file diff --git a/docs/llms-ctx-full.txt b/docs/llms-ctx-full.txt new file mode 100644 index 0000000000000000000000000000000000000000..404aa00cb605e6b5013c1e6c487b136edf6b7a67 --- /dev/null +++ b/docs/llms-ctx-full.txt @@ -0,0 +1,10274 @@ +Things to remember when writing FastHTML apps: + +- Although parts of its API are inspired by FastAPI, it is *not* compatible with FastAPI syntax and is not targeted at creating API services +- FastHTML includes support for Pico CSS and the fastlite sqlite library, although using both are optional; sqlalchemy can be used directly or via the fastsql library, and any CSS framework can be used. Support for the Surreal and css-scope-inline libraries are also included, but both are optional +- FastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte +- Use `serve()` for running uvicorn (`if __name__ == "__main__"` is not needed since it's automatic) +- When a title is needed with a response, use `Titled`; note that that already wraps children in `Container`, and already includes both the meta title as well as the H1 element.# Concise reference + + + +## About FastHTML + +``` python +from fasthtml.common import * +``` + +FastHTML is a python library which brings together Starlette, Uvicorn, +HTMX, and fastcore’s `FT` “FastTags” into a library for creating +server-rendered hypermedia applications. The +[`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml) class +itself inherits from `Starlette`, and adds decorator-based routing with +many additions, Beforeware, automatic `FT` to HTML rendering, and much +more. + +Things to remember when writing FastHTML apps: + +- *Not* compatible with FastAPI syntax; FastHTML is for HTML-first apps, + not API services (although it can implement APIs too) +- FastHTML includes support for Pico CSS and the fastlite sqlite + library, although using both are optional; sqlalchemy can be used + directly or via the fastsql library, and any CSS framework can be + used. MonsterUI is a richer FastHTML-first component framework with + similar capabilities to shadcn +- FastHTML is compatible with JS-native web components and any vanilla + JS library, but not with React, Vue, or Svelte +- Use [`serve()`](https://www.fastht.ml/docs/api/core.html#serve) for + running uvicorn (`if __name__ == "__main__"` is not needed since it’s + automatic) +- When a title is needed with a response, use + [`Titled`](https://www.fastht.ml/docs/api/xtend.html#titled); note + that that already wraps children in + [`Container`](https://www.fastht.ml/docs/api/pico.html#container), and + already includes both the meta title as well as the H1 element. + +## Minimal App + +The code examples here use fast.ai style: prefer ternary op, 1-line +docstring, minimize vertical space, etc. (Normally fast.ai style uses +few if any comments, but they’re added here as documentation.) + +A minimal FastHTML app looks something like this: + +``` python +# Meta-package with all key symbols from FastHTML and Starlette. Import it like this at the start of every FastHTML app. +from fasthtml.common import * +# The FastHTML app object and shortcut to `app.route` +app,rt = fast_app() + +# Enums constrain the values accepted for a route parameter +name = str_enum('names', 'Alice', 'Bev', 'Charlie') + +# Passing a path to `rt` is optional. If not passed (recommended), the function name is the route ('/foo') +# Both GET and POST HTTP methods are handled by default +# Type-annotated params are passed as query params (recommended) unless a path param is defined (which it isn't here) +@rt +def foo(nm: name): + # `Title` and `P` here are FastTags: direct m-expression mappings of HTML tags to Python functions with positional and named parameters. All standard HTML tags are included in the common wildcard import. + # When a tuple is returned, this returns concatenated HTML partials. HTMX by default will use a title HTML partial to set the current page name. HEAD tags (e.g. Meta, Link, etc) in the returned tuple are automatically placed in HEAD; everything else is placed in BODY. + # FastHTML will automatically return a complete HTML document with appropriate headers if a normal HTTP request is received. For an HTMX request, however, just the partials are returned. + return Title("FastHTML"), H1("My web app"), P(f"Hello, {name}!") +# By default `serve` runs uvicorn on port 5001. Never write `if __name__ == "__main__"` since `serve` checks it internally. +serve() +``` + +To run this web app: + +``` bash +python main.py # access via localhost:5001 +``` + +## FastTags (aka FT Components or FTs) + +FTs are m-expressions plus simple sugar. Positional params map to +children. Named parameters map to attributes. Aliases must be used for +Python reserved words. + +``` python +tags = Title("FastHTML"), H1("My web app"), P(f"Let's do this!", cls="myclass") +tags +``` + + (title(('FastHTML',),{}), + h1(('My web app',),{}), + p(("Let's do this!",),{'class': 'myclass'})) + +This example shows key aspects of how FTs handle attributes: + +``` python +Label( + "Choose an option", + Select( + Option("one", value="1", selected=True), # True renders just the attribute name + Option("two", value=2, selected=False), # Non-string values are converted to strings. False omits the attribute entirely + cls="selector", id="counter", # 'cls' becomes 'class' + **{'@click':"alert('Clicked');"}, # Dict unpacking for attributes with special chars + ), + _for="counter", # '_for' becomes 'for' (can also use 'fr') +) +``` + +Classes with `__ft__` defined are rendered using that method. + +``` python +class FtTest: + def __ft__(self): return P('test') + +to_xml(FtTest()) +``` + + '

test

\n' + +You can create new FTs by importing the new component from +`fasthtml.components`. If the FT doesn’t exist within that module, +FastHTML will create it. + +``` python +from fasthtml.components import Some_never_before_used_tag + +Some_never_before_used_tag() +``` + +``` html + +``` + +FTs can be combined by defining them as a function. + +``` python +def Hero(title, statement): return Div(H1(title),P(statement), cls="hero") +to_xml(Hero("Hello World", "This is a hero statement")) +``` + + '
\n

Hello World

\n

This is a hero statement

\n
\n' + +When handling a response, FastHTML will automatically render FTs using +the `to_xml` function. + +``` python +to_xml(tags) +``` + + 'FastHTML\n

My web app

\n

Let's do this!

\n' + +## JS + +The [`Script`](https://www.fastht.ml/docs/api/xtend.html#script) +function allows you to include JavaScript. You can use Python to +generate parts of your JS or JSON like this: + +``` python +# In future snippets this import will not be shown, but is required +from fasthtml.common import * +app,rt = fast_app(hdrs=[Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js")]) +# `index` is a special function name which maps to the `/` route. +@rt +def index(): + data = {'somedata':'fill me in…'} + # `Titled` returns a title tag and an h1 tag with the 1st param, with remaining params as children in a `Main` parent. + return Titled("Chart Demo", Div(id="myDiv"), Script(f"var data = {data}; Plotly.newPlot('myDiv', data);")) +# In future snippets `serve() will not be shown, but is required +serve() +``` + +Prefer Python whenever possible over JS. Never use React or shadcn. + +## fast_app hdrs + +``` python +# In future snippets we'll skip showing the `fast_app` call if it has no params +app, rt = fast_app( + pico=False, # The Pico CSS framework is included by default, so pass `False` to disable it if needed. No other CSS frameworks are included. + # These are added to the `head` part of the page for non-HTMX requests. + hdrs=( + Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'), + Link(rel='stylesheet', href='assets/sakura.css', type='text/css'), + Style("p {color: red;}"), + # `MarkdownJS` and `HighlightJS` are available via concise functions + MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), + # by default, all standard static extensions are served statically from the web app dir, + # which can be modified using e.g `static_path='public'` + ) +) + +@rt +def index(req): return Titled("Markdown rendering example", + # This will be client-side rendered to HTML with highlight-js + Div("*hi* there",cls="marked"), + # This will be syntax highlighted + Pre(Code("def foo(): pass"))) +``` + +## Responses + +Routes can return various types: + +1. FastTags or tuples of FastTags (automatically rendered to HTML) +2. Standard Starlette responses (used directly) +3. JSON-serializable types (returned as JSON in a plain text response) + +``` python +@rt("/{fname:path}.{ext:static}") +async def serve_static_file(fname:str, ext:str): return FileResponse(f'public/{fname}.{ext}') + +app, rt = fast_app(hdrs=(MarkdownJS(), HighlightJS(langs=['python', 'javascript']))) +@rt +def index(): + return Titled("Example", + Div("*markdown* here", cls="marked"), + Pre(Code("def foo(): pass"))) +``` + +Route functions can be used in attributes like `href` or `action` and +will be converted to paths. Use `.to()` to generate paths with query +parameters. + +``` python +@rt +def profile(email:str): return fill_form(profile_form, profiles[email]) + +profile_form = Form(action=profile)( + Label("Email", Input(name="email")), + Button("Save", type="submit") +) + +user_profile_path = profile.to(email="user@example.com") # '/profile?email=user%40example.com' +``` + +``` python +from dataclasses import dataclass + +app,rt = fast_app() +``` + +When a route handler function is used as a fasttag attribute (such as +`href`, `hx_get`, or `action`) it is converted to that route’s path. +[`fill_form`](https://www.fastht.ml/docs/api/components.html#fill_form) +is used to copy an object’s matching attrs into matching-name form +fields. + +``` python +@dataclass +class Profile: email:str; phone:str; age:int +email = 'john@example.com' +profiles = {email: Profile(email=email, phone='123456789', age=5)} +@rt +def profile(email:str): return fill_form(profile_form, profiles[email]) + +profile_form = Form(method="post", action=profile)( + Fieldset( + Label('Email', Input(name="email")), + Label("Phone", Input(name="phone")), + Label("Age", Input(name="age"))), + Button("Save", type="submit")) +``` + +## Testing + +We can use `TestClient` for testing. + +``` python +from starlette.testclient import TestClient +``` + +``` python +path = "/profile?email=john@example.com" +client = TestClient(app) +htmx_req = {'HX-Request':'1'} +print(client.get(path, headers=htmx_req).text) +``` + +
+ +## Form Handling and Data Binding + +When a dataclass, namedtuple, etc. is used as a type annotation, the +form body will be unpacked into matching attribute names automatically. + +``` python +@rt +def edit_profile(profile: Profile): + profiles[email]=profile + return RedirectResponse(url=path) + +new_data = dict(email='john@example.com', phone='7654321', age=25) +print(client.post("/edit_profile", data=new_data, headers=htmx_req).text) +``` + +
+ +## fasttag Rendering Rules + +The general rules for rendering children inside tuples or fasttag +children are: - `__ft__` method will be called (for default components +like `P`, `H2`, etc. or if you define your own components) - If you pass +a string, it will be escaped - On other python objects, `str()` will be +called + +If you want to include plain HTML tags directly into e.g. a `Div()` they +will get escaped by default (as a security measure to avoid code +injections). This can be avoided by using `Safe(...)`, e.g to show a +data frame use `Div(NotStr(df.to_html()))`. + +## Exceptions + +FastHTML allows customization of exception handlers. + +``` python +def not_found(req, exc): return Titled("404: I don't exist!") +exception_handlers = {404: not_found} +app, rt = fast_app(exception_handlers=exception_handlers) +``` + +## Cookies + +We can set cookies using the +[`cookie()`](https://www.fastht.ml/docs/api/core.html#cookie) function. + +``` python +@rt +def setcook(): return P(f'Set'), cookie('mycookie', 'foobar') +print(client.get('/setcook', headers=htmx_req).text) +``` + +

Set

+ +``` python +@rt +def getcook(mycookie:str): return f'Got {mycookie}' +# If handlers return text instead of FTs, then a plaintext response is automatically created +print(client.get('/getcook').text) +``` + + Got foobar + +FastHTML provide access to Starlette’s request object automatically +using special `request` parameter name (or any prefix of that name). + +``` python +@rt +def headers(req): return req.headers['host'] +``` + +## Request and Session Objects + +FastHTML provides access to Starlette’s session middleware automatically +using the special `session` parameter name (or any prefix of that name). + +``` python +@rt +def profile(req, sess, user_id: int=None): + ip = req.client.host + sess['last_visit'] = datetime.now().isoformat() + visits = sess.setdefault('visit_count', 0) + 1 + sess['visit_count'] = visits + user = get_user(user_id or sess.get('user_id')) + return Titled(f"Profile: {user.name}", + P(f"Visits: {visits}"), + P(f"IP: {ip}"), + Button("Logout", hx_post=logout)) +``` + +Handler functions can return the +[`HtmxResponseHeaders`](https://www.fastht.ml/docs/api/core.html#htmxresponseheaders) +object to set HTMX-specific response headers. + +``` python +@rt +def htmlredirect(app): return HtmxResponseHeaders(location="http://example.org") +``` + +## APIRouter + +[`APIRouter`](https://www.fastht.ml/docs/api/core.html#apirouter) lets +you organize routes across multiple files in a FastHTML app. + +``` python +# products.py +ar = APIRouter() + +@ar +def details(pid: int): return f"Here are the product details for ID: {pid}" + +@ar +def all_products(req): + return Div( + Div( + Button("Details",hx_get=details.to(pid=42),hx_target="#products_list",hx_swap="outerHTML",), + ), id="products_list") +``` + +``` python +# main.py +from products import ar,all_products + +app, rt = fast_app() +ar.to_app(app) + +@rt +def index(): + return Div( + "Products", + hx_get=all_products, hx_swap="outerHTML") +``` + +## Toasts + +Toasts can be of four types: + +- info +- success +- warning +- error + +Toasts require the use of the `setup_toasts()` function, plus every +handler needs: + +- The session argument +- Must return FT components + +``` python +setup_toasts(app) + +@rt +def toasting(session): + add_toast(session, f"cooked", "info") + add_toast(session, f"ready", "success") + return Titled("toaster") +``` + +`setup_toasts(duration)` allows you to specify how long a toast will be +visible before disappearing.10 seconds. + +Authentication and authorization are handled with Beforeware, which +functions that run before the route handler is called. + +## Auth + +``` python +def user_auth_before(req, sess): + # `auth` key in the request scope is automatically provided to any handler which requests it and can not be injected + auth = req.scope['auth'] = sess.get('auth', None) + if not auth: return RedirectResponse('/login', status_code=303) + +beforeware = Beforeware( + user_auth_before, + skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/'] +) + +app, rt = fast_app(before=beforeware) +``` + +## Server-Side Events (SSE) + +FastHTML supports the HTMX SSE extension. + +``` python +import random +hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.3/sse.js"),) +app,rt = fast_app(hdrs=hdrs) + +@rt +def index(): return Div(hx_ext="sse", sse_connect="/numstream", hx_swap="beforeend show:bottom", sse_swap="message") + +# `signal_shutdown()` gets an event that is set on shutdown +shutdown_event = signal_shutdown() + +async def number_generator(): + while not shutdown_event.is_set(): + data = Article(random.randint(1, 100)) + yield sse_message(data) + +@rt +async def numstream(): return EventStream(number_generator()) +``` + +## Websockets + +FastHTML provides useful tools for HTMX’s websockets extension. + +``` python +# These HTMX extensions are available through `exts`: +# head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer +app, rt = fast_app(exts='ws') + +def mk_inp(): return Input(id='msg', autofocus=True) + +@rt +async def index(request): + # `ws_send` tells HTMX to send a message to the nearest websocket based on the trigger for the form element + cts = Div( + Div(id='notifications'), + Form(mk_inp(), id='form', ws_send=True), + hx_ext='ws', ws_connect='/ws') + return Titled('Websocket Test', cts) + +async def on_connect(send): await send(Div('Hello, you have connected', id="notifications")) +async def on_disconnect(ws): print('Disconnected!') + +@app.ws('/ws', conn=on_connect, disconn=on_disconnect) +async def ws(msg:str, send): + # websocket hander returns/sends are treated as OOB swaps + await send(Div('Hello ' + msg, id="notifications")) + return Div('Goodbye ' + msg, id="notifications"), mk_inp() +``` + +Sample chatbot that uses FastHTML’s +[`setup_ws`](https://www.fastht.ml/docs/api/core.html#setup_ws) +function: + +``` py +app = FastHTML(exts='ws') +rt = app.route +msgs = [] + +@rt('/') +def home(): + return Div(hx_ext='ws', ws_connect='/ws')( + Div(Ul(*[Li(m) for m in msgs], id='msg-list')), + Form(Input(id='msg'), id='form', ws_send=True) + ) + +async def ws(msg:str): + msgs.append(msg) + await send(Ul(*[Li(m) for m in msgs], id='msg-list')) + +send = setup_ws(app, ws) +``` + +### Single File Uploads + +[`Form`](https://www.fastht.ml/docs/api/xtend.html#form) defaults to +“multipart/form-data”. A Starlette UploadFile is passed to the handler. + +``` python +upload_dir = Path("filez") + +@rt +def index(): + return ( + Form(hx_post=upload, hx_target="#result")( + Input(type="file", name="file"), + Button("Upload", type="submit")), + Div(id="result") + ) + +# Use `async` handlers where IO is used to avoid blocking other clients +@rt +async def upload(file: UploadFile): + filebuffer = await file.read() + (upload_dir / file.filename).write_bytes(filebuffer) + return P('Size: ', file.size) +``` + +For multi-file, use `Input(..., multiple=True)`, and a type annotation +of `list[UploadFile]` in the handler. + +## Fastlite + +Fastlite and the MiniDataAPI specification it’s built on are a +CRUD-oriented API for working with SQLite. APSW and apswutils is used to +connect to SQLite, optimized for speed and clean error handling. + +``` python +from fastlite import * +``` + +``` python +db = database(':memory:') # or database('data/app.db') +``` + +Tables are normally constructed with classes, field types are specified +as type hints. + +``` python +class Book: isbn: str; title: str; pages: int; userid: int +# The transform arg instructs fastlite to change the db schema when fields change. +# Create only creates a table if the table doesn't exist. +books = db.create(Book, pk='isbn', transform=True) + +class User: id: int; name: str; active: bool = True +# If no pk is provided, id is used as the primary key. +users = db.create(User, transform=True) +users +``` + +
+ +### Fastlite CRUD operations + +Every operation in fastlite returns a full superset of dataclass +functionality. + +``` python +user = users.insert(name='Alex',active=False) +user +``` + + User(id=1, name='Alex', active=0) + +``` python +# List all records +users() +``` + + [User(id=1, name='Alex', active=0)] + +``` python +# Limit, offset, and order results: +users(order_by='name', limit=2, offset=1) + +# Filter on the results +users(where="name='Alex'") + +# Placeholder for avoiding injection attacks +users("name=?", ('Alex',)) + +# A single record by pk +users[user.id] +``` + + User(id=1, name='Alex', active=0) + +Test if a record exists by using `in` keyword on primary key: + +``` python +1 in users +``` + + True + +Updates (which take a dict or a typed object) return the updated record. + +``` python +user.name='Lauren' +user.active=True +users.update(user) +``` + + User(id=1, name='Lauren', active=1) + +`.xtra()` to automatically constrain queries, updates, and inserts from +there on: + +``` python +users.xtra(active=True) +users() +``` + + [User(id=1, name='Lauren', active=1)] + +Deleting by pk: + +``` python +users.delete(user.id) +``` + +
+ +NotFoundError is raised by pk `[]`, updates, and deletes. + +``` python +try: users['Amy'] +except NotFoundError: print('User not found') +``` + + User not found + +## MonsterUI + +MonsterUI is a shadcn-like component library for FastHTML. It adds the +Tailwind-based libraries FrankenUI and DaisyUI to FastHTML, as well as +Python’s mistletoe for Markdown, HighlightJS for code highlighting, and +Katex for latex support, following semantic HTML patterns when possible. +It is recommended for when you wish to go beyond the basics provided by +FastHTML’s built-in pico support. + +A minimal app: + +``` python +from fasthtml.common import * +from monsterui.all import * + +app, rt = fast_app(hdrs=Theme.blue.headers(highlightjs=True)) # Use MonsterUI blue theme and highlight code in markdown + +@rt +def index(): + socials = (('github','https://github.com/AnswerDotAI/MonsterUI'),) + return Titled("App", + Card( + P("App", cls=TextPresets.muted_sm), + # LabelInput, DivLAigned, and UkIconLink are non-semantic MonsterUI FT Components, + LabelInput('Email', type='email', required=True), + footer=DivLAligned(*[UkIconLink(icon,href=url) for icon,url in socials]))) +``` + +MonsterUI recommendations: + +- Use defaults as much as possible, for example + [`Container`](https://www.fastht.ml/docs/api/pico.html#container) in + monsterui already has defaults for margins +- Use `*T` for button styling consistency, for example + `cls=ButtonT.destructive` for a red delete button or + `cls=ButtonT.primary` for a CTA button +- Use `Label*` functions for forms as much as possible + (e.g. `LabelInput`, `LabelRange`) which creates and links both the + `FormLabel` and user input appropriately to avoid boiler plate. + +Flex Layout Elements (such as `DivLAligned` and `DivFullySpaced`) can be +used to create layouts concisely + +``` python +def TeamCard(name, role, location="Remote"): + icons = ("mail", "linkedin", "github") + return Card( + DivLAligned( + DiceBearAvatar(name, h=24, w=24), + Div(H3(name), P(role))), + footer=DivFullySpaced( + DivHStacked(UkIcon("map-pin", height=16), P(location)), + DivHStacked(*(UkIconLink(icon, height=16) for icon in icons)))) +``` + +Forms are styled and spaced for you without significant additional +classes. + +``` python +def MonsterForm(): + relationship = ["Parent",'Sibling', "Friend", "Spouse", "Significant Other", "Relative", "Child", "Other"] + return Div( + DivCentered( + H3("Emergency Contact Form"), + P("Please fill out the form completely", cls=TextPresets.muted_sm)), + Form( + Grid(LabelInput("Name",id='name'),LabelInput("Email", id='email')), + H3("Relationship to patient"), + Grid(*[LabelCheckboxX(o) for o in relationship], cols=4, cls='space-y-3'), + DivCentered(Button("Submit Form", cls=ButtonT.primary))), + cls='space-y-4') +``` + +Text can be styled with markdown automatically with MonsterUI + +```` python +render_md(""" +# My Document + +> Important note here + ++ List item with **bold** ++ Another with `code` + +```python +def hello(): + print("world") +``` +""") +```` + + '

My Document

\n
\n

Important note here

\n
\n
    \n
  • List item with bold
  • \n
  • Another with code
  • \n
\n
def hello():\n    print("world")\n
\n
' + +Or using semantic HTML: + +``` python +def SemanticText(): + return Card( + H1("MonsterUI's Semantic Text"), + P( + Strong("MonsterUI"), " brings the power of semantic HTML to life with ", + Em("beautiful styling"), " and ", Mark("zero configuration"), "."), + Blockquote( + P("Write semantic HTML in pure Python, get modern styling for free."), + Cite("MonsterUI Team")), + footer=Small("Released February 2025"),) +```+++ +title = "Reference" ++++ + +## Contents + +* [htmx Core Attributes](#attributes) +* [htmx Additional Attributes](#attributes-additional) +* [htmx CSS Classes](#classes) +* [htmx Request Headers](#request_headers) +* [htmx Response Headers](#response_headers) +* [htmx Events](#events) +* [htmx Extensions](/extensions) +* [JavaScript API](#api) +* [Configuration Options](#config) + +## Core Attribute Reference {#attributes} + +The most common attributes when using htmx. + +
+ +| Attribute | Description | +|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| [`hx-get`](@/attributes/hx-get.md) | issues a `GET` to the specified URL | +| [`hx-post`](@/attributes/hx-post.md) | issues a `POST` to the specified URL | +| [`hx-on*`](@/attributes/hx-on.md) | handle events with inline scripts on elements | +| [`hx-push-url`](@/attributes/hx-push-url.md) | push a URL into the browser location bar to create history | +| [`hx-select`](@/attributes/hx-select.md) | select content to swap in from a response | +| [`hx-select-oob`](@/attributes/hx-select-oob.md) | select content to swap in from a response, somewhere other than the target (out of band) | +| [`hx-swap`](@/attributes/hx-swap.md) | controls how content will swap in (`outerHTML`, `beforeend`, `afterend`, ...) | +| [`hx-swap-oob`](@/attributes/hx-swap-oob.md) | mark element to swap in from a response (out of band) | +| [`hx-target`](@/attributes/hx-target.md) | specifies the target element to be swapped | +| [`hx-trigger`](@/attributes/hx-trigger.md) | specifies the event that triggers the request | +| [`hx-vals`](@/attributes/hx-vals.md) | add values to submit with the request (JSON format) | + +
+ +## Additional Attribute Reference {#attributes-additional} + +All other attributes available in htmx. + +
+ +| Attribute | Description | +|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| [`hx-boost`](@/attributes/hx-boost.md) | add [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement) for links and forms | +| [`hx-confirm`](@/attributes/hx-confirm.md) | shows a `confirm()` dialog before issuing a request | +| [`hx-delete`](@/attributes/hx-delete.md) | issues a `DELETE` to the specified URL | +| [`hx-disable`](@/attributes/hx-disable.md) | disables htmx processing for the given node and any children nodes | +| [`hx-disabled-elt`](@/attributes/hx-disabled-elt.md) | adds the `disabled` attribute to the specified elements while a request is in flight | +| [`hx-disinherit`](@/attributes/hx-disinherit.md) | control and disable automatic attribute inheritance for child nodes | +| [`hx-encoding`](@/attributes/hx-encoding.md) | changes the request encoding type | +| [`hx-ext`](@/attributes/hx-ext.md) | extensions to use for this element | +| [`hx-headers`](@/attributes/hx-headers.md) | adds to the headers that will be submitted with the request | +| [`hx-history`](@/attributes/hx-history.md) | prevent sensitive data being saved to the history cache | +| [`hx-history-elt`](@/attributes/hx-history-elt.md) | the element to snapshot and restore during history navigation | +| [`hx-include`](@/attributes/hx-include.md) | include additional data in requests | +| [`hx-indicator`](@/attributes/hx-indicator.md) | the element to put the `htmx-request` class on during the request | +| [`hx-inherit`](@/attributes/hx-inherit.md) | control and enable automatic attribute inheritance for child nodes if it has been disabled by default | +| [`hx-params`](@/attributes/hx-params.md) | filters the parameters that will be submitted with a request | +| [`hx-patch`](@/attributes/hx-patch.md) | issues a `PATCH` to the specified URL | +| [`hx-preserve`](@/attributes/hx-preserve.md) | specifies elements to keep unchanged between requests | +| [`hx-prompt`](@/attributes/hx-prompt.md) | shows a `prompt()` before submitting a request | +| [`hx-put`](@/attributes/hx-put.md) | issues a `PUT` to the specified URL | +| [`hx-replace-url`](@/attributes/hx-replace-url.md) | replace the URL in the browser location bar | +| [`hx-request`](@/attributes/hx-request.md) | configures various aspects of the request | +| [`hx-sync`](@/attributes/hx-sync.md) | control how requests made by different elements are synchronized | +| [`hx-validate`](@/attributes/hx-validate.md) | force elements to validate themselves before a request | +| [`hx-vars`](@/attributes/hx-vars.md) | adds values dynamically to the parameters to submit with the request (deprecated, please use [`hx-vals`](@/attributes/hx-vals.md)) | + +
+ +## CSS Class Reference {#classes} + +
+ +| Class | Description | +|-----------|-------------| +| `htmx-added` | Applied to a new piece of content before it is swapped, removed after it is settled. +| `htmx-indicator` | A dynamically generated class that will toggle visible (opacity:1) when a `htmx-request` class is present +| `htmx-request` | Applied to either the element or the element specified with [`hx-indicator`](@/attributes/hx-indicator.md) while a request is ongoing +| `htmx-settling` | Applied to a target after content is swapped, removed after it is settled. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md). +| `htmx-swapping` | Applied to a target before any content is swapped, removed after it is swapped. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md). + +
+ +## HTTP Header Reference {#headers} + +### Request Headers Reference {#request_headers} + +
+ +| Header | Description | +|--------|-------------| +| `HX-Boosted` | indicates that the request is via an element using [hx-boost](@/attributes/hx-boost.md) +| `HX-Current-URL` | the current URL of the browser +| `HX-History-Restore-Request` | "true" if the request is for history restoration after a miss in the local history cache +| `HX-Prompt` | the user response to an [hx-prompt](@/attributes/hx-prompt.md) +| `HX-Request` | always "true" +| `HX-Target` | the `id` of the target element if it exists +| `HX-Trigger-Name` | the `name` of the triggered element if it exists +| `HX-Trigger` | the `id` of the triggered element if it exists + +
+ +### Response Headers Reference {#response_headers} + +
+ +| Header | Description | +|------------------------------------------------------|-------------| +| [`HX-Location`](@/headers/hx-location.md) | allows you to do a client-side redirect that does not do a full page reload +| [`HX-Push-Url`](@/headers/hx-push-url.md) | pushes a new url into the history stack +| [`HX-Redirect`](@/headers/hx-redirect.md) | can be used to do a client-side redirect to a new location +| `HX-Refresh` | if set to "true" the client-side will do a full refresh of the page +| [`HX-Replace-Url`](@/headers/hx-replace-url.md) | replaces the current URL in the location bar +| `HX-Reswap` | allows you to specify how the response will be swapped. See [hx-swap](@/attributes/hx-swap.md) for possible values +| `HX-Retarget` | a CSS selector that updates the target of the content update to a different element on the page +| `HX-Reselect` | a CSS selector that allows you to choose which part of the response is used to be swapped in. Overrides an existing [`hx-select`](@/attributes/hx-select.md) on the triggering element +| [`HX-Trigger`](@/headers/hx-trigger.md) | allows you to trigger client-side events +| [`HX-Trigger-After-Settle`](@/headers/hx-trigger.md) | allows you to trigger client-side events after the settle step +| [`HX-Trigger-After-Swap`](@/headers/hx-trigger.md) | allows you to trigger client-side events after the swap step + +
+ +## Event Reference {#events} + +
+ +| Event | Description | +|-------|-------------| +| [`htmx:abort`](@/events.md#htmx:abort) | send this event to an element to abort a request +| [`htmx:afterOnLoad`](@/events.md#htmx:afterOnLoad) | triggered after an AJAX request has completed processing a successful response +| [`htmx:afterProcessNode`](@/events.md#htmx:afterProcessNode) | triggered after htmx has initialized a node +| [`htmx:afterRequest`](@/events.md#htmx:afterRequest) | triggered after an AJAX request has completed +| [`htmx:afterSettle`](@/events.md#htmx:afterSettle) | triggered after the DOM has settled +| [`htmx:afterSwap`](@/events.md#htmx:afterSwap) | triggered after new content has been swapped in +| [`htmx:beforeCleanupElement`](@/events.md#htmx:beforeCleanupElement) | triggered before htmx [disables](@/attributes/hx-disable.md) an element or removes it from the DOM +| [`htmx:beforeOnLoad`](@/events.md#htmx:beforeOnLoad) | triggered before any response processing occurs +| [`htmx:beforeProcessNode`](@/events.md#htmx:beforeProcessNode) | triggered before htmx initializes a node +| [`htmx:beforeRequest`](@/events.md#htmx:beforeRequest) | triggered before an AJAX request is made +| [`htmx:beforeSwap`](@/events.md#htmx:beforeSwap) | triggered before a swap is done, allows you to configure the swap +| [`htmx:beforeSend`](@/events.md#htmx:beforeSend) | triggered just before an ajax request is sent +| [`htmx:beforeTransition`](@/events.md#htmx:beforeTransition) | triggered before the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) wrapped swap occurs +| [`htmx:configRequest`](@/events.md#htmx:configRequest) | triggered before the request, allows you to customize parameters, headers +| [`htmx:confirm`](@/events.md#htmx:confirm) | triggered after a trigger occurs on an element, allows you to cancel (or delay) issuing the AJAX request +| [`htmx:historyCacheError`](@/events.md#htmx:historyCacheError) | triggered on an error during cache writing +| [`htmx:historyCacheMiss`](@/events.md#htmx:historyCacheMiss) | triggered on a cache miss in the history subsystem +| [`htmx:historyCacheMissError`](@/events.md#htmx:historyCacheMissError) | triggered on a unsuccessful remote retrieval +| [`htmx:historyCacheMissLoad`](@/events.md#htmx:historyCacheMissLoad) | triggered on a successful remote retrieval +| [`htmx:historyRestore`](@/events.md#htmx:historyRestore) | triggered when htmx handles a history restoration action +| [`htmx:beforeHistorySave`](@/events.md#htmx:beforeHistorySave) | triggered before content is saved to the history cache +| [`htmx:load`](@/events.md#htmx:load) | triggered when new content is added to the DOM +| [`htmx:noSSESourceError`](@/events.md#htmx:noSSESourceError) | triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined +| [`htmx:onLoadError`](@/events.md#htmx:onLoadError) | triggered when an exception occurs during the onLoad handling in htmx +| [`htmx:oobAfterSwap`](@/events.md#htmx:oobAfterSwap) | triggered after an out of band element as been swapped in +| [`htmx:oobBeforeSwap`](@/events.md#htmx:oobBeforeSwap) | triggered before an out of band element swap is done, allows you to configure the swap +| [`htmx:oobErrorNoTarget`](@/events.md#htmx:oobErrorNoTarget) | triggered when an out of band element does not have a matching ID in the current DOM +| [`htmx:prompt`](@/events.md#htmx:prompt) | triggered after a prompt is shown +| [`htmx:pushedIntoHistory`](@/events.md#htmx:pushedIntoHistory) | triggered after a url is pushed into history +| [`htmx:replacedInHistory`](@/events.md#htmx:replacedInHistory) | triggered after a url is replaced in history +| [`htmx:responseError`](@/events.md#htmx:responseError) | triggered when an HTTP response error (non-`200` or `300` response code) occurs +| [`htmx:sendAbort`](@/events.md#htmx:sendAbort) | triggered when a request is aborted +| [`htmx:sendError`](@/events.md#htmx:sendError) | triggered when a network error prevents an HTTP request from happening +| [`htmx:sseError`](@/events.md#htmx:sseError) | triggered when an error occurs with a SSE source +| [`htmx:sseOpen`](/events#htmx:sseOpen) | triggered when a SSE source is opened +| [`htmx:swapError`](@/events.md#htmx:swapError) | triggered when an error occurs during the swap phase +| [`htmx:targetError`](@/events.md#htmx:targetError) | triggered when an invalid target is specified +| [`htmx:timeout`](@/events.md#htmx:timeout) | triggered when a request timeout occurs +| [`htmx:validation:validate`](@/events.md#htmx:validation:validate) | triggered before an element is validated +| [`htmx:validation:failed`](@/events.md#htmx:validation:failed) | triggered when an element fails validation +| [`htmx:validation:halted`](@/events.md#htmx:validation:halted) | triggered when a request is halted due to validation errors +| [`htmx:xhr:abort`](@/events.md#htmx:xhr:abort) | triggered when an ajax request aborts +| [`htmx:xhr:loadend`](@/events.md#htmx:xhr:loadend) | triggered when an ajax request ends +| [`htmx:xhr:loadstart`](@/events.md#htmx:xhr:loadstart) | triggered when an ajax request starts +| [`htmx:xhr:progress`](@/events.md#htmx:xhr:progress) | triggered periodically during an ajax request that supports progress events + +
+ +## JavaScript API Reference {#api} + +
+ +| Method | Description | +|-------|-------------| +| [`htmx.addClass()`](@/api.md#addClass) | Adds a class to the given element +| [`htmx.ajax()`](@/api.md#ajax) | Issues an htmx-style ajax request +| [`htmx.closest()`](@/api.md#closest) | Finds the closest parent to the given element matching the selector +| [`htmx.config`](@/api.md#config) | A property that holds the current htmx config object +| [`htmx.createEventSource`](@/api.md#createEventSource) | A property holding the function to create SSE EventSource objects for htmx +| [`htmx.createWebSocket`](@/api.md#createWebSocket) | A property holding the function to create WebSocket objects for htmx +| [`htmx.defineExtension()`](@/api.md#defineExtension) | Defines an htmx [extension](https://htmx.org/extensions) +| [`htmx.find()`](@/api.md#find) | Finds a single element matching the selector +| [`htmx.findAll()` `htmx.findAll(elt, selector)`](@/api.md#find) | Finds all elements matching a given selector +| [`htmx.logAll()`](@/api.md#logAll) | Installs a logger that will log all htmx events +| [`htmx.logger`](@/api.md#logger) | A property set to the current logger (default is `null`) +| [`htmx.off()`](@/api.md#off) | Removes an event listener from the given element +| [`htmx.on()`](@/api.md#on) | Creates an event listener on the given element, returning it +| [`htmx.onLoad()`](@/api.md#onLoad) | Adds a callback handler for the `htmx:load` event +| [`htmx.parseInterval()`](@/api.md#parseInterval) | Parses an interval declaration into a millisecond value +| [`htmx.process()`](@/api.md#process) | Processes the given element and its children, hooking up any htmx behavior +| [`htmx.remove()`](@/api.md#remove) | Removes the given element +| [`htmx.removeClass()`](@/api.md#removeClass) | Removes a class from the given element +| [`htmx.removeExtension()`](@/api.md#removeExtension) | Removes an htmx [extension](https://htmx.org/extensions) +| [`htmx.swap()`](@/api.md#swap) | Performs swapping (and settling) of HTML content +| [`htmx.takeClass()`](@/api.md#takeClass) | Takes a class from other elements for the given element +| [`htmx.toggleClass()`](@/api.md#toggleClass) | Toggles a class from the given element +| [`htmx.trigger()`](@/api.md#trigger) | Triggers an event on an element +| [`htmx.values()`](@/api.md#values) | Returns the input values associated with the given element + +
+ + +## Configuration Reference {#config} + +Htmx has some configuration options that can be accessed either programmatically or declaratively. They are +listed below: + +
+ +| Config Variable | Info | +|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `htmx.config.historyEnabled` | defaults to `true`, really only useful for testing | +| `htmx.config.historyCacheSize` | defaults to 10 | +| `htmx.config.refreshOnHistoryMiss` | defaults to `false`, if set to `true` htmx will issue a full page refresh on history misses rather than use an AJAX request | +| `htmx.config.defaultSwapStyle` | defaults to `innerHTML` | +| `htmx.config.defaultSwapDelay` | defaults to 0 | +| `htmx.config.defaultSettleDelay` | defaults to 20 | +| `htmx.config.includeIndicatorStyles` | defaults to `true` (determines if the indicator styles are loaded) | +| `htmx.config.indicatorClass` | defaults to `htmx-indicator` | +| `htmx.config.requestClass` | defaults to `htmx-request` | +| `htmx.config.addedClass` | defaults to `htmx-added` | +| `htmx.config.settlingClass` | defaults to `htmx-settling` | +| `htmx.config.swappingClass` | defaults to `htmx-swapping` | +| `htmx.config.allowEval` | defaults to `true`, can be used to disable htmx's use of eval for certain features (e.g. trigger filters) | +| `htmx.config.allowScriptTags` | defaults to `true`, determines if htmx will process script tags found in new content | +| `htmx.config.inlineScriptNonce` | defaults to `''`, meaning that no nonce will be added to inline scripts | +| `htmx.config.inlineStyleNonce` | defaults to `''`, meaning that no nonce will be added to inline styles | +| `htmx.config.attributesToSettle` | defaults to `["class", "style", "width", "height"]`, the attributes to settle during the settling phase | +| `htmx.config.wsReconnectDelay` | defaults to `full-jitter` | +| `htmx.config.wsBinaryType` | defaults to `blob`, the [the type of binary data](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) being received over the WebSocket connection | +| `htmx.config.disableSelector` | defaults to `[hx-disable], [data-hx-disable]`, htmx will not process elements with this attribute on it or a parent | +| `htmx.config.disableInheritance` | defaults to `false`. If it is set to `true`, the inheritance of attributes is completely disabled and you can explicitly specify the inheritance with the [hx-inherit](@/attributes/hx-inherit.md) attribute. +| `htmx.config.withCredentials` | defaults to `false`, allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates | +| `htmx.config.timeout` | defaults to 0, the number of milliseconds a request can take before automatically being terminated | +| `htmx.config.scrollBehavior` | defaults to 'instant', the scroll behavior when using the [show](@/attributes/hx-swap.md#scrolling-scroll-show) modifier with `hx-swap`. The allowed values are `instant` (scrolling should happen instantly in a single jump), `smooth` (scrolling should animate smoothly) and `auto` (scroll behavior is determined by the computed value of [scroll-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior)). | +| `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. | +| `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will append the target element to the `GET` request in the format `org.htmx.cache-buster=targetElementId` | +| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. | +| `htmx.config.methodsThatUseUrlParams` | defaults to `["get", "delete"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body | +| `htmx.config.selfRequestsOnly` | defaults to `true`, whether to only allow AJAX requests to the same domain as the current document | +| `htmx.config.ignoreTitle` | defaults to `false`, if set to `true` htmx will not update the title of the document when a `title` tag is found in new content | +| `htmx.config.scrollIntoViewOnBoost` | defaults to `true`, whether or not the target of a boosted element is scrolled into the viewport. If `hx-target` is omitted on a boosted element, the target defaults to `body`, causing the page to scroll to the top. | +| `htmx.config.triggerSpecsCache` | defaults to `null`, the cache to store evaluated trigger specifications into, improving parsing performance at the cost of more memory usage. You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) | +| `htmx.config.responseHandling` | the default [Response Handling](@/docs.md#response-handling) behavior for response status codes can be configured here to either swap or error | +| `htmx.config.allowNestedOobSwaps` | defaults to `true`, whether to process OOB swaps on elements that are nested within the main response element. See [Nested OOB Swaps](@/attributes/hx-swap-oob.md#nested-oob-swaps). | + +
+ +You can set them directly in javascript, or you can use a `meta` tag: + +```html + +```
# 🌟 Starlette Quick Manual + + +2020-02-09 + +Starlette is the ASGI web framework used as the foundation of FastHTML. Listed here are some Starlette features FastHTML developers can use directly, since the `FastHTML` class inherits from the `Starlette` class (but note that FastHTML has its own customised `RouteX` and `RouterX` classes for routing, to handle FT element trees etc). + +## Get uploaded file content + +``` +async def handler(request): + inp = await request.form() + uploaded_file = inp["filename"] + filename = uploaded_file.filename # abc.png + content_type = uploaded.content_type # MIME type, e.g. image/png + content = await uploaded_file.read() # image content + +``` + +## Return a customized response (status code and headers) + +``` +import json +from starlette.responses import Response + +async def handler(request): + data = { + "name": "Bo" + } + return Response(json.dumps(data), media_type="application/json") + +``` + +`Response` takes `status_code`, `headers` and `media_type`, so if we want to change a response's status code, we can do: + +``` +return Response(content, statu_code=404) + +``` + +And customized headers: + +``` +headers = { + "x-extra-key": "value" +} +return Response(content, status_code=200, headers=headers) + +``` + +## Redirect + +``` +from starlette.responses import RedirectResponse + +async handler(request): + # Customize status_code: + # 301: permanent redirect + # 302: temporary redirect + # 303: see others + # 307: temporary redirect (default) + return RedirectResponse(url=url, status_code=303) + +``` + +## Request context + +### URL Object: `request.url` + + * Get request full url: `url = str(request.url)` + * Get scheme: `request.url.scheme` (http, https, ws, wss) + * Get netloc: `request.url.netloc`, e.g.: example.com:8080 + * Get path: `request.url.path`, e.g.: /search + * Get query string: `request.url.query`, e.g.: kw=hello + * Get hostname: `request.url.hostname`, e.g.: example.com + * Get port: `request.url.port`, e.g.: 8080 + * If using secure scheme: `request.url.is_secure`, True is schme is `https` or `wss` + +### Headers: `request.headers` + +``` +{ + 'host': 'example.com:8080', + 'connection': 'keep-alive', + 'cache-control': 'max-age=0', + 'sec-ch-ua': 'Google Chrome 80', + 'dnt': '1', + 'upgrade-insecure-requests': '1', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) ...', + 'sec-fetch-dest': 'document', + 'accept': 'text/html,image/apng,*/*;q=0.8;v=b3;q=0.9', + 'sec-origin-policy': '0', + 'sec-fetch-site': 'none', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-user': '?1', + 'accept-encoding': 'gzip, deflate, br', + 'accept-language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6', + 'cookie': 'session=eyJhZG1pbl91c2_KiQ...' +} + +``` + +### Client: `request.client` + + * `request.client.host`: get client sock IP + * `request.client.port`: get client sock port + +### Method: `request.method` + + * `request.method`: GET, POST, etc. + +### Get Data + + * `await request.body()`: get raw data from body + * `await request.json()`: get passed data and parse it as JSON + * `await request.form()`: get posted data and pass it as dictionary + +### Scope: `request.scope` + +``` +{ + 'type': 'http', + 'http_version': '1.1', + 'server': ('127.0.0.1', 9092), + 'client': ('127.0.0.1', 53102), + 'scheme': 'https', + 'method': 'GET', + 'root_path': '', + 'path': '/', + 'raw_path': b'/', + 'query_string': b'kw=hello', + 'headers': [ + (b'host', b'example.com:8080'), + (b'connection', b'keep-alive'), + (b'cache-control', b'max-age=0'), + ... + ], + 'app': , + 'session': {'uid': '57ba03ea7333f72a25f837cf'}, + 'router': , + 'endpoint': , + 'path_params': {} +} + +``` + +## Put varaible in request & app scope + +``` +app.state.dbconn = get_db_conn() +request.state.start_time = time.time() +# use app-scope state variable in a request +request.app.state.dbconn + +``` + +## Utility functions + +### Use `State` to wrap a dictionary + +``` +from starlette.datastructures import State + +data = { + "name": "Bo" +} +print(data["name"]) +# now wrap it with State function +wrapped = State(data) +# You can use the dot syntaxt, but can't use `wrapped["name"]` any more. +print(wrapped.name) + +``` + +### login_required wrapper function + +NB: This is easier to do in FastHTML using Beforeware. + +``` +import functools +from starlette.endpoints import HTTPEndpoint +from starlette.responses import Response + +def login_required(login_url="/signin"): + def decorator(handler): + @functools.wraps(handler) + async def new_handler(obj, req, *args, **kwargs): + user = req.session.get("login_user") + if user is None: + return seeother(login_url) + return await handler(obj, req, *args, **kwargs) + return new_handler + return decorator + +class MyAccount(HTTPEndpiont): + @login_required() + async def get(self, request): + # some logic here + content = "hello" + return Response(content) + +``` + +## Exceptions + +Handle exception and customize 403, 404, 503, 500 page: + +``` +from starlette.exceptions import HTTPException + +async def exc_handle_403(request, exc): + return HTMLResponse("My 403 page", status_code=exc.status_code) + +async def exc_handle_404(request, exc): + return HTMLResponse("My 404 page", status_code=exc.status_code) + +async def exc_handle_503(request, exc): + return HTMLResponse("Failed, please try it later", status_code=exc.status_code) + +# error is not exception, 500 is server side unexpected error, all other status code will be treated as Exception +async def err_handle_500(request, exc): + import traceback + Log.error(traceback.format_exc()) + return HTMLResponse("My 500 page", status_code=500) + +# To add handler, we can add either status_code or Exception itself as key +exception_handlers = { + 403: exc_handle_403, + 404: exc_handle_404, + 503: exc_handle_503, + 500: err_handle_500, + #HTTPException: exc_handle_500, +} + +app = Starlette(routes=routes, exception_handlers=exception_handlers) + +``` + +## Background Task + +### Put some async task as background task + +``` +import aiofiles +from starlette.background import BackgroundTask +from starlette.responses import Response + +aiofiles_remove = aiofiles.os.wrap(os.remove) + +async def del_file(fpath): + await aiofiles_remove(fpath) + +async def handler(request): + content = "" + fpath = "/tmp/tmpfile.txt" + task = BackgroundTask(del_file, fpath=fpath) + return Response(content, background=task) + +``` + +### Put multiple tasks as background task + +``` +from starlette.background import BackgroundTasks + +async def task1(name): + pass + +async def task2(email): + pass + +async def handler(request): + tasks = BackgroundTasks() + tasks.add_task(task1, name="John") + tasks.add_task(task2, email="info@example.com") + content = "" + return Response(content, background=tasks) + +``` + +## Write middleware + +There are 2 ways to write middleware: + +### Define `__call__` function: + +``` +class MyMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + # see above scope dictionary as reference + headers = dict(scope["headers"]) + # do something + # pass to next middleware + return await self.app(scope, receive, send) + +``` + +### Use `BaseHTTPMiddleware` + +``` +from starlette.middleware.base import BaseHTTPMiddleware + +class CustomHeaderMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request, call_next): + # do something before pass to next middleware + response = await call_next(request) + # do something after next middleware returned + response.headers['X-Author'] = 'John' + return response + +```# fasthtml Module Documentation + +## fasthtml.authmw + +- `class BasicAuthMiddleware` + - `def __init__(self, app, cb, skip)` + - `def __call__(self, scope, receive, send)` + - `def authenticate(self, conn)` + +## fasthtml.cli + +- `@call_parse def railway_link()` + Link the current directory to the current project's Railway service + +- `@call_parse def railway_deploy(name, mount)` + Deploy a FastHTML app to Railway + +## fasthtml.components + +> `ft_html` and `ft_hx` functions to add some conveniences to `ft`, along with a full set of basic HTML components, and functions to work with forms and `FT` conversion + +- `def File(fname)` + Use the unescaped text in file `fname` directly + +- `def show(ft, *rest)` + Renders FT Components into HTML within a Jupyter notebook. + +- `def fill_form(form, obj)` + Fills named items in `form` using attributes in `obj` + +- `def fill_dataclass(src, dest)` + Modifies dataclass in-place and returns it + +- `def find_inputs(e, tags, **kw)` + Recursively find all elements in `e` with `tags` and attrs matching `kw` + +- `def html2ft(html, attr1st)` + Convert HTML to an `ft` expression + +- `def sse_message(elm, event)` + Convert element `elm` into a format suitable for SSE streaming + +## fasthtml.core + +> The `FastHTML` subclass of `Starlette`, along with the `RouterX` and `RouteX` classes it automatically uses. + +- `def parsed_date(s)` + Convert `s` to a datetime + +- `def snake2hyphens(s)` + Convert `s` from snake case to hyphenated and capitalised + +- `@dataclass class HtmxHeaders` + - `def __bool__(self)` + - `def __init__(self, boosted, current_url, history_restore_request, prompt, request, target, trigger_name, trigger)` + +- `@dataclass class HttpHeader` + - `def __init__(self, k, v)` + +- `@use_kwargs_dict(**htmx_resps) def HtmxResponseHeaders(**kwargs)` + HTMX response headers + +- `def form2dict(form)` + Convert starlette form data to a dict + +- `def parse_form(req)` + Starlette errors on empty multipart forms, so this checks for that situation + +- `class JSONResponse` + Same as starlette's version, but auto-stringifies non serializable types + + - `def render(self, content)` + +- `def flat_xt(lst)` + Flatten lists + +- `class Beforeware` + - `def __init__(self, f, skip)` + +- `def EventStream(s)` + Create a text/event-stream response from `s` + +- `def flat_tuple(o)` + Flatten lists + +- `def noop_body(c, req)` + Default Body wrap function which just returns the content + +- `def respond(req, heads, bdy)` + Default FT response creation function + +- `class Redirect` + Use HTMX or Starlette RedirectResponse as required to redirect to `loc` + + - `def __init__(self, loc)` + - `def __response__(self, req)` + +- `def qp(p, **kw)` + Add parameters kw to path p + +- `def def_hdrs(htmx, surreal)` + Default headers for a FastHTML app + +- `class FastHTML` + - `def __init__(self, debug, routes, middleware, title, exception_handlers, on_startup, on_shutdown, lifespan, hdrs, ftrs, exts, before, after, surreal, htmx, default_hdrs, sess_cls, secret_key, session_cookie, max_age, sess_path, same_site, sess_https_only, sess_domain, key_fname, body_wrap, htmlkw, nb_hdrs, canonical, **bodykw)` + - `def add_route(self, route)` + +- `@patch def ws(self, path, conn, disconn, name, middleware)` + Add a websocket route at `path` + +- `def nested_name(f)` + Get name of function `f` using '_' to join nested function names + +- `@patch def route(self, path, methods, name, include_in_schema, body_wrap)` + Add a route at `path` + +- `def serve(appname, app, host, port, reload, reload_includes, reload_excludes)` + Run the app in an async server, with live reload set as the default. + +- `class Client` + A simple httpx ASGI client that doesn't require `async` + + - `def __init__(self, app, url)` + +- `class RouteFuncs` + - `def __init__(self)` + - `def __setattr__(self, name, value)` + - `def __getattr__(self, name)` + - `def __dir__(self)` + +- `class APIRouter` + Add routes to an app + + - `def __init__(self, prefix, body_wrap)` + - `def __call__(self, path, methods, name, include_in_schema, body_wrap)` + Add a route at `path` + + - `def __getattr__(self, name)` + - `def to_app(self, app)` + Add routes to `app` + + - `def ws(self, path, conn, disconn, name, middleware)` + Add a websocket route at `path` + + +- `def cookie(key, value, max_age, expires, path, domain, secure, httponly, samesite)` + Create a 'set-cookie' `HttpHeader` + +- `@patch def static_route_exts(self, prefix, static_path, exts)` + Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()` + +- `@patch def static_route(self, ext, prefix, static_path)` + Add a static route at URL path `prefix` with files from `static_path` and single `ext` (including the '.') + +- `class MiddlewareBase` + - `def __call__(self, scope, receive, send)` + +- `class FtResponse` + Wrap an FT response with any Starlette `Response` + + - `def __init__(self, content, status_code, headers, cls, media_type, background)` + - `def __response__(self, req)` + +## fasthtml.fastapp + +> The `fast_app` convenience wrapper + +- `def fast_app(db_file, render, hdrs, ftrs, tbls, before, middleware, live, debug, routes, exception_handlers, on_startup, on_shutdown, lifespan, default_hdrs, pico, surreal, htmx, exts, canonical, secret_key, key_fname, session_cookie, max_age, sess_path, same_site, sess_https_only, sess_domain, htmlkw, bodykw, reload_attempts, reload_interval, static_path, body_wrap, nb_hdrs, **kwargs)` + Create a FastHTML or FastHTMLWithLiveReload app. + +## fasthtml.js + +> Basic external Javascript lib wrappers + +- `def light_media(css)` + Render light media for day mode views + +- `def dark_media(css)` + Render dark media for night mode views + +- `def MarkdownJS(sel)` + Implements browser-based markdown rendering. + +- `def HighlightJS(sel, langs, light, dark)` + Implements browser-based syntax highlighting. Usage example [here](/tutorials/quickstart_for_web_devs.html#code-highlighting). + +- `def MermaidJS(sel, theme)` + Implements browser-based Mermaid diagram rendering. + +## fasthtml.jupyter + +> Use FastHTML in Jupyter notebooks + +- `def nb_serve(app, log_level, port, host, **kwargs)` + Start a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level` + +- `def nb_serve_async(app, log_level, port, host, **kwargs)` + Async version of `nb_serve` + +- `def is_port_free(port, host)` + Check if `port` is free on `host` + +- `def wait_port_free(port, host, max_wait)` + Wait for `port` to be free on `host` + +- `class JupyUvi` + Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level` + + - `def __init__(self, app, log_level, host, port, start, **kwargs)` + - `def start(self)` + - `def start_async(self)` + - `def stop(self)` + +- `class JupyUviAsync` + Start and stop an async Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level` + + - `def __init__(self, app, log_level, host, port, **kwargs)` + - `def start(self)` + - `def stop(self)` + +- `def HTMX(path, app, host, port, height, link, iframe)` + An iframe which displays the HTMX application in a notebook. + +## fasthtml.live_reload + +- `class FastHTMLWithLiveReload` + `FastHTMLWithLiveReload` enables live reloading. + This means that any code changes saved on the server will automatically + trigger a reload of both the server and browser window. + + How does it work? + - a websocket is created at `/live-reload` + - a small js snippet `LIVE_RELOAD_SCRIPT` is injected into each webpage + - this snippet connects to the websocket at `/live-reload` and listens for an `onclose` event + - when the `onclose` event is detected the browser is reloaded + + Why do we listen for an `onclose` event? + When code changes are saved the server automatically reloads if the --reload flag is set. + The server reload kills the websocket connection. The `onclose` event serves as a proxy + for "developer has saved some changes". + + Usage + >>> from fasthtml.common import * + >>> app = FastHTMLWithLiveReload() + + Run: + serve() + + - `def __init__(self, *args, **kwargs)` + +## fasthtml.oauth + +> Basic scaffolding for handling OAuth + +- `class GoogleAppClient` + A `WebApplicationClient` for Google oauth2 + + - `def __init__(self, client_id, client_secret, code, scope, project_id, **kwargs)` + - `@classmethod def from_file(cls, fname, code, scope, **kwargs)` + +- `class GitHubAppClient` + A `WebApplicationClient` for GitHub oauth2 + + - `def __init__(self, client_id, client_secret, code, scope, **kwargs)` + +- `class HuggingFaceClient` + A `WebApplicationClient` for HuggingFace oauth2 + + - `def __init__(self, client_id, client_secret, code, scope, state, **kwargs)` + +- `class DiscordAppClient` + A `WebApplicationClient` for Discord oauth2 + + - `def __init__(self, client_id, client_secret, is_user, perms, scope, **kwargs)` + - `def login_link(self, redirect_uri, scope, state)` + - `def parse_response(self, code, redirect_uri)` + +- `class Auth0AppClient` + A `WebApplicationClient` for Auth0 OAuth2 + + - `def __init__(self, domain, client_id, client_secret, code, scope, redirect_uri, **kwargs)` + - `def login_link(self, req)` + +- `@patch def login_link(self, redirect_uri, scope, state, **kwargs)` + Get a login link for this client + +- `def redir_url(request, redir_path, scheme)` + Get the redir url for the host in `request` + +- `@patch def parse_response(self, code, redirect_uri)` + Get the token from the oauth2 server response + +- `@patch def get_info(self, token)` + Get the info for authenticated user + +- `@patch def retr_info(self, code, redirect_uri)` + Combines `parse_response` and `get_info` + +- `@patch def retr_id(self, code, redirect_uri)` + Call `retr_info` and then return id/subscriber value + +- `class OAuth` + - `def __init__(self, app, cli, skip, redir_path, error_path, logout_path, login_path, https, http_patterns)` + - `def redir_login(self, session)` + - `def redir_url(self, req)` + - `def login_link(self, req, scope, state)` + - `def check_invalid(self, req, session, auth)` + - `def logout(self, session)` + - `def get_auth(self, info, ident, session, state)` + +- `@patch() def consent_url(self, proj)` + Get Google OAuth consent screen URL + +- `@patch def save(self, fname)` + Save credentials to `fname` + +- `def load_creds(fname)` + Load credentials from `fname` + +- `@patch def creds(self)` + Create `Credentials` from the client, refreshing if needed + +## fasthtml.pico + +> Basic components for generating Pico CSS tags + +- `@delegates(ft_hx, keep=True) def Card(*c, **kwargs)` + A PicoCSS Card, implemented as an Article with optional Header and Footer + +- `@delegates(ft_hx, keep=True) def Group(*c, **kwargs)` + A PicoCSS Group, implemented as a Fieldset with role 'group' + +- `@delegates(ft_hx, keep=True) def Search(*c, **kwargs)` + A PicoCSS Search, implemented as a Form with role 'search' + +- `@delegates(ft_hx, keep=True) def Grid(*c, **kwargs)` + A PicoCSS Grid, implemented as child Divs in a Div with class 'grid' + +- `@delegates(ft_hx, keep=True) def DialogX(*c, **kwargs)` + A PicoCSS Dialog, with children inside a Card + +- `@delegates(ft_hx, keep=True) def Container(*args, **kwargs)` + A PicoCSS Container, implemented as a Main with class 'container' + +## fasthtml.stripe_otp + +- `def create_price(app_nm, amt, currency)` + Create a product and bind it to a price object. If product already exist just return the price list. + +- `def archive_price(app_nm)` + Archive a price - useful for cleanup if testing. + +- `class Payment` + +## fasthtml.svg + +> Simple SVG FT elements + +- `def Svg(*args, **kwargs)` + An SVG tag; xmlns is added automatically, and viewBox defaults to height and width if not provided + +- `@delegates(ft_hx) def ft_svg(tag, *c, **kwargs)` + Create a standard `FT` element with some SVG-specific attrs + +- `@delegates(ft_svg) def Rect(width, height, x, y, fill, stroke, stroke_width, rx, ry, **kwargs)` + A standard SVG `rect` element + +- `@delegates(ft_svg) def Circle(r, cx, cy, fill, stroke, stroke_width, **kwargs)` + A standard SVG `circle` element + +- `@delegates(ft_svg) def Ellipse(rx, ry, cx, cy, fill, stroke, stroke_width, **kwargs)` + A standard SVG `ellipse` element + +- `def transformd(translate, scale, rotate, skewX, skewY, matrix)` + Create an SVG `transform` kwarg dict + +- `@delegates(ft_svg) def Line(x1, y1, x2, y2, stroke, w, stroke_width, **kwargs)` + A standard SVG `line` element + +- `@delegates(ft_svg) def Polyline(*args, **kwargs)` + A standard SVG `polyline` element + +- `@delegates(ft_svg) def Polygon(*args, **kwargs)` + A standard SVG `polygon` element + +- `@delegates(ft_svg) def Text(*args, **kwargs)` + A standard SVG `text` element + +- `class PathFT` + - `def M(self, x, y)` + Move to. + + - `def L(self, x, y)` + Line to. + + - `def H(self, x)` + Horizontal line to. + + - `def V(self, y)` + Vertical line to. + + - `def Z(self)` + Close path. + + - `def C(self, x1, y1, x2, y2, x, y)` + Cubic Bézier curve. + + - `def S(self, x2, y2, x, y)` + Smooth cubic Bézier curve. + + - `def Q(self, x1, y1, x, y)` + Quadratic Bézier curve. + + - `def T(self, x, y)` + Smooth quadratic Bézier curve. + + - `def A(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y)` + Elliptical Arc. + + +- `def SvgOob(*args, **kwargs)` + Wraps an SVG shape as required for an HTMX OOB swap + +- `def SvgInb(*args, **kwargs)` + Wraps an SVG shape as required for an HTMX inband swap + +## fasthtml.xtend + +> Simple extensions to standard HTML components, such as adding sensible defaults + +- `@delegates(ft_hx, keep=True) def A(*c, **kwargs)` + An A tag; `href` defaults to '#' for more concise use with HTMX + +- `@delegates(ft_hx, keep=True) def AX(txt, hx_get, target_id, hx_swap, href, **kwargs)` + An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be positional params + +- `@delegates(ft_hx, keep=True) def Form(*c, **kwargs)` + A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'` + +- `@delegates(ft_hx, keep=True) def Hidden(value, id, **kwargs)` + An Input of type 'hidden' + +- `@delegates(ft_hx, keep=True) def CheckboxX(checked, label, value, id, name, **kwargs)` + A Checkbox optionally inside a Label, preceded by a `Hidden` with matching name + +- `@delegates(ft_html, keep=True) def Script(code, **kwargs)` + A Script tag that doesn't escape its code + +- `@delegates(ft_html, keep=True) def Style(*c, **kwargs)` + A Style tag that doesn't escape its code + +- `def double_braces(s)` + Convert single braces to double braces if next to special chars or newline + +- `def undouble_braces(s)` + Convert double braces to single braces if next to special chars or newline + +- `def loose_format(s, **kw)` + String format `s` using `kw`, without being strict about braces outside of template params + +- `def ScriptX(fname, src, nomodule, type, _async, defer, charset, crossorigin, integrity, **kw)` + A `script` element with contents read from `fname` + +- `def replace_css_vars(css, pre, **kwargs)` + Replace `var(--)` CSS variables with `kwargs` if name prefix matches `pre` + +- `def StyleX(fname, **kw)` + A `style` element with contents read from `fname` and variables replaced from `kw` + +- `def Nbsp()` + A non-breaking space + +- `def Surreal(code)` + Wrap `code` in `domReadyExecute` and set `m=me()` and `p=me('-')` + +- `def On(code, event, sel, me)` + An async surreal.js script block event handler for `event` on selector `sel,p`, making available parent `p`, event `ev`, and target `e` + +- `def Prev(code, event)` + An async surreal.js script block event handler for `event` on previous sibling, with same vars as `On` + +- `def Now(code, sel)` + An async surreal.js script block on selector `me(sel)` + +- `def AnyNow(sel, code)` + An async surreal.js script block on selector `any(sel)` + +- `def run_js(js, id, **kw)` + Run `js` script, auto-generating `id` based on name of caller if needed, and js-escaping any `kw` params + +- `def jsd(org, repo, root, path, prov, typ, ver, esm, **kwargs)` + jsdelivr `Script` or CSS `Link` tag, or URL + +- `class Fragment` + An empty tag, used as a container + + - `def __init__(self, *c)` + +- `@delegates(ft_hx, keep=True) def Titled(title, *args, **kwargs)` + An HTML partial containing a `Title`, and `H1`, and any provided children + +- `def Socials(title, site_name, description, image, url, w, h, twitter_site, creator, card)` + OG and Twitter social card headers + +- `def YouTubeEmbed(video_id, **kwargs)` + Embed a YouTube video + +- `def Favicon(light_icon, dark_icon)` + Light and dark favicon headers +# monsterui Module Documentation + +## monsterui.core + +- `class ThemeRadii(Enum)` + Members: none, sm, md, lg + + +- `class ThemeShadows` + +- `class ThemeFont` + +- `class Theme(Enum)` + Selector to choose theme and get all headers needed for app. Includes frankenui + tailwind + daisyui + highlight.js options + Members: slate, stone, gray, neutral, red, rose, orange, green, blue, yellow, violet, zinc + + - `headers(self, mode, icons, daisy, highlightjs, katex, apex_charts, radii, shadows, font)` + Create frankenui and tailwind cdns + + - `local_headers(self, mode, static_dir, icons, daisy, highlightjs, katex, apex_charts, radii, shadows, font)` + Create headers using local files downloaded from CDNs + + +## monsterui.daisy + +- `class AlertT(Enum)` + Alert styles from DaisyUI + Members: info, success, warning, error + + +- `def Alert(*c, **kwargs)` + Alert informs users about important events. + +- `class StepsT(Enum)` + Options for Steps + Members: vertical, horizonal + + +- `class StepT(Enum)` + Step styles for LiStep + Members: primary, secondary, accent, info, success, warning, error, neutral + + +- `def Steps(*li, **kwargs)` + Creates a steps container + +- `def LiStep(*c, **kwargs)` + Creates a step list item + +- `class LoadingT(Enum)` + Members: spinner, dots, ring, ball, bars, infinity, xs, sm, md, lg + + +- `def Loading(cls, htmx_indicator, **kwargs)` + Creates a loading animation component + +- `class ToastHT(Enum)` + Horizontal position for Toast + Members: start, center, end + + +- `class ToastVT(Enum)` + Vertical position for Toast + Members: top, middle, bottom + + +## monsterui.foundations + +> Data Structures and Utilties + +- `def stringify(o)` + Converts input types into strings that can be passed to FT components + +- `class VEnum(Enum)` + Members: + + - `__str__(self)` + - `__add__(self, other)` + - `__radd__(self, other)` + +## monsterui.franken + +- `class TextT(Enum)` + Text Styles from https://franken-ui.dev/docs/text + Members: paragraph, lead, meta, gray, italic, xs, sm, lg, xl, light, normal, medium, bold, extrabold, muted, primary, secondary, success, warning, error, info, left, right, center, justify, start, end, top, middle, bottom, truncate, break_, nowrap, underline, highlight + + +- `class TextPresets(Enum)` + Common Typography Presets + Members: muted_sm, muted_lg, bold_sm, bold_lg, md_weight_sm, md_weight_muted + + +- `def CodeSpan(*c, **kwargs)` + A CodeSpan with Styling + +- `def CodeBlock(*c, **kwargs)` + CodeBlock with Styling + +- `def H1(*c, **kwargs)` + H1 with styling and appropriate size + +- `def H2(*c, **kwargs)` + H2 with styling and appropriate size + +- `def H3(*c, **kwargs)` + H3 with styling and appropriate size + +- `def H4(*c, **kwargs)` + H4 with styling and appropriate size + +- `def H5(*c, **kwargs)` + H5 with styling and appropriate size + +- `def H6(*c, **kwargs)` + H6 with styling and appropriate size + +- `def Subtitle(*c, **kwargs)` + Styled muted_sm text designed to go under Headings and Titles + +- `def Q(*c, **kwargs)` + Styled quotation mark + +- `def Em(*c, **kwargs)` + Styled emphasis text + +- `def Strong(*c, **kwargs)` + Styled strong text + +- `def I(*c, **kwargs)` + Styled italic text + +- `def Small(*c, **kwargs)` + Styled small text + +- `def Mark(*c, **kwargs)` + Styled highlighted text + +- `def Del(*c, **kwargs)` + Styled deleted text + +- `def Ins(*c, **kwargs)` + Styled inserted text + +- `def Sub(*c, **kwargs)` + Styled subscript text + +- `def Sup(*c, **kwargs)` + Styled superscript text + +- `def Blockquote(*c, **kwargs)` + Blockquote with Styling + +- `def Caption(*c, **kwargs)` + Styled caption text + +- `def Cite(*c, **kwargs)` + Styled citation text + +- `def Time(*c, **kwargs)` + Styled time element + +- `def Address(*c, **kwargs)` + Styled address element + +- `def Abbr(*c, **kwargs)` + Styled abbreviation with dotted underline + +- `def Dfn(*c, **kwargs)` + Styled definition term with italic and medium weight + +- `def Kbd(*c, **kwargs)` + Styled keyboard input with subtle background + +- `def Samp(*c, **kwargs)` + Styled sample output with subtle background + +- `def Var(*c, **kwargs)` + Styled variable with italic monospace + +- `def Figure(*c, **kwargs)` + Styled figure container with card-like appearance + +- `def Details(*c, **kwargs)` + Styled details element + +- `def Summary(*c, **kwargs)` + Styled summary element + +- `def Data(*c, **kwargs)` + Styled data element + +- `def Meter(*c, **kwargs)` + Styled meter element + +- `def S(*c, **kwargs)` + Styled strikethrough text (different semantic meaning from Del) + +- `def U(*c, **kwargs)` + Styled underline (for proper names in Chinese, proper spelling etc) + +- `def Output(*c, **kwargs)` + Styled output element for form results + +- `def PicSumImg(h, w, id, grayscale, blur, **kwargs)` + Creates a placeholder image using https://picsum.photos/ + +- `def AccordionItem(title, *c)` + Creates a single item for use within an Accordion component, handling title, content, and open state. + +- `def Accordion(*c, **kwargs)` + Creates a styled Accordion container using accordion component. + +- `class ButtonT(Enum)` + Options for styling Buttons + Members: default, ghost, primary, secondary, destructive, text, link, xs, sm, lg, xl, icon + + +- `def Button(*c, **kwargs)` + Button with Styling (defaults to `submit` for form submission) + +- `class ContainerT(Enum)` + Max width container sizes from https://franken-ui.dev/docs/container + Members: xs, sm, lg, xl, expand + + +- `class BackgroundT(Enum)` + Members: muted, primary, secondary, default + + +- `def Container(*c, **kwargs)` + Div to be used as a container that often wraps large sections or a page of content + +- `def Titled(title, *c, **kwargs)` + Creates a standard page structure for titled page. Main(Container(title, content)) + +- `class DividerT(Enum)` + Divider Styles from https://franken-ui.dev/docs/divider + Members: icon, sm, vertical + + +- `def Divider(*c, **kwargs)` + Divider with default styling and margin + +- `def DividerSplit(*c)` + Creates a simple horizontal line divider with configurable thickness and vertical spacing + +- `def Article(*c, **kwargs)` + A styled article container for blog posts or similar content + +- `def ArticleTitle(*c, **kwargs)` + A title component for use within an Article + +- `def ArticleMeta(*c, **kwargs)` + A metadata component for use within an Article showing things like date, author etc + +- `class SectionT(Enum)` + Section styles from https://franken-ui.dev/docs/section + Members: default, muted, primary, secondary, xs, sm, lg, xl, remove_vertical + + +- `def Section(*c, **kwargs)` + Section with styling and margins + +- `def Form(*c, **kwargs)` + A Form with default spacing between form elements + +- `def Fieldset(*c, **kwargs)` + A Fieldset with default styling + +- `def Legend(*c, **kwargs)` + A Legend with default styling + +- `def Input(*c, **kwargs)` + An Input with default styling + +- `def Radio(*c, **kwargs)` + A Radio with default styling + +- `def CheckboxX(*c, **kwargs)` + A Checkbox with default styling + +- `def Range(*c, **kwargs)` + A Range with default styling + +- `def TextArea(*c, **kwargs)` + A Textarea with default styling + +- `def Switch(*c, **kwargs)` + A Switch with default styling + +- `def Upload(*c, **kwargs)` + A file upload component with default styling + +- `def UploadZone(*c, **kwargs)` + A file drop zone component with default styling + +- `def FormLabel(*c, **kwargs)` + A Label with default styling + +- `class LabelT(Enum)` + Members: primary, secondary, destructive + + +- `def Label(*c, **kwargs)` + FrankenUI labels, which look like pills + +- `def UkFormSection(title, description, *c)` + A form section with a title, description and optional button + +- `def GenericLabelInput(label, lbl_cls, input_cls, container, cls, id, input_fn, **kwargs)` + `Div(Label,Input)` component with Uk styling injected appropriately. Generally you should higher level API, such as `LabelInput` which is created for you in this library + +- `def LabelInput(label, lbl_cls, input_cls, cls, id, **kwargs)` + A `FormLabel` and `Input` pair that provides default spacing and links/names them based on id + +- `def LabelRadio(label, lbl_cls, input_cls, container, cls, id, **kwargs)` + A FormLabel and Radio pair that provides default spacing and links/names them based on id + +- `def LabelCheckboxX(label, lbl_cls, input_cls, container, cls, id, **kwargs)` + A FormLabel and CheckboxX pair that provides default spacing and links/names them based on id + +- `def Options(*c)` + Helper function to wrap things into `Option`s for use in `Select` + +- `def Select(*option, **kwargs)` + Creates a select dropdown with uk styling and option for adding a search box + +- `def LabelSelect(*option, **kwargs)` + A FormLabel and Select pair that provides default spacing and links/names them based on id + +- `@delegates(GenericLabelInput, but=['input_fn', 'cls']) def LabelRange(label, lbl_cls, input_cls, cls, id, value, min, max, step, label_range, **kwargs)` + A FormLabel and Range pair that provides default spacing and links/names them based on id + +- `class AT(Enum)` + Link styles from https://franken-ui.dev/docs/link + Members: muted, text, reset, primary, classic + + +- `class ListT(Enum)` + List styles using Tailwind CSS + Members: disc, circle, square, decimal, hyphen, bullet, divider, striped + + +- `def ModalContainer(*c, **kwargs)` + Creates a modal container that components go in + +- `def ModalDialog(*c, **kwargs)` + Creates a modal dialog + +- `def ModalHeader(*c, **kwargs)` + Creates a modal header + +- `def ModalBody(*c, **kwargs)` + Creates a modal body + +- `def ModalFooter(*c, **kwargs)` + Creates a modal footer + +- `def ModalTitle(*c, **kwargs)` + Creates a modal title + +- `def ModalCloseButton(*c, **kwargs)` + Creates a button that closes a modal with js + +- `def Modal(*c, **kwargs)` + Creates a modal with the appropriate classes to put the boilerplate in the appropriate places for you + +- `def Placeholder(*c, **kwargs)` + Creates a placeholder + +- `def Progress(*c, **kwargs)` + Creates a progress bar + +- `def UkIcon(icon, height, width, stroke_width, cls, **kwargs)` + Creates an icon using lucide icons + +- `def UkIconLink(icon, height, width, stroke_width, cls, button, **kwargs)` + Creates an icon link using lucide icons + +- `def DiceBearAvatar(seed_name, h, w)` + Creates an Avatar using https://dicebear.com/ + +- `def Center(*c, **kwargs)` + Centers contents both vertically and horizontally by default + +- `class FlexT(Enum)` + Flexbox modifiers using Tailwind CSS + Members: block, inline, left, center, right, between, around, stretch, top, middle, bottom, row, row_reverse, column, column_reverse, nowrap, wrap, wrap_reverse + + +- `def Grid(*div, **kwargs)` + Creates a responsive grid layout with smart defaults based on content + +- `def DivFullySpaced(*c, **kwargs)` + Creates a flex div with it's components having as much space between them as possible + +- `def DivCentered(*c, **kwargs)` + Creates a flex div with it's components centered in it + +- `def DivLAligned(*c, **kwargs)` + Creates a flex div with it's components aligned to the left + +- `def DivRAligned(*c, **kwargs)` + Creates a flex div with it's components aligned to the right + +- `def DivVStacked(*c, **kwargs)` + Creates a flex div with it's components stacked vertically + +- `def DivHStacked(*c, **kwargs)` + Creates a flex div with it's components stacked horizontally + +- `class NavT(Enum)` + Members: default, primary, secondary + + +- `def NavContainer(*li, **kwargs)` + Creates a navigation container (useful for creating a sidebar navigation). A Nav is a list (NavBar is something different) + +- `def NavParentLi(*nav_container, **kwargs)` + Creates a navigation list item with a parent nav for nesting + +- `def NavDividerLi(*c, **kwargs)` + Creates a navigation list item with a divider + +- `def NavHeaderLi(*c, **kwargs)` + Creates a navigation list item with a header + +- `def NavSubtitle(*c, **kwargs)` + Creates a navigation subtitle + +- `def NavCloseLi(*c, **kwargs)` + Creates a navigation list item with a close button + +- `class ScrollspyT(Enum)` + Members: underline, bold + + +- `def NavBar(*c)` + Creates a responsive navigation bar with mobile menu support + +- `def SliderContainer(*c, **kwargs)` + Creates a slider container + +- `def SliderItems(*c, **kwargs)` + Creates a slider items container + +- `def SliderNav(cls, prev_cls, next_cls, **kwargs)` + Navigation arrows for Slider component + +- `def Slider(*c, **kwargs)` + Creates a slider with optional navigation arrows + +- `def DropDownNavContainer(*li, **kwargs)` + A Nav that is part of a DropDown + +- `def TabContainer(*li, **kwargs)` + A TabContainer where children will be different tabs + +- `class CardT(Enum)` + Card styles from UIkit + Members: default, primary, secondary, destructive, hover + + +- `def CardTitle(*c, **kwargs)` + Creates a card title + +- `def CardHeader(*c, **kwargs)` + Creates a card header + +- `def CardBody(*c, **kwargs)` + Creates a card body + +- `def CardFooter(*c, **kwargs)` + Creates a card footer + +- `def CardContainer(*c, **kwargs)` + Creates a card container + +- `def Card(*c, **kwargs)` + Creates a Card with a header, body, and footer + +- `class TableT(Enum)` + Members: divider, striped, hover, sm, lg, justify, middle, responsive + + +- `def Table(*c, **kwargs)` + Creates a table + +- `def TableFromLists(header_data, body_data, footer_data, header_cell_render, body_cell_render, footer_cell_render, cls, sortable, **kwargs)` + Creates a Table from a list of header data and a list of lists of body data + +- `def TableFromDicts(header_data, body_data, footer_data, header_cell_render, body_cell_render, footer_cell_render, cls, sortable, **kwargs)` + Creates a Table from a list of header data and a list of dicts of body data + +- `def apply_classes(html_str, class_map, class_map_mods)` + Apply classes to html string + +- `def render_md(md_content, class_map, class_map_mods)` + Renders markdown using mistletoe and lxml + +- `def get_franken_renderer(img_dir)` + Create a renderer class with the specified img_dir + +- `def ThemePicker(color, radii, shadows, font, mode, cls, custom_themes)` + Theme picker component with configurable sections + +- `def LightboxContainer(*lightboxitem, **kwargs)` + Lightbox container that will hold `LightboxItems` + +- `def LightboxItem(*c, **kwargs)` + Anchor tag with appropriate structure to go inside a `LightBoxContainer` + +- `def ApexChart(**kws)` + Apex chart component +from asyncio import sleep +from fasthtml.common import * + +app = FastHTML(exts='ws') +rt = app.route + +def mk_inp(): return Input(id='msg') +nid = 'notifications' + +@rt('/') +async def get(): + cts = Div( + Div(id=nid), + Form(mk_inp(), id='form', ws_send=True), + hx_ext='ws', ws_connect='/ws') + return Titled('Websocket Test', cts) + +async def on_connect(send): await send(Div('Hello, you have connected', id=nid)) +async def on_disconnect( ): print('Disconnected!') + +@app.ws('/ws', conn=on_connect, disconn=on_disconnect) +async def ws(msg:str, send): + await send(Div('Hello ' + msg, id=nid)) + await sleep(2) + return Div('Goodbye ' + msg, id=nid), mk_inp() + +serve() +### +# Walkthrough of an idiomatic fasthtml app +### + +# This fasthtml app includes functionality from fastcore, starlette, fastlite, and fasthtml itself. +# Run with: `python adv_app.py` +# Importing from `fasthtml.common` brings the key parts of all of these together. +# For simplicity, you can just `from fasthtml.common import *`: +from fasthtml.common import * +# ...or you can import everything into a namespace: +# from fasthtml import common as fh +# ...or you can import each symbol explicitly (which we're commenting out here but including for completeness): +""" +from fasthtml.common import ( + # These are the HTML components we use in this app + A, AX, Button, Card, CheckboxX, Container, Div, Form, Grid, Group, H1, H2, Hidden, Input, Li, Main, Script, Style, Textarea, Title, Titled, Ul, + # These are FastHTML symbols we'll use + Beforeware, FastHTML, fast_app, SortableJS, fill_form, picolink, serve, + # These are from Starlette, Fastlite, fastcore, and the Python stdlib + FileResponse, NotFoundError, RedirectResponse, database, patch, dataclass +) +""" + +from hmac import compare_digest + +# You can use any database you want; it'll be easier if you pick a lib that supports the MiniDataAPI spec. +# Here we are using SQLite, with the FastLite library, which supports the MiniDataAPI spec. +db = database('data/utodos.db') +# The `t` attribute is the table collection. The `todos` and `users` tables are not created if they don't exist. +# Instead, you can use the `create` method to create them if needed. +todos,users = db.t.todos,db.t.users +if todos not in db.t: + # You can pass a dict, or kwargs, to most MiniDataAPI methods. + users.create(dict(name=str, pwd=str), pk='name') + todos.create(id=int, title=str, done=bool, name=str, details=str, priority=int, pk='id') +# Although you can just use dicts, it can be helpful to have types for your DB objects. +# The `dataclass` method creates that type, and stores it in the object, so it will use it for any returned items. +Todo,User = todos.dataclass(),users.dataclass() + +# Any Starlette response class can be returned by a FastHTML route handler. +# In that case, FastHTML won't change it at all. +# Status code 303 is a redirect that can change POST to GET, so it's appropriate for a login page. +login_redir = RedirectResponse('/login', status_code=303) + +# The `before` function is a *Beforeware* function. These are functions that run before a route handler is called. +def before(req, sess): + # This sets the `auth` attribute in the request scope, and gets it from the session. + # The session is a Starlette session, which is a dict-like object which is cryptographically signed, + # so it can't be tampered with. + # The `auth` key in the scope is automatically provided to any handler which requests it, and can not + # be injected by the user using query params, cookies, etc, so it should be secure to use. + auth = req.scope['auth'] = sess.get('auth', None) + # If the session key is not there, it redirects to the login page. + if not auth: return login_redir + # `xtra` is part of the MiniDataAPI spec. It adds a filter to queries and DDL statements, + # to ensure that the user can only see/edit their own todos. + todos.xtra(name=auth) + +markdown_js = """ +import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; +proc_htmx('.markdown', e => e.innerHTML = marked.parse(e.textContent)); +""" + +# We will use this in our `exception_handlers` dict +def _not_found(req, exc): return Titled('Oh no!', Div('We could not find that page :(')) + +# To create a Beforeware object, we pass the function itself, and optionally a list of regexes to skip. +bware = Beforeware(before, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', '/login']) +# The `FastHTML` class is a subclass of `Starlette`, so you can use any parameters that `Starlette` accepts. +# In addition, you can add your Beforeware here, and any headers you want included in HTML responses. +# FastHTML includes the "HTMX" and "Surreal" libraries in headers, unless you pass `default_hdrs=False`. +app = FastHTML(before=bware, + # These are the same as Starlette exception_handlers, except they also support `FT` results + exception_handlers={404: _not_found}, + # PicoCSS is a particularly simple CSS framework, with some basic integration built in to FastHTML. + # `picolink` is pre-defined with the header for the PicoCSS stylesheet. + # You can use any CSS framework you want, or none at all. + hdrs=(picolink, + # `Style` is an `FT` object, which are 3-element lists consisting of: + # (tag_name, children_list, attrs_dict). + # FastHTML composes them from trees and auto-converts them to HTML when needed. + # You can also use plain HTML strings in handlers and headers, + # which will be auto-escaped, unless you use `NotStr(...string...)`. + Style(':root { --pico-font-size: 100%; }'), + # Have a look at fasthtml/js.py to see how these Javascript libraries are added to FastHTML. + # They are only 5-10 lines of code each, and you can add your own too. + SortableJS('.sortable'), + # MarkdownJS is actually provided as part of FastHTML, but we've included the js code here + # so that you can see how it works. + Script(markdown_js, type='module')) + ) +# We add `rt` as a shortcut for `app.route`, which is what we'll use to decorate our route handlers. +# When using `app.route` (or this shortcut), the only required argument is the path. +# The name of the decorated function (eg `get`, `post`, etc) is used as the HTTP verb for the handler. +rt = app.route + +# For instance, this function handles GET requests to the `/login` path. +@rt("/login") +def get(): + # This creates a form with two input fields, and a submit button. + # All of these components are `FT` objects. All HTML tags are provided in this form by FastHTML. + # If you want other custom tags (e.g. `MyTag`), they can be auto-generated by e.g + # `from fasthtml.components import MyTag`. + # Alternatively, manually call e.g `ft(tag_name, *children, **attrs)`. + frm = Form( + # Tags with a `name` attr will have `name` auto-set to the same as `id` if not provided + Input(id='name', placeholder='Name'), + Input(id='pwd', type='password', placeholder='Password'), + Button('login'), + action='/login', method='post') + # If a user visits the URL directly, FastHTML auto-generates a full HTML page. + # However, if the URL is accessed by HTMX, then one HTML partial is created for each element of the tuple. + # To avoid this auto-generation of a full page, return a `HTML` object, or a Starlette `Response`. + # `Titled` returns a tuple of a `Title` with the first arg and a `Container` with the rest. + # See the comments for `Title` later for details. + return Titled("Login", frm) + +# Handlers are passed whatever information they "request" in the URL, as keyword arguments. +# Dataclasses, dicts, namedtuples, TypedDicts, and custom classes are automatically instantiated +# from form data. +# In this case, the `Login` class is a dataclass, so the handler will be passed `name` and `pwd`. +@dataclass +class Login: name:str; pwd:str + +# This handler is called when a POST request is made to the `/login` path. +# The `login` argument is an instance of the `Login` class, which has been auto-instantiated from the form data. +# There are a number of special parameter names, which will be passed useful information about the request: +# `session`: the Starlette session; `request`: the Starlette request; `auth`: the value of `scope['auth']`, +# `htmx`: the HTMX headers, if any; `app`: the FastHTML app object. +# You can also pass any string prefix of `request` or `session`. +@rt("/login") +def post(login:Login, sess): + if not login.name or not login.pwd: return login_redir + # Indexing into a MiniDataAPI table queries by primary key, which is `name` here. + # It returns a dataclass object, if `dataclass()` has been called at some point, or a dict otherwise. + try: u = users[login.name] + # If the primary key does not exist, the method raises a `NotFoundError`. + # Here we use this to just generate a user -- in practice you'd probably to redirect to a signup page. + except NotFoundError: u = users.insert(login) + # This compares the passwords using a constant time string comparison + # https://sqreen.github.io/DevelopersSecurityBestPractices/timing-attack/python + if not compare_digest(u.pwd.encode("utf-8"), login.pwd.encode("utf-8")): return login_redir + # Because the session is signed, we can securely add information to it. It's stored in the browser cookies. + # If you don't pass a secret signing key to `FastHTML`, it will auto-generate one and store it in a file `./sesskey`. + sess['auth'] = u.name + return RedirectResponse('/', status_code=303) + +# Instead of using `app.route` (or the `rt` shortcut), you can also use `app.get`, `app.post`, etc. +# In this case, the function name is not used to determine the HTTP verb. +@app.get("/logout") +def logout(sess): + del sess['auth'] + return login_redir + +# FastHTML uses Starlette's path syntax, and adds a `static` type which matches standard static file extensions. +# You can define your own regex path specifiers -- for instance this is how `static` is defined in FastHTML +# `reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|xml|html")` +# In this app, we only actually have one static file, which is `favicon.ico`. But it would also be needed if +# we were referencing images, CSS/JS files, etc. +# Note, this function is unnecessary, as the `fast_app()` call already includes this functionality. +# However, it's included here to show how you can define your own static file handler. +@rt("/{fname:path}.{ext:static}") +def get(fname:str, ext:str): return FileResponse(f'{fname}.{ext}') + +# The `patch` decorator, which is defined in `fastcore`, adds a method to an existing class. +# Here we are adding a method to the `Todo` class, which is returned by the `todos` table. +# The `__ft__` method is a special method that FastHTML uses to convert the object into an `FT` object, +# so that it can be composed into an FT tree, and later rendered into HTML. +@patch +def __ft__(self:Todo): + # Some FastHTML tags have an 'X' suffix, which means they're "extended" in some way. + # For instance, here `AX` is an extended `A` tag, which takes 3 positional arguments: + # `(text, hx_get, target_id)`. + # All underscores in FT attrs are replaced with hyphens, so this will create an `hx-get` attr, + # which HTMX uses to trigger a GET request. + # Generally, most of your route handlers in practice (as in this demo app) are likely to be HTMX handlers. + # For instance, for this demo, we only have two full-page handlers: the '/login' and '/' GET handlers. + show = AX(self.title, f'/todos/{self.id}', 'current-todo') + edit = AX('edit', f'/edit/{self.id}' , 'current-todo') + dt = '✅ ' if self.done else '' + # FastHTML provides some shortcuts. For instance, `Hidden` is defined as simply: + # `return Input(type="hidden", value=value, **kwargs)` + cts = (dt, show, ' | ', edit, Hidden(id="id", value=self.id), Hidden(id="priority", value="0")) + # Any FT object can take a list of children as positional args, and a dict of attrs as keyword args. + return Li(*cts, id=f'todo-{self.id}') + +# This is the handler for the main todo list application. +# By including the `auth` parameter, it gets passed the current username, for displaying in the title. +@rt("/") +def get(auth): + title = f"{auth}'s Todo list" + top = Grid(H1(title), Div(A('logout', href='/logout'), style='text-align: right')) + # We don't normally need separate "screens" for adding or editing data. Here for instance, + # we're using an `hx-post` to add a new todo, which is added to the start of the list (using 'afterbegin'). + new_inp = Input(id="new-title", name="title", placeholder="New Todo") + add = Form(Group(new_inp, Button("Add")), + hx_post="/", target_id='todo-list', hx_swap="afterbegin") + # In the MiniDataAPI spec, treating a table as a callable (i.e with `todos(...)` here) queries the table. + # Because we called `xtra` in our Beforeware, this queries the todos for the current user only. + # We can include the todo objects directly as children of the `Form`, because the `Todo` class has `__ft__` defined. + # This is automatically called by FastHTML to convert the `Todo` objects into `FT` objects when needed. + # The reason we put the todo list inside a form is so that we can use the 'sortable' js library to reorder them. + # That library calls the js `end` event when dragging is complete, so our trigger here causes our `/reorder` + # handler to be called. + frm = Form(*todos(order_by='priority'), + id='todo-list', cls='sortable', hx_post="/reorder", hx_trigger="end") + # We create an empty 'current-todo' Div at the bottom of our page, as a target for the details and editing views. + card = Card(Ul(frm), header=add, footer=Div(id='current-todo')) + # PicoCSS uses `
` page content; `Container` is a tiny function that generates that. + # A handler can return either a single `FT` object or string, or a tuple of them. + # In the case of a tuple, the stringified objects are concatenated and returned to the browser. + # The `Title` tag has a special purpose: it sets the title of the page. + return Title(title), Container(top, card) + +# This is the handler for the reordering of todos. +# It's a POST request, which is used by the 'sortable' js library. +# Because the todo list form created earlier included hidden inputs with the todo IDs, +# they are passed as form data. By using a parameter called (e.g) "id", FastHTML will try to find +# something suitable in the request with this name. In order, it searches as follows: +# path; query; cookies; headers; session keys; form data. +# Although all these are provided in the request as strings, FastHTML will use your parameter's type +# annotation to try to cast the value to the requested type. +# In the case of form data, there can be multiple values with the same key. So in this case, +# the parameter is a list of ints. +@rt("/reorder") +def post(id:list[int]): + for i,id_ in enumerate(id): todos.update({'priority':i}, id_) + # HTMX by default replaces the inner HTML of the calling element, which in this case is the todo list form. + # Therefore, we return the list of todos, now in the correct order, which will be auto-converted to FT for us. + # In this case, it's not strictly necessary, because sortable.js has already reorder the DOM elements. + # However, by returning the updated data, we can be assured that there aren't sync issues between the DOM + # and the server. + return tuple(todos(order_by='priority')) + +# Refactoring components in FastHTML is as simple as creating Python functions. +# The `clr_details` function creates a Div with specific HTMX attributes. +# `hx_swap_oob='innerHTML'` tells HTMX to swap the inner HTML of the target element out-of-band, +# meaning it will update this element regardless of where the HTMX request originated from. +def clr_details(): return Div(hx_swap_oob='innerHTML', id='current-todo') + +# This route handler uses a path parameter `{id}` which is automatically parsed and passed as an int. +@rt("/todos/{id}") +def delete(id:int): + # The `delete` method is part of the MiniDataAPI spec, removing the item with the given primary key. + todos.delete(id) + # Returning `clr_details()` ensures the details view is cleared after deletion, + # leveraging HTMX's out-of-band swap feature. + # Note that we are not returning *any* FT component that doesn't have an "OOB" swap, so the target element + # inner HTML is simply deleted. That's why the deleted todo is removed from the list. + return clr_details() + +@rt("/edit/{id}") +def get(id:int): + # The `hx_put` attribute tells HTMX to send a PUT request when the form is submitted. + # `target_id` specifies which element will be updated with the server's response. + res = Form(Group(Input(id="title"), Button("Save")), + Hidden(id="id"), CheckboxX(id="done", label='Done'), + Textarea(id="details", name="details", rows=10), + hx_put="/", target_id=f'todo-{id}', id="edit") + # `fill_form` populates the form with existing todo data, and returns the result. + # Indexing into a table (`todos`) queries by primary key, which is `id` here. It also includes + # `xtra`, so this will only return the id if it belongs to the current user. + return fill_form(res, todos[id]) + +@rt("/") +def put(todo: Todo): + # `update` is part of the MiniDataAPI spec. + # Note that the updated todo is returned. By returning the updated todo, we can update the list directly. + # Because we return a tuple with `clr_details()`, the details view is also cleared. + return todos.update(todo), clr_details() + +@rt("/") +def post(todo:Todo): + # `hx_swap_oob='true'` tells HTMX to perform an out-of-band swap, updating this element wherever it appears. + # This is used to clear the input field after adding the new todo. + new_inp = Input(id="new-title", name="title", placeholder="New Todo", hx_swap_oob='true') + # `insert` returns the inserted todo, which is appended to the start of the list, because we used + # `hx_swap='afterbegin'` when creating the todo list form. + return todos.insert(todo), new_inp + +@rt("/todos/{id}") +def get(id:int): + todo = todos[id] + # `hx_swap` determines how the update should occur. We use "outerHTML" to replace the entire todo `Li` element. + btn = Button('delete', hx_delete=f'/todos/{todo.id}', + target_id=f'todo-{todo.id}', hx_swap="outerHTML") + # The "markdown" class is used here because that's the CSS selector we used in the JS earlier. + # Therefore this will trigger the JS to parse the markdown in the details field. + # Because `class` is a reserved keyword in Python, we use `cls` instead, which FastHTML auto-converts. + return Div(H2(todo.title), Div(todo.details, cls="markdown"), btn) + +serve()# 🗿 Surreal +### Tiny jQuery alternative for plain Javascript with inline [Locality of Behavior](https://htmx.org/essays/locality-of-behaviour/)! + +![cover](https://user-images.githubusercontent.com/24665/171092805-b41286b2-be4a-4aab-9ee6-d604699cc507.png) +(Art by [shahabalizadeh](https://www.deviantart.com/shahabalizadeh)) + + +## Why does this exist? + +For devs who love ergonomics! You may appreciate Surreal if: + +* You want to stay as close as possible to Vanilla JS. +* Hate typing `document.querySelector` over.. and over.. +* Hate typing `addEventListener` over.. and over.. +* Really wish `document.querySelectorAll` had Array functions.. +* Really wish `this` would work in any inline ` + +``` + +See the [Live Example](https://gnat.github.io/surreal/example.html)! Then [view source](https://github.com/gnat/surreal/blob/main/example.html). + +## 🎁 Install + +Surreal is only 320 lines. No build step. No dependencies. + +[📥 Download](https://raw.githubusercontent.com/gnat/surreal/main/surreal.js) into your project, and add `` in your `` + +Or, 🌐 via CDN: `` + +## ⚡ Usage + +### 🔍️ DOM Selection + +* Select **one** element: `me(...)` + * Can be any of: + * CSS selector: `".button"`, `"#header"`, `"h1"`, `"body > .block"` + * Variables: `body`, `e`, `some_element` + * Events: `event.currentTarget` will be used. + * Surreal selectors: `me()`,`any()` + * Choose the start location in the DOM with the 2nd arg. (Default: `document`) + * 🔥 `any('button', me('#header')).classAdd('red')` + * Add `.red` to any ` + +
+
+ + + + + +
+ + +
+ + + +
+ +
+
+

FastHTML Best Practices

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

FastHTML applications are different to applications using FastAPI/react, Django, etc. Don’t assume that FastHTML best practices are the same as those for other frameworks. Best practices embody the fast.ai philosophy: remove ceremony, leverage smart defaults, and write code that’s both concise and clear. The following are some particular opportunities that both humans and language models sometimes miss:

+
+

Database Table Creation

+

Before:

+
todos = db.t.todos
+if not todos.exists():
+todos.create(id=int, task=str, completed=bool, created=str, pk='id')
+

After:

+
class Todo: id:int; task:str; completed:bool; created:str
+todos = db.create(Todo)
+

FastLite’s create() is idempotent - it creates the table if needed and returns the table object either way. Using a dataclass-style definition is cleaner and more Pythonic. The id field is automatically the primary key.

+
+
+

Route Naming Conventions

+

Before:

+
@rt("/")
+def get(): return Titled("Todo List", ...)
+
+@rt("/add")
+def post(task: str): ...
+

After:

+
@rt
+def index(): return Titled("Todo List", ...) # Special name for "/"
+@rt
+def add(task: str): ... # Function name becomes route
+

Use @rt without arguments and let the function name define the route. The special name index maps to /.

+
+
+

Query Parameters over Path Parameters

+

Before:

+
@rt("/toggle/{todo_id}")
+def post(todo_id: int): ...
+# URL: /toggle/123
+

After:

+
@rt
+def toggle(id: int): ...
+# URL: /toggle?id=123
+

Query parameters are more idiomatic in FastHTML and avoid duplicating param names in the path.

+
+
+

Leverage Return Values

+
+

Before:

+
@rt
+def add(task: str):
+  new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat())
+  return todo_item(todos[new_todo])
+
+@rt
+def toggle(id: int):
+  todo = todos[id]
+  todos.update(completed=not todo.completed, id=id)
+  return todo_item(todos[id])
+

After:

+
@rt
+def add(task: str):
+  return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat()))
+
+@rt
+def toggle(id: int):
+  return todo_item(todos.update(completed=not todos[id].completed, id=id))
+

Both insert() and update() return the affected object, enabling functional chaining.

+
+
+
+

Use .to() for URL Generation

+

Before:

+
hx_post=f"/toggle?id={todo.id}"
+

After:

+
hx_post=toggle.to(id=todo.id)
+

The .to() method generates URLs with type safety and is refactoring-friendly.

+
+
+

PicoCSS comes free

+

Before:

+
style = Style("""
+.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; }
+/* ... many more lines ... */
+""")
+

After:

+
# Just use semantic HTML - Pico styles it automatically
+Container(...), Article(...), Card(...), Group(...)
+

fast_app() includes PicoCSS by default. Use semantic HTML elements that Pico styles automatically. Use MonsterUI (like shadcn, but for FastHTML) for more complex UI needs.

+
+
+

Smart Defaults

+

Before:

+
return Titled("Todo List", Container(...))
+
+if __name__ == "__main__":
+  serve()
+

After:

+
return Titled("Todo List", ...)  # Container is automatic
+
+serve()  # No need for if __name__ guard
+

Titled already wraps content in a Container, and serve() handles the main check internally.

+
+
+

FastHTML Handles Iterables

+

Before:

+
Section(*[todo_item(todo) for todo in all_todos], id="todo-list")
+

After:

+
Section(map(todo_item, all_todos), id="todo-list")
+

FastHTML components accept iterables directly - no need to unpack with *.

+
+
+

Functional Patterns

+

List comprehensions are great, but map() is often cleaner for simple transformations, especially when combined with FastHTML’s iterable handling.

+
+
+

Minimal Code

+

Before:

+
@rt
+def delete(id: int):
+  # Delete from database
+  todos.delete(id)
+  # Return empty response
+  return ""
+

After:

+
@rt
+def delete(id: int): todos.delete(id)
+
    +
  • Skip comments when code is self-documenting
  • +
  • Don’t return empty strings - None is returned by default
  • +
  • Use a single line for a single idea.
  • +
+
+
+

Use POST for All Mutations

+

Before:

+
hx_delete=f"/delete?id={todo.id}"
+

After:

+
hx_post=delete.to(id=todo.id)
+

FastHTML routes handle only GET and POST by default. Using only these two verbs is more idiomatic and simpler.

+
+
+

Modern HTMX Event Syntax

+

Before:

+
hx_on="htmx:afterRequest: this.reset()"
+

After:

+
hx_on__after_request="this.reset()"
+

This works because:

+
    +
  • hx-on="event: code" is deprecated; hx-on-event="code" is preferred
  • +
  • FastHTML converts _ to - (so hx_on__after_request becomes hx-on--after-request)
  • +
  • :: in HTMX can be used as a shortcut for :htmx:.
  • +
  • HTMX natively accepts - instead of : (so -htmx- works like :htmx:)
  • +
  • HTMX accepts e.g after-request as an alternative to camelCase afterRequest
  • +
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/ref/best_practice.md b/docs/ref/best_practice.md new file mode 100644 index 0000000000000000000000000000000000000000..5d0b0b02a3b430dbb1e5aea907b03449f20c76be --- /dev/null +++ b/docs/ref/best_practice.md @@ -0,0 +1,262 @@ +# FastHTML Best Practices + + +FastHTML applications are different to applications using FastAPI/react, +Django, etc. Don’t assume that FastHTML best practices are the same as +those for other frameworks. Best practices embody the fast.ai +philosophy: remove ceremony, leverage smart defaults, and write code +that’s both concise and clear. The following are some particular +opportunities that both humans and language models sometimes miss: + +## Database Table Creation + +**Before:** + +``` python +todos = db.t.todos +if not todos.exists(): +todos.create(id=int, task=str, completed=bool, created=str, pk='id') +``` + +**After:** + +``` python +class Todo: id:int; task:str; completed:bool; created:str +todos = db.create(Todo) +``` + +FastLite’s `create()` is idempotent - it creates the table if needed and +returns the table object either way. Using a dataclass-style definition +is cleaner and more Pythonic. The `id` field is automatically the +primary key. + +## Route Naming Conventions + +**Before:** + +``` python +@rt("/") +def get(): return Titled("Todo List", ...) + +@rt("/add") +def post(task: str): ... +``` + +**After:** + +``` python +@rt +def index(): return Titled("Todo List", ...) # Special name for "/" +@rt +def add(task: str): ... # Function name becomes route +``` + +Use `@rt` without arguments and let the function name define the route. +The special name `index` maps to `/`. + +## Query Parameters over Path Parameters + +**Before:** + +``` python +@rt("/toggle/{todo_id}") +def post(todo_id: int): ... +# URL: /toggle/123 +``` + +**After:** + +``` python +@rt +def toggle(id: int): ... +# URL: /toggle?id=123 +``` + +Query parameters are more idiomatic in FastHTML and avoid duplicating +param names in the path. + +## Leverage Return Values + +
+ +**Before:** + +``` python +@rt +def add(task: str): + new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat()) + return todo_item(todos[new_todo]) + +@rt +def toggle(id: int): + todo = todos[id] + todos.update(completed=not todo.completed, id=id) + return todo_item(todos[id]) +``` + +**After:** + +``` python +@rt +def add(task: str): + return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat())) + +@rt +def toggle(id: int): + return todo_item(todos.update(completed=not todos[id].completed, id=id)) +``` + +Both `insert()` and `update()` return the affected object, enabling +functional chaining. + +
+ +## Use `.to()` for URL Generation + +**Before:** + +``` python +hx_post=f"/toggle?id={todo.id}" +``` + +**After:** + +``` python +hx_post=toggle.to(id=todo.id) +``` + +The `.to()` method generates URLs with type safety and is +refactoring-friendly. + +## PicoCSS comes free + +**Before:** + +``` python +style = Style(""" +.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; } +/* ... many more lines ... */ +""") +``` + +**After:** + +``` python +# Just use semantic HTML - Pico styles it automatically +Container(...), Article(...), Card(...), Group(...) +``` + +`fast_app()` includes PicoCSS by default. Use semantic HTML elements +that Pico styles automatically. Use MonsterUI (like shadcn, but for +FastHTML) for more complex UI needs. + +## Smart Defaults + +**Before:** + +``` python +return Titled("Todo List", Container(...)) + +if __name__ == "__main__": + serve() +``` + +**After:** + +``` python +return Titled("Todo List", ...) # Container is automatic + +serve() # No need for if __name__ guard +``` + +`Titled` already wraps content in a `Container`, and `serve()` handles +the main check internally. + +## FastHTML Handles Iterables + +**Before:** + +``` python +Section(*[todo_item(todo) for todo in all_todos], id="todo-list") +``` + +**After:** + +``` python +Section(map(todo_item, all_todos), id="todo-list") +``` + +FastHTML components accept iterables directly - no need to unpack with +`*`. + +## Functional Patterns + +List comprehensions are great, but `map()` is often cleaner for simple +transformations, especially when combined with FastHTML’s iterable +handling. + +## Minimal Code + +**Before:** + +``` python +@rt +def delete(id: int): + # Delete from database + todos.delete(id) + # Return empty response + return "" +``` + +**After:** + +``` python +@rt +def delete(id: int): todos.delete(id) +``` + +- Skip comments when code is self-documenting +- Don’t return empty strings - `None` is returned by default +- Use a single line for a single idea. + +## Use POST for All Mutations + +**Before:** + +``` python +hx_delete=f"/delete?id={todo.id}" +``` + +**After:** + +``` python +hx_post=delete.to(id=todo.id) +``` + +FastHTML routes handle only GET and POST by default. Using only these +two verbs is more idiomatic and simpler. + +## Modern HTMX Event Syntax + +**Before:** + +``` python +hx_on="htmx:afterRequest: this.reset()" +``` + +**After:** + +``` python +hx_on__after_request="this.reset()" +``` + +This works because: + +- `hx-on="event: code"` is deprecated; `hx-on-event="code"` is preferred +- FastHTML converts `_` to `-` (so `hx_on__after_request` becomes + `hx-on--after-request`) +- `::` in HTMX can be used as a shortcut for `:htmx:`. +- HTMX natively accepts `-` instead of `:` (so `-htmx-` works like + `:htmx:`) +- HTMX accepts e.g `after-request` as an alternative to camelCase + `afterRequest` diff --git a/docs/ref/concise_guide.html b/docs/ref/concise_guide.html new file mode 100644 index 0000000000000000000000000000000000000000..f907c329714ce467b1bbf8491de713b02c27ae2b --- /dev/null +++ b/docs/ref/concise_guide.html @@ -0,0 +1,1519 @@ + + + + + + + + + + +Concise reference – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Concise reference

+
+ +
+
+ An information-dense guide showing LLMs how to create FastHTML apps with MonsterUI and Fastlite +
+
+ + +
+ + + + +
+ + + +
+ + + +
+

About FastHTML

+
+
from fasthtml.common import *
+
+

FastHTML is a python library which brings together Starlette, Uvicorn, HTMX, and fastcore’s FT “FastTags” into a library for creating server-rendered hypermedia applications. The FastHTML class itself inherits from Starlette, and adds decorator-based routing with many additions, Beforeware, automatic FT to HTML rendering, and much more.

+

Things to remember when writing FastHTML apps:

+
    +
  • Not compatible with FastAPI syntax; FastHTML is for HTML-first apps, not API services (although it can implement APIs too)
  • +
  • FastHTML includes support for Pico CSS and the fastlite sqlite library, although using both are optional; sqlalchemy can be used directly or via the fastsql library, and any CSS framework can be used. MonsterUI is a richer FastHTML-first component framework with similar capabilities to shadcn
  • +
  • FastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte
  • +
  • Use serve() for running uvicorn (if __name__ == "__main__" is not needed since it’s automatic)
  • +
  • When a title is needed with a response, use Titled; note that that already wraps children in Container, and already includes both the meta title as well as the H1 element.
  • +
+
+
+

Minimal App

+

The code examples here use fast.ai style: prefer ternary op, 1-line docstring, minimize vertical space, etc. (Normally fast.ai style uses few if any comments, but they’re added here as documentation.)

+

A minimal FastHTML app looks something like this:

+
# Meta-package with all key symbols from FastHTML and Starlette. Import it like this at the start of every FastHTML app.
+from fasthtml.common import *
+# The FastHTML app object and shortcut to `app.route`
+app,rt = fast_app()
+
+# Enums constrain the values accepted for a route parameter
+name = str_enum('names', 'Alice', 'Bev', 'Charlie')
+
+# Passing a path to `rt` is optional. If not passed (recommended), the function name is the route ('/foo')
+# Both GET and POST HTTP methods are handled by default
+# Type-annotated params are passed as query params (recommended) unless a path param is defined (which it isn't here)
+@rt
+def foo(nm: name):
+    # `Title` and `P` here are FastTags: direct m-expression mappings of HTML tags to Python functions with positional and named parameters. All standard HTML tags are included in the common wildcard import.
+    # When a tuple is returned, this returns concatenated HTML partials. HTMX by default will use a title HTML partial to set the current page name. HEAD tags (e.g. Meta, Link, etc) in the returned tuple are automatically placed in HEAD; everything else is placed in BODY.
+    # FastHTML will automatically return a complete HTML document with appropriate headers if a normal HTTP request is received. For an HTMX request, however, just the partials are returned.
+    return Title("FastHTML"), H1("My web app"), P(f"Hello, {name}!")
+# By default `serve` runs uvicorn on port 5001. Never write `if __name__ == "__main__"` since `serve` checks it internally.
+serve()
+

To run this web app:

+
python main.py  # access via localhost:5001
+
+
+

FastTags (aka FT Components or FTs)

+

FTs are m-expressions plus simple sugar. Positional params map to children. Named parameters map to attributes. Aliases must be used for Python reserved words.

+
+
tags = Title("FastHTML"), H1("My web app"), P(f"Let's do this!", cls="myclass")
+tags
+
+
(title(('FastHTML',),{}),
+ h1(('My web app',),{}),
+ p(("Let's do this!",),{'class': 'myclass'}))
+
+
+

This example shows key aspects of how FTs handle attributes:

+
Label(
+    "Choose an option", 
+    Select(
+        Option("one", value="1", selected=True),  # True renders just the attribute name
+        Option("two", value=2, selected=False),   # Non-string values are converted to strings. False omits the attribute entirely
+        cls="selector", id="counter",             # 'cls' becomes 'class'
+        **{'@click':"alert('Clicked');"},         # Dict unpacking for attributes with special chars
+    ),
+    _for="counter",                               # '_for' becomes 'for' (can also use 'fr')
+)
+

Classes with __ft__ defined are rendered using that method.

+
+
class FtTest:
+    def __ft__(self): return P('test')
+    
+to_xml(FtTest())
+
+
'<p>test</p>\n'
+
+
+

You can create new FTs by importing the new component from fasthtml.components. If the FT doesn’t exist within that module, FastHTML will create it.

+
+
from fasthtml.components import Some_never_before_used_tag
+
+Some_never_before_used_tag()
+
+
<some-never-before-used-tag></some-never-before-used-tag>
+
+
+

FTs can be combined by defining them as a function.

+
+
def Hero(title, statement): return Div(H1(title),P(statement), cls="hero")
+to_xml(Hero("Hello World", "This is a hero statement"))
+
+
'<div class="hero">\n  <h1>Hello World</h1>\n  <p>This is a hero statement</p>\n</div>\n'
+
+
+

When handling a response, FastHTML will automatically render FTs using the to_xml function.

+
+
to_xml(tags)
+
+
'<title>FastHTML</title>\n<h1>My web app</h1>\n<p class="myclass">Let&#x27;s do this!</p>\n'
+
+
+
+
+

JS

+

The Script function allows you to include JavaScript. You can use Python to generate parts of your JS or JSON like this:

+
# In future snippets this import will not be shown, but is required
+from fasthtml.common import * 
+app,rt = fast_app(hdrs=[Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js")])
+# `index` is a special function name which maps to the `/` route. 
+@rt
+def index():
+    data = {'somedata':'fill me in…'}
+    # `Titled` returns a title tag and an h1 tag with the 1st param, with remaining params as children in a `Main` parent.
+    return Titled("Chart Demo", Div(id="myDiv"), Script(f"var data = {data}; Plotly.newPlot('myDiv', data);"))
+# In future snippets `serve() will not be shown, but is required
+serve()
+

Prefer Python whenever possible over JS. Never use React or shadcn.

+
+
+

fast_app hdrs

+
# In future snippets we'll skip showing the `fast_app` call if it has no params
+app, rt = fast_app(
+    pico=False, # The Pico CSS framework is included by default, so pass `False` to disable it if needed. No other CSS frameworks are included.
+    # These are added to the `head` part of the page for non-HTMX requests.
+    hdrs=(
+        Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),
+        Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),
+        Style("p {color: red;}"),
+        # `MarkdownJS` and `HighlightJS` are available via concise functions
+        MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']),
+        # by default, all standard static extensions are served statically from the web app dir,
+        #   which can be modified using e.g `static_path='public'`
+        )
+)
+
+@rt
+def index(req): return Titled("Markdown rendering example",
+                              # This will be client-side rendered to HTML with highlight-js
+                              Div("*hi* there",cls="marked"),
+                              # This will be syntax highlighted
+                              Pre(Code("def foo(): pass")))
+
+
+

Responses

+

Routes can return various types:

+
    +
  1. FastTags or tuples of FastTags (automatically rendered to HTML)
  2. +
  3. Standard Starlette responses (used directly)
  4. +
  5. JSON-serializable types (returned as JSON in a plain text response)
  6. +
+
@rt("/{fname:path}.{ext:static}")
+async def serve_static_file(fname:str, ext:str): return FileResponse(f'public/{fname}.{ext}')
+
+app, rt = fast_app(hdrs=(MarkdownJS(), HighlightJS(langs=['python', 'javascript'])))
+@rt
+def index(): 
+    return Titled("Example",
+                  Div("*markdown* here", cls="marked"),
+                  Pre(Code("def foo(): pass")))
+

Route functions can be used in attributes like href or action and will be converted to paths. Use .to() to generate paths with query parameters.

+
@rt
+def profile(email:str): return fill_form(profile_form, profiles[email])
+
+profile_form = Form(action=profile)(
+    Label("Email", Input(name="email")),
+    Button("Save", type="submit")
+)
+
+user_profile_path = profile.to(email="user@example.com")  # '/profile?email=user%40example.com'
+
+
from dataclasses import dataclass
+
+app,rt = fast_app()
+
+

When a route handler function is used as a fasttag attribute (such as href, hx_get, or action) it is converted to that route’s path. fill_form is used to copy an object’s matching attrs into matching-name form fields.

+
+
@dataclass
+class Profile: email:str; phone:str; age:int
+email = 'john@example.com'
+profiles = {email: Profile(email=email, phone='123456789', age=5)}
+@rt
+def profile(email:str): return fill_form(profile_form, profiles[email])
+
+profile_form = Form(method="post", action=profile)(
+        Fieldset(
+            Label('Email', Input(name="email")),
+            Label("Phone", Input(name="phone")),
+            Label("Age", Input(name="age"))),
+        Button("Save", type="submit"))
+
+
+
+

Testing

+

We can use TestClient for testing.

+
+
from starlette.testclient import TestClient
+
+
+
path = "/profile?email=john@example.com"
+client = TestClient(app)
+htmx_req = {'HX-Request':'1'}
+print(client.get(path, headers=htmx_req).text)
+
+
<form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email       <input name="email" value="john@example.com">
+</label><label>Phone       <input name="phone" value="123456789">
+</label><label>Age       <input name="age" value="5">
+</label></fieldset><button type="submit">Save</button></form>
+
+
+
+
+

Form Handling and Data Binding

+

When a dataclass, namedtuple, etc. is used as a type annotation, the form body will be unpacked into matching attribute names automatically.

+
+
@rt
+def edit_profile(profile: Profile):
+    profiles[email]=profile
+    return RedirectResponse(url=path)
+
+new_data = dict(email='john@example.com', phone='7654321', age=25)
+print(client.post("/edit_profile", data=new_data, headers=htmx_req).text)
+
+
<form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email       <input name="email" value="john@example.com">
+</label><label>Phone       <input name="phone" value="7654321">
+</label><label>Age       <input name="age" value="25">
+</label></fieldset><button type="submit">Save</button></form>
+
+
+
+
+

fasttag Rendering Rules

+

The general rules for rendering children inside tuples or fasttag children are: - __ft__ method will be called (for default components like P, H2, etc. or if you define your own components) - If you pass a string, it will be escaped - On other python objects, str() will be called

+

If you want to include plain HTML tags directly into e.g. a Div() they will get escaped by default (as a security measure to avoid code injections). This can be avoided by using Safe(...), e.g to show a data frame use Div(NotStr(df.to_html())).

+
+
+

Exceptions

+

FastHTML allows customization of exception handlers.

+
def not_found(req, exc): return Titled("404: I don't exist!")
+exception_handlers = {404: not_found}
+app, rt = fast_app(exception_handlers=exception_handlers)
+
+
+

Cookies

+

We can set cookies using the cookie() function.

+
+
@rt
+def setcook(): return P(f'Set'), cookie('mycookie', 'foobar')
+print(client.get('/setcook', headers=htmx_req).text)
+
+
 <p>Set</p>
+
+
+
+
+
@rt
+def getcook(mycookie:str): return f'Got {mycookie}'
+# If handlers return text instead of FTs, then a plaintext response is automatically created
+print(client.get('/getcook').text)
+
+
Got foobar
+
+
+

FastHTML provide access to Starlette’s request object automatically using special request parameter name (or any prefix of that name).

+
+
@rt
+def headers(req): return req.headers['host']
+
+
+
+

Request and Session Objects

+

FastHTML provides access to Starlette’s session middleware automatically using the special session parameter name (or any prefix of that name).

+
+
@rt
+def profile(req, sess, user_id: int=None):
+    ip = req.client.host
+    sess['last_visit'] = datetime.now().isoformat()
+    visits = sess.setdefault('visit_count', 0) + 1
+    sess['visit_count'] = visits
+    user = get_user(user_id or sess.get('user_id'))
+    return Titled(f"Profile: {user.name}", 
+                  P(f"Visits: {visits}"), 
+                  P(f"IP: {ip}"),
+                  Button("Logout", hx_post=logout))
+
+

Handler functions can return the HtmxResponseHeaders object to set HTMX-specific response headers.

+
+
@rt
+def htmlredirect(app): return HtmxResponseHeaders(location="http://example.org")
+
+
+
+

APIRouter

+

APIRouter lets you organize routes across multiple files in a FastHTML app.

+
# products.py
+ar = APIRouter()
+
+@ar
+def details(pid: int): return f"Here are the product details for ID: {pid}"
+
+@ar
+def all_products(req):
+    return Div(
+        Div(
+            Button("Details",hx_get=details.to(pid=42),hx_target="#products_list",hx_swap="outerHTML",),
+        ), id="products_list")
+
# main.py
+from products import ar,all_products
+
+app, rt = fast_app()
+ar.to_app(app)
+
+@rt
+def index():
+    return Div(
+        "Products",
+        hx_get=all_products, hx_swap="outerHTML")
+
+
+

Toasts

+

Toasts can be of four types:

+
    +
  • info
  • +
  • success
  • +
  • warning
  • +
  • error
  • +
+

Toasts require the use of the setup_toasts() function, plus every handler needs:

+
    +
  • The session argument
  • +
  • Must return FT components
  • +
+
setup_toasts(app)
+
+@rt
+def toasting(session):
+    add_toast(session, f"cooked", "info")
+    add_toast(session, f"ready", "success")
+    return Titled("toaster")
+

setup_toasts(duration) allows you to specify how long a toast will be visible before disappearing.10 seconds.

+

Authentication and authorization are handled with Beforeware, which functions that run before the route handler is called.

+
+
+

Auth

+
+
def user_auth_before(req, sess):
+    # `auth` key in the request scope is automatically provided to any handler which requests it and can not be injected
+    auth = req.scope['auth'] = sess.get('auth', None)
+    if not auth: return RedirectResponse('/login', status_code=303)
+
+beforeware = Beforeware(
+    user_auth_before,
+    skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/']
+)
+
+app, rt = fast_app(before=beforeware)
+
+
+
+

Server-Side Events (SSE)

+

FastHTML supports the HTMX SSE extension.

+
+
import random
+hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.3/sse.js"),)
+app,rt = fast_app(hdrs=hdrs)
+
+@rt
+def index(): return Div(hx_ext="sse", sse_connect="/numstream", hx_swap="beforeend show:bottom", sse_swap="message")
+
+# `signal_shutdown()` gets an event that is set on shutdown
+shutdown_event = signal_shutdown()
+
+async def number_generator():
+    while not shutdown_event.is_set():
+        data = Article(random.randint(1, 100))
+        yield sse_message(data)
+
+@rt
+async def numstream(): return EventStream(number_generator())
+
+
+
+

Websockets

+

FastHTML provides useful tools for HTMX’s websockets extension.

+
+
# These HTMX extensions are available through `exts`:
+#   head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer
+app, rt = fast_app(exts='ws')
+
+def mk_inp(): return Input(id='msg', autofocus=True)
+
+@rt
+async def index(request):
+    # `ws_send` tells HTMX to send a message to the nearest websocket based on the trigger for the form element
+    cts = Div(
+        Div(id='notifications'),
+        Form(mk_inp(), id='form', ws_send=True),
+        hx_ext='ws', ws_connect='/ws')
+    return Titled('Websocket Test', cts)
+
+async def on_connect(send): await send(Div('Hello, you have connected', id="notifications"))
+async def on_disconnect(ws): print('Disconnected!')
+
+@app.ws('/ws', conn=on_connect, disconn=on_disconnect)
+async def ws(msg:str, send):
+    # websocket hander returns/sends are treated as OOB swaps
+    await send(Div('Hello ' + msg, id="notifications"))
+    return Div('Goodbye ' + msg, id="notifications"), mk_inp()
+
+

Sample chatbot that uses FastHTML’s setup_ws function:

+
app = FastHTML(exts='ws')
+rt = app.route
+msgs = []
+
+@rt('/')
+def home():
+    return Div(hx_ext='ws', ws_connect='/ws')(
+        Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
+        Form(Input(id='msg'), id='form', ws_send=True)
+    )
+
+async def ws(msg:str):
+    msgs.append(msg)
+    await send(Ul(*[Li(m) for m in msgs], id='msg-list'))
+
+send = setup_ws(app, ws)
+
+

Single File Uploads

+

Form defaults to “multipart/form-data”. A Starlette UploadFile is passed to the handler.

+
upload_dir = Path("filez")
+
+@rt
+def index():
+    return (
+        Form(hx_post=upload, hx_target="#result")(
+            Input(type="file", name="file"),
+            Button("Upload", type="submit")),
+        Div(id="result")
+    )
+
+# Use `async` handlers where IO is used to avoid blocking other clients
+@rt
+async def upload(file: UploadFile):
+    filebuffer = await file.read()
+    (upload_dir / file.filename).write_bytes(filebuffer)
+    return P('Size: ', file.size)
+

For multi-file, use Input(..., multiple=True), and a type annotation of list[UploadFile] in the handler.

+
+
+
+

Fastlite

+

Fastlite and the MiniDataAPI specification it’s built on are a CRUD-oriented API for working with SQLite. APSW and apswutils is used to connect to SQLite, optimized for speed and clean error handling.

+
+
from fastlite import *
+
+
+
db = database(':memory:') # or database('data/app.db')
+
+

Tables are normally constructed with classes, field types are specified as type hints.

+
+
class Book: isbn: str; title: str; pages: int; userid: int
+# The transform arg instructs fastlite to change the db schema when fields change.
+# Create only creates a table if the table doesn't exist.
+books = db.create(Book, pk='isbn', transform=True)
+                
+class User: id: int; name: str; active: bool = True
+# If no pk is provided, id is used as the primary key.
+users = db.create(User, transform=True)
+users
+
+
<Table user (id, name, active)>
+
+
+
+

Fastlite CRUD operations

+

Every operation in fastlite returns a full superset of dataclass functionality.

+
+
user = users.insert(name='Alex',active=False)
+user
+
+
User(id=1, name='Alex', active=0)
+
+
+
+
# List all records
+users()
+
+
[User(id=1, name='Alex', active=0)]
+
+
+
+
# Limit, offset, and order results:
+users(order_by='name', limit=2, offset=1)
+
+# Filter on the results
+users(where="name='Alex'")
+
+# Placeholder for avoiding injection attacks
+users("name=?", ('Alex',))
+
+# A single record by pk
+users[user.id]
+
+
User(id=1, name='Alex', active=0)
+
+
+

Test if a record exists by using in keyword on primary key:

+
+
1 in users
+
+
True
+
+
+

Updates (which take a dict or a typed object) return the updated record.

+
+
user.name='Lauren'
+user.active=True
+users.update(user)
+
+
User(id=1, name='Lauren', active=1)
+
+
+

.xtra() to automatically constrain queries, updates, and inserts from there on:

+
+
users.xtra(active=True)
+users()
+
+
[User(id=1, name='Lauren', active=1)]
+
+
+

Deleting by pk:

+
+
users.delete(user.id)
+
+
<Table user (id, name, active)>
+
+
+

NotFoundError is raised by pk [], updates, and deletes.

+
+
try: users['Amy']
+except NotFoundError: print('User not found')
+
+
User not found
+
+
+
+
+
+

MonsterUI

+

MonsterUI is a shadcn-like component library for FastHTML. It adds the Tailwind-based libraries FrankenUI and DaisyUI to FastHTML, as well as Python’s mistletoe for Markdown, HighlightJS for code highlighting, and Katex for latex support, following semantic HTML patterns when possible. It is recommended for when you wish to go beyond the basics provided by FastHTML’s built-in pico support.

+

A minimal app:

+
+
from fasthtml.common import *
+from monsterui.all import *
+
+app, rt = fast_app(hdrs=Theme.blue.headers(highlightjs=True)) # Use MonsterUI blue theme and highlight code in markdown
+
+@rt
+def index():
+    socials = (('github','https://github.com/AnswerDotAI/MonsterUI'),)
+    return Titled("App",
+        Card(
+            P("App", cls=TextPresets.muted_sm),
+            # LabelInput, DivLAigned, and UkIconLink are non-semantic MonsterUI FT Components,
+            LabelInput('Email', type='email', required=True),
+            footer=DivLAligned(*[UkIconLink(icon,href=url) for icon,url in socials])))
+
+

MonsterUI recommendations:

+
    +
  • Use defaults as much as possible, for example Container in monsterui already has defaults for margins
  • +
  • Use *T for button styling consistency, for example cls=ButtonT.destructive for a red delete button or cls=ButtonT.primary for a CTA button
  • +
  • Use Label* functions for forms as much as possible (e.g. LabelInput, LabelRange) which creates and links both the FormLabel and user input appropriately to avoid boiler plate.
  • +
+

Flex Layout Elements (such as DivLAligned and DivFullySpaced) can be used to create layouts concisely

+
+
def TeamCard(name, role, location="Remote"):
+    icons = ("mail", "linkedin", "github")
+    return Card(
+        DivLAligned(
+            DiceBearAvatar(name, h=24, w=24),
+            Div(H3(name), P(role))),
+        footer=DivFullySpaced(
+            DivHStacked(UkIcon("map-pin", height=16), P(location)),
+            DivHStacked(*(UkIconLink(icon, height=16) for icon in icons))))
+
+

Forms are styled and spaced for you without significant additional classes.

+
+
def MonsterForm():
+    relationship = ["Parent",'Sibling', "Friend", "Spouse", "Significant Other", "Relative", "Child", "Other"]
+    return Div(
+        DivCentered(
+            H3("Emergency Contact Form"),
+            P("Please fill out the form completely", cls=TextPresets.muted_sm)),
+        Form(
+            Grid(LabelInput("Name",id='name'),LabelInput("Email",     id='email')),
+            H3("Relationship to patient"),
+            Grid(*[LabelCheckboxX(o) for o in relationship], cols=4, cls='space-y-3'),
+            DivCentered(Button("Submit Form", cls=ButtonT.primary))),
+        cls='space-y-4')
+
+

Text can be styled with markdown automatically with MonsterUI

+
+
render_md("""
+# My Document
+
+> Important note here
+
++ List item with **bold**
++ Another with `code`
+
+```python
+def hello():
+    print("world")
+```
+""")
+
+
'<div><h1 class="uk-h1 text-4xl font-bold mt-12 mb-6">My Document</h1>\n<blockquote class="uk-blockquote pl-4 border-l-4 border-primary italic mb-6">\n<p class="text-lg leading-relaxed mb-6">Important note here</p>\n</blockquote>\n<ul class="uk-list uk-list-bullet space-y-2 mb-6 ml-6 text-lg">\n<li class="leading-relaxed">List item with <strong>bold</strong></li>\n<li class="leading-relaxed">Another with <code class="uk-codespan px-1">code</code></li>\n</ul>\n<pre class="bg-base-200 rounded-lg p-4 mb-6"><code class="language-python uk-codespan px-1 uk-codespan px-1 block overflow-x-auto">def hello():\n    print("world")\n</code></pre>\n</div>'
+
+
+

Or using semantic HTML:

+
+
def SemanticText():
+    return Card(
+        H1("MonsterUI's Semantic Text"),
+        P(
+            Strong("MonsterUI"), " brings the power of semantic HTML to life with ",
+            Em("beautiful styling"), " and ", Mark("zero configuration"), "."),
+        Blockquote(
+            P("Write semantic HTML in pure Python, get modern styling for free."),
+            Cite("MonsterUI Team")),
+        footer=Small("Released February 2025"),)
+
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/ref/concise_guide.html.md b/docs/ref/concise_guide.html.md new file mode 100644 index 0000000000000000000000000000000000000000..8f8ff0a8bc5dacdf6e8089128496495dbd221e14 --- /dev/null +++ b/docs/ref/concise_guide.html.md @@ -0,0 +1,785 @@ +# Concise reference + + + + +## About FastHTML + +``` python +from fasthtml.common import * +``` + +FastHTML is a python library which brings together Starlette, Uvicorn, +HTMX, and fastcore’s `FT` “FastTags” into a library for creating +server-rendered hypermedia applications. The +[`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml) class +itself inherits from `Starlette`, and adds decorator-based routing with +many additions, Beforeware, automatic `FT` to HTML rendering, and much +more. + +Things to remember when writing FastHTML apps: + +- *Not* compatible with FastAPI syntax; FastHTML is for HTML-first apps, + not API services (although it can implement APIs too) +- FastHTML includes support for Pico CSS and the fastlite sqlite + library, although using both are optional; sqlalchemy can be used + directly or via the fastsql library, and any CSS framework can be + used. MonsterUI is a richer FastHTML-first component framework with + similar capabilities to shadcn +- FastHTML is compatible with JS-native web components and any vanilla + JS library, but not with React, Vue, or Svelte +- Use [`serve()`](https://www.fastht.ml/docs/api/core.html#serve) for + running uvicorn (`if __name__ == "__main__"` is not needed since it’s + automatic) +- When a title is needed with a response, use + [`Titled`](https://www.fastht.ml/docs/api/xtend.html#titled); note + that that already wraps children in + [`Container`](https://www.fastht.ml/docs/api/pico.html#container), and + already includes both the meta title as well as the H1 element. + +## Minimal App + +The code examples here use fast.ai style: prefer ternary op, 1-line +docstring, minimize vertical space, etc. (Normally fast.ai style uses +few if any comments, but they’re added here as documentation.) + +A minimal FastHTML app looks something like this: + +``` python +# Meta-package with all key symbols from FastHTML and Starlette. Import it like this at the start of every FastHTML app. +from fasthtml.common import * +# The FastHTML app object and shortcut to `app.route` +app,rt = fast_app() + +# Enums constrain the values accepted for a route parameter +name = str_enum('names', 'Alice', 'Bev', 'Charlie') + +# Passing a path to `rt` is optional. If not passed (recommended), the function name is the route ('/foo') +# Both GET and POST HTTP methods are handled by default +# Type-annotated params are passed as query params (recommended) unless a path param is defined (which it isn't here) +@rt +def foo(nm: name): + # `Title` and `P` here are FastTags: direct m-expression mappings of HTML tags to Python functions with positional and named parameters. All standard HTML tags are included in the common wildcard import. + # When a tuple is returned, this returns concatenated HTML partials. HTMX by default will use a title HTML partial to set the current page name. HEAD tags (e.g. Meta, Link, etc) in the returned tuple are automatically placed in HEAD; everything else is placed in BODY. + # FastHTML will automatically return a complete HTML document with appropriate headers if a normal HTTP request is received. For an HTMX request, however, just the partials are returned. + return Title("FastHTML"), H1("My web app"), P(f"Hello, {name}!") +# By default `serve` runs uvicorn on port 5001. Never write `if __name__ == "__main__"` since `serve` checks it internally. +serve() +``` + +To run this web app: + +``` bash +python main.py # access via localhost:5001 +``` + +## FastTags (aka FT Components or FTs) + +FTs are m-expressions plus simple sugar. Positional params map to +children. Named parameters map to attributes. Aliases must be used for +Python reserved words. + +``` python +tags = Title("FastHTML"), H1("My web app"), P(f"Let's do this!", cls="myclass") +tags +``` + + (title(('FastHTML',),{}), + h1(('My web app',),{}), + p(("Let's do this!",),{'class': 'myclass'})) + +This example shows key aspects of how FTs handle attributes: + +``` python +Label( + "Choose an option", + Select( + Option("one", value="1", selected=True), # True renders just the attribute name + Option("two", value=2, selected=False), # Non-string values are converted to strings. False omits the attribute entirely + cls="selector", id="counter", # 'cls' becomes 'class' + **{'@click':"alert('Clicked');"}, # Dict unpacking for attributes with special chars + ), + _for="counter", # '_for' becomes 'for' (can also use 'fr') +) +``` + +Classes with `__ft__` defined are rendered using that method. + +``` python +class FtTest: + def __ft__(self): return P('test') + +to_xml(FtTest()) +``` + + '

test

\n' + +You can create new FTs by importing the new component from +`fasthtml.components`. If the FT doesn’t exist within that module, +FastHTML will create it. + +``` python +from fasthtml.components import Some_never_before_used_tag + +Some_never_before_used_tag() +``` + +``` html + +``` + +FTs can be combined by defining them as a function. + +``` python +def Hero(title, statement): return Div(H1(title),P(statement), cls="hero") +to_xml(Hero("Hello World", "This is a hero statement")) +``` + + '
\n

Hello World

\n

This is a hero statement

\n
\n' + +When handling a response, FastHTML will automatically render FTs using +the `to_xml` function. + +``` python +to_xml(tags) +``` + + 'FastHTML\n

My web app

\n

Let's do this!

\n' + +## JS + +The [`Script`](https://www.fastht.ml/docs/api/xtend.html#script) +function allows you to include JavaScript. You can use Python to +generate parts of your JS or JSON like this: + +``` python +# In future snippets this import will not be shown, but is required +from fasthtml.common import * +app,rt = fast_app(hdrs=[Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js")]) +# `index` is a special function name which maps to the `/` route. +@rt +def index(): + data = {'somedata':'fill me in…'} + # `Titled` returns a title tag and an h1 tag with the 1st param, with remaining params as children in a `Main` parent. + return Titled("Chart Demo", Div(id="myDiv"), Script(f"var data = {data}; Plotly.newPlot('myDiv', data);")) +# In future snippets `serve() will not be shown, but is required +serve() +``` + +Prefer Python whenever possible over JS. Never use React or shadcn. + +## fast_app hdrs + +``` python +# In future snippets we'll skip showing the `fast_app` call if it has no params +app, rt = fast_app( + pico=False, # The Pico CSS framework is included by default, so pass `False` to disable it if needed. No other CSS frameworks are included. + # These are added to the `head` part of the page for non-HTMX requests. + hdrs=( + Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'), + Link(rel='stylesheet', href='assets/sakura.css', type='text/css'), + Style("p {color: red;}"), + # `MarkdownJS` and `HighlightJS` are available via concise functions + MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), + # by default, all standard static extensions are served statically from the web app dir, + # which can be modified using e.g `static_path='public'` + ) +) + +@rt +def index(req): return Titled("Markdown rendering example", + # This will be client-side rendered to HTML with highlight-js + Div("*hi* there",cls="marked"), + # This will be syntax highlighted + Pre(Code("def foo(): pass"))) +``` + +## Responses + +Routes can return various types: + +1. FastTags or tuples of FastTags (automatically rendered to HTML) +2. Standard Starlette responses (used directly) +3. JSON-serializable types (returned as JSON in a plain text response) + +``` python +@rt("/{fname:path}.{ext:static}") +async def serve_static_file(fname:str, ext:str): return FileResponse(f'public/{fname}.{ext}') + +app, rt = fast_app(hdrs=(MarkdownJS(), HighlightJS(langs=['python', 'javascript']))) +@rt +def index(): + return Titled("Example", + Div("*markdown* here", cls="marked"), + Pre(Code("def foo(): pass"))) +``` + +Route functions can be used in attributes like `href` or `action` and +will be converted to paths. Use `.to()` to generate paths with query +parameters. + +``` python +@rt +def profile(email:str): return fill_form(profile_form, profiles[email]) + +profile_form = Form(action=profile)( + Label("Email", Input(name="email")), + Button("Save", type="submit") +) + +user_profile_path = profile.to(email="user@example.com") # '/profile?email=user%40example.com' +``` + +``` python +from dataclasses import dataclass + +app,rt = fast_app() +``` + +When a route handler function is used as a fasttag attribute (such as +`href`, `hx_get`, or `action`) it is converted to that route’s path. +[`fill_form`](https://www.fastht.ml/docs/api/components.html#fill_form) +is used to copy an object’s matching attrs into matching-name form +fields. + +``` python +@dataclass +class Profile: email:str; phone:str; age:int +email = 'john@example.com' +profiles = {email: Profile(email=email, phone='123456789', age=5)} +@rt +def profile(email:str): return fill_form(profile_form, profiles[email]) + +profile_form = Form(method="post", action=profile)( + Fieldset( + Label('Email', Input(name="email")), + Label("Phone", Input(name="phone")), + Label("Age", Input(name="age"))), + Button("Save", type="submit")) +``` + +## Testing + +We can use `TestClient` for testing. + +``` python +from starlette.testclient import TestClient +``` + +``` python +path = "/profile?email=john@example.com" +client = TestClient(app) +htmx_req = {'HX-Request':'1'} +print(client.get(path, headers=htmx_req).text) +``` + +
+ +## Form Handling and Data Binding + +When a dataclass, namedtuple, etc. is used as a type annotation, the +form body will be unpacked into matching attribute names automatically. + +``` python +@rt +def edit_profile(profile: Profile): + profiles[email]=profile + return RedirectResponse(url=path) + +new_data = dict(email='john@example.com', phone='7654321', age=25) +print(client.post("/edit_profile", data=new_data, headers=htmx_req).text) +``` + +
+ +## fasttag Rendering Rules + +The general rules for rendering children inside tuples or fasttag +children are: - `__ft__` method will be called (for default components +like `P`, `H2`, etc. or if you define your own components) - If you pass +a string, it will be escaped - On other python objects, `str()` will be +called + +If you want to include plain HTML tags directly into e.g. a `Div()` they +will get escaped by default (as a security measure to avoid code +injections). This can be avoided by using `Safe(...)`, e.g to show a +data frame use `Div(NotStr(df.to_html()))`. + +## Exceptions + +FastHTML allows customization of exception handlers. + +``` python +def not_found(req, exc): return Titled("404: I don't exist!") +exception_handlers = {404: not_found} +app, rt = fast_app(exception_handlers=exception_handlers) +``` + +## Cookies + +We can set cookies using the +[`cookie()`](https://www.fastht.ml/docs/api/core.html#cookie) function. + +``` python +@rt +def setcook(): return P(f'Set'), cookie('mycookie', 'foobar') +print(client.get('/setcook', headers=htmx_req).text) +``` + +

Set

+ +``` python +@rt +def getcook(mycookie:str): return f'Got {mycookie}' +# If handlers return text instead of FTs, then a plaintext response is automatically created +print(client.get('/getcook').text) +``` + + Got foobar + +FastHTML provide access to Starlette’s request object automatically +using special `request` parameter name (or any prefix of that name). + +``` python +@rt +def headers(req): return req.headers['host'] +``` + +## Request and Session Objects + +FastHTML provides access to Starlette’s session middleware automatically +using the special `session` parameter name (or any prefix of that name). + +``` python +@rt +def profile(req, sess, user_id: int=None): + ip = req.client.host + sess['last_visit'] = datetime.now().isoformat() + visits = sess.setdefault('visit_count', 0) + 1 + sess['visit_count'] = visits + user = get_user(user_id or sess.get('user_id')) + return Titled(f"Profile: {user.name}", + P(f"Visits: {visits}"), + P(f"IP: {ip}"), + Button("Logout", hx_post=logout)) +``` + +Handler functions can return the +[`HtmxResponseHeaders`](https://www.fastht.ml/docs/api/core.html#htmxresponseheaders) +object to set HTMX-specific response headers. + +``` python +@rt +def htmlredirect(app): return HtmxResponseHeaders(location="http://example.org") +``` + +## APIRouter + +[`APIRouter`](https://www.fastht.ml/docs/api/core.html#apirouter) lets +you organize routes across multiple files in a FastHTML app. + +``` python +# products.py +ar = APIRouter() + +@ar +def details(pid: int): return f"Here are the product details for ID: {pid}" + +@ar +def all_products(req): + return Div( + Div( + Button("Details",hx_get=details.to(pid=42),hx_target="#products_list",hx_swap="outerHTML",), + ), id="products_list") +``` + +``` python +# main.py +from products import ar,all_products + +app, rt = fast_app() +ar.to_app(app) + +@rt +def index(): + return Div( + "Products", + hx_get=all_products, hx_swap="outerHTML") +``` + +## Toasts + +Toasts can be of four types: + +- info +- success +- warning +- error + +Toasts require the use of the `setup_toasts()` function, plus every +handler needs: + +- The session argument +- Must return FT components + +``` python +setup_toasts(app) + +@rt +def toasting(session): + add_toast(session, f"cooked", "info") + add_toast(session, f"ready", "success") + return Titled("toaster") +``` + +`setup_toasts(duration)` allows you to specify how long a toast will be +visible before disappearing.10 seconds. + +Authentication and authorization are handled with Beforeware, which +functions that run before the route handler is called. + +## Auth + +``` python +def user_auth_before(req, sess): + # `auth` key in the request scope is automatically provided to any handler which requests it and can not be injected + auth = req.scope['auth'] = sess.get('auth', None) + if not auth: return RedirectResponse('/login', status_code=303) + +beforeware = Beforeware( + user_auth_before, + skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/'] +) + +app, rt = fast_app(before=beforeware) +``` + +## Server-Side Events (SSE) + +FastHTML supports the HTMX SSE extension. + +``` python +import random +hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.3/sse.js"),) +app,rt = fast_app(hdrs=hdrs) + +@rt +def index(): return Div(hx_ext="sse", sse_connect="/numstream", hx_swap="beforeend show:bottom", sse_swap="message") + +# `signal_shutdown()` gets an event that is set on shutdown +shutdown_event = signal_shutdown() + +async def number_generator(): + while not shutdown_event.is_set(): + data = Article(random.randint(1, 100)) + yield sse_message(data) + +@rt +async def numstream(): return EventStream(number_generator()) +``` + +## Websockets + +FastHTML provides useful tools for HTMX’s websockets extension. + +``` python +# These HTMX extensions are available through `exts`: +# head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer +app, rt = fast_app(exts='ws') + +def mk_inp(): return Input(id='msg', autofocus=True) + +@rt +async def index(request): + # `ws_send` tells HTMX to send a message to the nearest websocket based on the trigger for the form element + cts = Div( + Div(id='notifications'), + Form(mk_inp(), id='form', ws_send=True), + hx_ext='ws', ws_connect='/ws') + return Titled('Websocket Test', cts) + +async def on_connect(send): await send(Div('Hello, you have connected', id="notifications")) +async def on_disconnect(ws): print('Disconnected!') + +@app.ws('/ws', conn=on_connect, disconn=on_disconnect) +async def ws(msg:str, send): + # websocket hander returns/sends are treated as OOB swaps + await send(Div('Hello ' + msg, id="notifications")) + return Div('Goodbye ' + msg, id="notifications"), mk_inp() +``` + +Sample chatbot that uses FastHTML’s +[`setup_ws`](https://www.fastht.ml/docs/api/core.html#setup_ws) +function: + +``` py +app = FastHTML(exts='ws') +rt = app.route +msgs = [] + +@rt('/') +def home(): + return Div(hx_ext='ws', ws_connect='/ws')( + Div(Ul(*[Li(m) for m in msgs], id='msg-list')), + Form(Input(id='msg'), id='form', ws_send=True) + ) + +async def ws(msg:str): + msgs.append(msg) + await send(Ul(*[Li(m) for m in msgs], id='msg-list')) + +send = setup_ws(app, ws) +``` + +### Single File Uploads + +[`Form`](https://www.fastht.ml/docs/api/xtend.html#form) defaults to +“multipart/form-data”. A Starlette UploadFile is passed to the handler. + +``` python +upload_dir = Path("filez") + +@rt +def index(): + return ( + Form(hx_post=upload, hx_target="#result")( + Input(type="file", name="file"), + Button("Upload", type="submit")), + Div(id="result") + ) + +# Use `async` handlers where IO is used to avoid blocking other clients +@rt +async def upload(file: UploadFile): + filebuffer = await file.read() + (upload_dir / file.filename).write_bytes(filebuffer) + return P('Size: ', file.size) +``` + +For multi-file, use `Input(..., multiple=True)`, and a type annotation +of `list[UploadFile]` in the handler. + +## Fastlite + +Fastlite and the MiniDataAPI specification it’s built on are a +CRUD-oriented API for working with SQLite. APSW and apswutils is used to +connect to SQLite, optimized for speed and clean error handling. + +``` python +from fastlite import * +``` + +``` python +db = database(':memory:') # or database('data/app.db') +``` + +Tables are normally constructed with classes, field types are specified +as type hints. + +``` python +class Book: isbn: str; title: str; pages: int; userid: int +# The transform arg instructs fastlite to change the db schema when fields change. +# Create only creates a table if the table doesn't exist. +books = db.create(Book, pk='isbn', transform=True) + +class User: id: int; name: str; active: bool = True +# If no pk is provided, id is used as the primary key. +users = db.create(User, transform=True) +users +``` + +
+ +### Fastlite CRUD operations + +Every operation in fastlite returns a full superset of dataclass +functionality. + +``` python +user = users.insert(name='Alex',active=False) +user +``` + + User(id=1, name='Alex', active=0) + +``` python +# List all records +users() +``` + + [User(id=1, name='Alex', active=0)] + +``` python +# Limit, offset, and order results: +users(order_by='name', limit=2, offset=1) + +# Filter on the results +users(where="name='Alex'") + +# Placeholder for avoiding injection attacks +users("name=?", ('Alex',)) + +# A single record by pk +users[user.id] +``` + + User(id=1, name='Alex', active=0) + +Test if a record exists by using `in` keyword on primary key: + +``` python +1 in users +``` + + True + +Updates (which take a dict or a typed object) return the updated record. + +``` python +user.name='Lauren' +user.active=True +users.update(user) +``` + + User(id=1, name='Lauren', active=1) + +`.xtra()` to automatically constrain queries, updates, and inserts from +there on: + +``` python +users.xtra(active=True) +users() +``` + + [User(id=1, name='Lauren', active=1)] + +Deleting by pk: + +``` python +users.delete(user.id) +``` + +
+ +NotFoundError is raised by pk `[]`, updates, and deletes. + +``` python +try: users['Amy'] +except NotFoundError: print('User not found') +``` + + User not found + +## MonsterUI + +MonsterUI is a shadcn-like component library for FastHTML. It adds the +Tailwind-based libraries FrankenUI and DaisyUI to FastHTML, as well as +Python’s mistletoe for Markdown, HighlightJS for code highlighting, and +Katex for latex support, following semantic HTML patterns when possible. +It is recommended for when you wish to go beyond the basics provided by +FastHTML’s built-in pico support. + +A minimal app: + +``` python +from fasthtml.common import * +from monsterui.all import * + +app, rt = fast_app(hdrs=Theme.blue.headers(highlightjs=True)) # Use MonsterUI blue theme and highlight code in markdown + +@rt +def index(): + socials = (('github','https://github.com/AnswerDotAI/MonsterUI'),) + return Titled("App", + Card( + P("App", cls=TextPresets.muted_sm), + # LabelInput, DivLAigned, and UkIconLink are non-semantic MonsterUI FT Components, + LabelInput('Email', type='email', required=True), + footer=DivLAligned(*[UkIconLink(icon,href=url) for icon,url in socials]))) +``` + +MonsterUI recommendations: + +- Use defaults as much as possible, for example + [`Container`](https://www.fastht.ml/docs/api/pico.html#container) in + monsterui already has defaults for margins +- Use `*T` for button styling consistency, for example + `cls=ButtonT.destructive` for a red delete button or + `cls=ButtonT.primary` for a CTA button +- Use `Label*` functions for forms as much as possible + (e.g. `LabelInput`, `LabelRange`) which creates and links both the + `FormLabel` and user input appropriately to avoid boiler plate. + +Flex Layout Elements (such as `DivLAligned` and `DivFullySpaced`) can be +used to create layouts concisely + +``` python +def TeamCard(name, role, location="Remote"): + icons = ("mail", "linkedin", "github") + return Card( + DivLAligned( + DiceBearAvatar(name, h=24, w=24), + Div(H3(name), P(role))), + footer=DivFullySpaced( + DivHStacked(UkIcon("map-pin", height=16), P(location)), + DivHStacked(*(UkIconLink(icon, height=16) for icon in icons)))) +``` + +Forms are styled and spaced for you without significant additional +classes. + +``` python +def MonsterForm(): + relationship = ["Parent",'Sibling', "Friend", "Spouse", "Significant Other", "Relative", "Child", "Other"] + return Div( + DivCentered( + H3("Emergency Contact Form"), + P("Please fill out the form completely", cls=TextPresets.muted_sm)), + Form( + Grid(LabelInput("Name",id='name'),LabelInput("Email", id='email')), + H3("Relationship to patient"), + Grid(*[LabelCheckboxX(o) for o in relationship], cols=4, cls='space-y-3'), + DivCentered(Button("Submit Form", cls=ButtonT.primary))), + cls='space-y-4') +``` + +Text can be styled with markdown automatically with MonsterUI + +```` python +render_md(""" +# My Document + +> Important note here + ++ List item with **bold** ++ Another with `code` + +```python +def hello(): + print("world") +``` +""") +```` + + '

My Document

\n
\n

Important note here

\n
\n
    \n
  • List item with bold
  • \n
  • Another with code
  • \n
\n
def hello():\n    print("world")\n
\n
' + +Or using semantic HTML: + +``` python +def SemanticText(): + return Card( + H1("MonsterUI's Semantic Text"), + P( + Strong("MonsterUI"), " brings the power of semantic HTML to life with ", + Em("beautiful styling"), " and ", Mark("zero configuration"), "."), + Blockquote( + P("Write semantic HTML in pure Python, get modern styling for free."), + Cite("MonsterUI Team")), + footer=Small("Released February 2025"),) +``` diff --git a/docs/ref/defining_xt_component.html b/docs/ref/defining_xt_component.html new file mode 100644 index 0000000000000000000000000000000000000000..aa16c1eedbf939684821aaa58ef8ab24a11174e0 --- /dev/null +++ b/docs/ref/defining_xt_component.html @@ -0,0 +1,1011 @@ + + + + + + + + + +Custom Components – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Custom Components

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

The majority of the time the default ft components are all you need (for example Div, P, H1, etc.).

+
+
+
+ +
+
+Pre-requisite Knowledge +
+
+
+

If you don’t know what an ft component is, you should read the explaining ft components explainer first.

+
+
+

However, there are many situations where you need a custom ft component that creates a unique HTML tag (for example <zero-md></zero-md>). There are many options in FastHTML to do this, and this section will walk through them. Generally you want to use the highest level option that fits your needs.

+
+
+
+ +
+
+Real-world example +
+
+
+

This external tutorial walks through a practical situation where you may want to create a custom HTML tag using a custom ft component. Seeing a real-world example is a good way to understand why the contents of this guide is useful.

+
+
+
+

NotStr

+

The first way is to use the NotStr class to use an HTML tag as a string. It works as a one-off but quickly becomes harder to work with as complexity grows. However we can see that you can genenrate the same xml using NotStr as the out-of-the-box components.

+
+
from fasthtml.common import NotStr,Div, to_xml
+
+
+
div_NotStr = NotStr('<div></div>') 
+print(div_NotStr)
+
+
<div></div>
+
+
+
+
+

Automatic Creation

+

The next (and better) approach is to let FastHTML generate the component function for you. As you can see in our assert this creates a function that creates the HTML just as we wanted. This works even though there is not a Some_never_before_used_tag function in the fasthtml.components source code (you can verify this yourself by looking at the source code).

+
+
+
+ +
+
+Tip +
+
+
+

Typically these tags are needed because a CSS or Javascript library created a new XML tag that isn’t default HTML. For example the zero-md javascript library looks for a <zero-md></zero-md> tag to know what to run its javascript code on. Most CSS libraries work by creating styling based on the class attribute, but they can also apply styling to an arbitrary HTML tag that they made up.

+
+
+
+
from fasthtml.components import Some_never_before_used_tag
+
+Some_never_before_used_tag()
+
+
<some-never-before-used-tag></some-never-before-used-tag>
+
+
+
+
+

Manual Creation

+

The automatic creation isn’t magic. It’s just calling a python function __getattr__ and you can call it yourself to get the same result.

+
+
import fasthtml
+
+auto_called = fasthtml.components.Some_never_before_used_tag()
+manual_called = fasthtml.components.__getattr__('Some_never_before_used_tag')()
+
+# Proving they generate the same xml
+assert to_xml(auto_called) == to_xml(manual_called)
+
+

Knowing that, we know that it’s possible to create a different function that has different behavior than FastHTMLs default behavior by modifying how the ___getattr__ function creates the components! It’s only a few lines of code and reading that what it does is a great way to understand components more deeply.

+
+
+
+ +
+
+Tip +
+
+
+

Dunder methods and functions are special functions that have double underscores at the beginning and end of their name. They are called at specific times in python so you can use them to cause customized behavior that makes sense for your specific use case. They can appear magical if you don’t know how python works, but they are extremely commonly used to modify python’s default behavior (__init__ is probably the most common one).

+

In a module __getattr__ is called to get an attribute. In fasthtml.components, this is defined to create components automatically for you.

+
+
+

For example if you want a component that creates <path></path> that doesn’t conflict names with pathlib.Path you can do that. FastHTML automatically creates new components with a 1:1 mapping and a consistent name, which is almost always what you want. But in some cases you may want to customize that and you can use the ft_hx function to do that differently than the default.

+
+
from fasthtml.common import ft_hx
+
+def ft_path(*c, target_id=None, **kwargs): 
+    return ft_hx('path', *c, target_id=target_id, **kwargs)
+
+ft_path()
+
+
<path></path>
+
+
+

We can add any behavior in that function that we need to, so let’s go through some progressively complex examples that you may need in some of your projects.

+
+

Underscores in tags

+

Now that we understand how FastHTML generates components, we can create our own in all kinds of ways. For example, maybe we need a weird HTML tag that uses underscores. FastHTML replaces _ with - in tags because underscores in tags are highly unusual and rarely what you want, though it does come up rarely.

+
+
def tag_with_underscores(*c, target_id=None, **kwargs): 
+    return ft_hx('tag_with_underscores', *c, target_id=target_id, **kwargs)
+
+tag_with_underscores()
+
+
<tag_with_underscores></tag_with_underscores>
+
+
+
+
+

Symbols (ie @) in tags

+

Sometimes you may need to use a tag that uses characters that are not allowed in function names in python (again, very unusual).

+
+
def tag_with_AtSymbol(*c, target_id=None, **kwargs): 
+    return ft_hx('tag-with-@symbol', *c, target_id=target_id, **kwargs)
+
+tag_with_AtSymbol()
+
+
<tag-with-@symbol></tag-with-@symbol>
+
+
+
+
+

Symbols (ie @) in tag attributes

+

It also may be that an argument in an HTML tag uses characters that can’t be used in python arguments. To handle these you can define those args using a dictionary.

+
+
Div(normal_arg='normal stuff',**{'notNormal:arg:with_varing@symbols!':'123'})
+
+
<div normal-arg="normal stuff" notnormal:arg:with_varing@symbols!="123"></div>
+
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/ref/defining_xt_component.md b/docs/ref/defining_xt_component.md new file mode 100644 index 0000000000000000000000000000000000000000..254ad72d6d791b040da6fec6d43f9df67c550512 --- /dev/null +++ b/docs/ref/defining_xt_component.md @@ -0,0 +1,203 @@ +# Custom Components + + + + +The majority of the time the default [ft +components](../explains/explaining_xt_components.html) are all you need +(for example `Div`, `P`, `H1`, etc.). + +
+ +> **Pre-requisite Knowledge** +> +> If you don’t know what an ft component is, you should read [the +> explaining ft components explainer +> first](../explains/explaining_xt_components.html). + +
+ +However, there are many situations where you need a custom ft component +that creates a unique HTML tag (for example ``). +There are many options in FastHTML to do this, and this section will +walk through them. Generally you want to use the highest level option +that fits your needs. + +
+ +> **Real-world example** +> +> [This external +> tutorial](https://isaac-flath.github.io/website/posts/boots/FasthtmlTutorial.html) +> walks through a practical situation where you may want to create a +> custom HTML tag using a custom ft component. Seeing a real-world +> example is a good way to understand why the contents of this guide is +> useful. + +
+ +## NotStr + +The first way is to use the `NotStr` class to use an HTML tag as a +string. It works as a one-off but quickly becomes harder to work with as +complexity grows. However we can see that you can genenrate the same xml +using `NotStr` as the out-of-the-box components. + +``` python +from fasthtml.common import NotStr,Div, to_xml +``` + +``` python +div_NotStr = NotStr('
') +print(div_NotStr) +``` + +
+ +## Automatic Creation + +The next (and better) approach is to let FastHTML generate the component +function for you. As you can see in our `assert` this creates a function +that creates the HTML just as we wanted. This works even though there is +not a `Some_never_before_used_tag` function in the `fasthtml.components` +source code (you can verify this yourself by looking at the source +code). + +
+ +> **Tip** +> +> Typically these tags are needed because a CSS or Javascript library +> created a new XML tag that isn’t default HTML. For example the +> `zero-md` javascript library looks for a `` tag to +> know what to run its javascript code on. Most CSS libraries work by +> creating styling based on the `class` attribute, but they can also +> apply styling to an arbitrary HTML tag that they made up. + +
+ +``` python +from fasthtml.components import Some_never_before_used_tag + +Some_never_before_used_tag() +``` + +``` html + +``` + +## Manual Creation + +The automatic creation isn’t magic. It’s just calling a python function +[`__getattr__`](https://www.fastht.ml/docs/api/components.html#__getattr__) +and you can call it yourself to get the same result. + +``` python +import fasthtml + +auto_called = fasthtml.components.Some_never_before_used_tag() +manual_called = fasthtml.components.__getattr__('Some_never_before_used_tag')() + +# Proving they generate the same xml +assert to_xml(auto_called) == to_xml(manual_called) +``` + +Knowing that, we know that it’s possible to create a different function +that has different behavior than FastHTMLs default behavior by modifying +how the `___getattr__` function creates the components! It’s only a few +lines of code and reading that what it does is a great way to understand +components more deeply. + +
+ +> **Tip** +> +> Dunder methods and functions are special functions that have double +> underscores at the beginning and end of their name. They are called at +> specific times in python so you can use them to cause customized +> behavior that makes sense for your specific use case. They can appear +> magical if you don’t know how python works, but they are extremely +> commonly used to modify python’s default behavior (`__init__` is +> probably the most common one). +> +> In a module +> [`__getattr__`](https://www.fastht.ml/docs/api/components.html#__getattr__) +> is called to get an attribute. In `fasthtml.components`, this is +> defined to create components automatically for you. + +
+ +For example if you want a component that creates `` that +doesn’t conflict names with +[`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) +you can do that. FastHTML automatically creates new components with a +1:1 mapping and a consistent name, which is almost always what you want. +But in some cases you may want to customize that and you can use the +[`ft_hx`](https://www.fastht.ml/docs/api/components.html#ft_hx) function +to do that differently than the default. + +``` python +from fasthtml.common import ft_hx + +def ft_path(*c, target_id=None, **kwargs): + return ft_hx('path', *c, target_id=target_id, **kwargs) + +ft_path() +``` + +``` html + +``` + +We can add any behavior in that function that we need to, so let’s go +through some progressively complex examples that you may need in some of +your projects. + +### Underscores in tags + +Now that we understand how FastHTML generates components, we can create +our own in all kinds of ways. For example, maybe we need a weird HTML +tag that uses underscores. FastHTML replaces `_` with `-` in tags +because underscores in tags are highly unusual and rarely what you want, +though it does come up rarely. + +``` python +def tag_with_underscores(*c, target_id=None, **kwargs): + return ft_hx('tag_with_underscores', *c, target_id=target_id, **kwargs) + +tag_with_underscores() +``` + +``` html + +``` + +### Symbols (ie @) in tags + +Sometimes you may need to use a tag that uses characters that are not +allowed in function names in python (again, very unusual). + +``` python +def tag_with_AtSymbol(*c, target_id=None, **kwargs): + return ft_hx('tag-with-@symbol', *c, target_id=target_id, **kwargs) + +tag_with_AtSymbol() +``` + +``` html + +``` + +### Symbols (ie @) in tag attributes + +It also may be that an argument in an HTML tag uses characters that +can’t be used in python arguments. To handle these you can define those +args using a dictionary. + +``` python +Div(normal_arg='normal stuff',**{'notNormal:arg:with_varing@symbols!':'123'}) +``` + +``` html +
+``` diff --git a/docs/ref/handlers.html b/docs/ref/handlers.html new file mode 100644 index 0000000000000000000000000000000000000000..9cac28881e3261990a628f09144f4dd4af53dd76 --- /dev/null +++ b/docs/ref/handlers.html @@ -0,0 +1,1712 @@ + + + + + + + + + + +Handling handlers – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Handling handlers

+
+ +
+
+ How handlers work in FastHTML +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from fasthtml.common import *
+from collections import namedtuple
+from typing import TypedDict
+from datetime import datetime
+import json,time
+
+
+
app = FastHTML()
+
+

The FastHTML class is the main application class for FastHTML apps.

+
+
rt = app.route
+
+

app.route is used to register route handlers. It is a decorator, which means we place it before a function that is used as a handler. Because it’s used frequently in most FastHTML applications, we often alias it as rt, as we do here.

+
+

Basic Route Handling

+
+
@rt("/hi")
+def get(): return 'Hi there'
+
+

Handler functions can return strings directly. These strings are sent as the response body to the client.

+
+
cli = Client(app)
+
+

Client is a test client for FastHTML applications. It allows you to simulate requests to your app without running a server.

+
+
cli.get('/hi').text
+
+
'Hi there'
+
+
+

The get method on a Client instance simulates GET requests to the app. It returns a response object that has a .text attribute, which you can use to access the body of the response. It calls httpx.get internally – all httpx HTTP verbs are supported.

+
+
@rt("/hi")
+def post(): return 'Postal'
+cli.post('/hi').text
+
+
'Postal'
+
+
+

Handler functions can be defined for different HTTP methods on the same route. Here, we define a post handler for the /hi route. The Client instance can simulate different HTTP methods, including POST requests.

+
+
+

Request and Response Objects

+
+
@app.get("/hostie")
+def show_host(req): return req.headers['host']
+cli.get('/hostie').text
+
+
'testserver'
+
+
+

Handler functions can accept a req (or request) parameter, which represents the incoming request. This object contains information about the request, including headers. In this example, we return the host header from the request. The test client uses ‘testserver’ as the default host.

+

In this example, we use @app.get("/hostie") instead of @rt("/hostie"). The @app.get() decorator explicitly specifies the HTTP method (GET) for the route, while @rt() by default handles both GET and POST requests.

+
+
@rt
+def yoyo(): return 'a yoyo'
+cli.post('/yoyo').text
+
+
'a yoyo'
+
+
+

If the @rt decorator is used without arguments, it uses the function name as the route path. Here, the yoyo function becomes the handler for the /yoyo route. This handler responds to GET and POST methods, since a specific method wasn’t provided.

+
+
@rt
+def ft1(): return Html(Div('Text.'))
+print(cli.get('/ft1').text)
+
+
 <!doctype html>
+ <html>
+   <div>Text.</div>
+ </html>
+
+
+
+

Handler functions can return FT objects, which are automatically converted to HTML strings. The FT class can take other FT components as arguments, such as Div. This allows for easy composition of HTML elements in your responses.

+
+
@app.get
+def autopost(): return Html(Div('Text.', hx_post=yoyo.to()))
+print(cli.get('/autopost').text)
+
+
 <!doctype html>
+ <html>
+   <div hx-post="/yoyo">Text.</div>
+ </html>
+
+
+
+

The rt decorator modifies the yoyo function by adding a to() method. This method returns the route path associated with the handler. It’s a convenient way to reference the route of a handler function dynamically.

+

In the example, yoyo.to() is used as the value for hx_post. This means when the div is clicked, it will trigger an HTMX POST request to the route of the yoyo handler. This approach allows for flexible, DRY code by avoiding hardcoded route strings and automatically updating if the route changes.

+

This pattern is particularly useful in larger applications where routes might change, or when building reusable components that need to reference their own routes dynamically.

+
+
@app.get
+def autoget(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b'))))
+print(cli.get('/autoget').text)
+
+
 <!doctype html>
+ <html>
+   <body>
+     <div hx-post="/hostie?a=b" class="px-2">Text.</div>
+   </body>
+ </html>
+
+
+
+

The rt() method of handler functions can also accept parameters. When called with parameters, it returns the route path with a query string appended. In this example, show_host.to(a='b') generates the path /hostie?a=b.

+

The Body component is used here to demonstrate nesting of FT components. Div is nested inside Body, showcasing how you can create more complex HTML structures.

+

The cls parameter is used to add a CSS class to the Div. This translates to the class attribute in the rendered HTML. (class can’t be used as a parameter name directly in Python since it’s a reserved word.)

+
+
@rt('/ft2')
+def get(): return Title('Foo'),H1('bar')
+print(cli.get('/ft2').text)
+
+
 <!doctype html>
+ <html>
+   <head>
+     <title>Foo</title>
+     <meta charset="utf-8">
+     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
+<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script><script>
+    function sendmsg() {
+        window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
+    }
+    window.onload = function() {
+        sendmsg();
+        document.body.addEventListener('htmx:afterSettle',    sendmsg);
+        document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
+    };</script>   </head>
+   <body>
+     <h1>bar</h1>
+   </body>
+ </html>
+
+
+
+

Handler functions can return multiple FT objects as a tuple. The first item is treated as the Title, and the rest are added to the Body. When the request is not an HTMX request, FastHTML automatically adds necessary HTML boilerplate, including default head content with required scripts.

+

When using app.route (or rt), if the function name matches an HTTP verb (e.g., get, post, put, delete), that HTTP method is automatically used for the route. In this case, a path must be explicitly provided as an argument to the decorator.

+
+
hxhdr = {'headers':{'hx-request':"1"}}
+print(cli.get('/ft2', **hxhdr).text)
+
+
 <title>Foo</title>
+ <h1>bar</h1>
+
+
+
+

For HTMX requests (indicated by the hx-request header), FastHTML returns only the specified components without the full HTML structure. This allows for efficient partial page updates in HTMX applications.

+
+
@rt('/ft3')
+def get(): return H1('bar')
+print(cli.get('/ft3', **hxhdr).text)
+
+
 <h1>bar</h1>
+
+
+
+

When a handler function returns a single FT object for an HTMX request, it’s rendered as a single HTML partial.

+
+
@rt('/ft4')
+def get(): return Html(Head(Title('hi')), Body(P('there')))
+
+print(cli.get('/ft4').text)
+
+
 <!doctype html>
+ <html>
+   <head>
+     <title>hi</title>
+   </head>
+   <body>
+     <p>there</p>
+   </body>
+ </html>
+
+
+
+

Handler functions can return a complete Html structure, including Head and Body components. When a full HTML structure is returned, FastHTML doesn’t add any additional boilerplate. This gives you full control over the HTML output when needed.

+
+
@rt
+def index(): return "welcome!"
+print(cli.get('/').text)
+
+
welcome!
+
+
+

The index function is a special handler in FastHTML. When defined without arguments to the @rt decorator, it automatically becomes the handler for the root path ('/'). This is a convenient way to define the main page or entry point of your application.

+
+
+

Path and Query Parameters

+
+
@rt('/user/{nm}', name='gday')
+def get(nm:str=''): return f"Good day to you, {nm}!"
+cli.get('/user/Alexis').text
+
+
'Good day to you, Alexis!'
+
+
+

Handler functions can use path parameters, defined using curly braces in the route – this is implemented by Starlette directly, so all Starlette path parameters can be used. These parameters are passed as arguments to the function.

+

The name parameter in the decorator allows you to give the route a name, which can be used for URL generation.

+

In this example, {nm} in the route becomes the nm parameter in the function. The function uses this parameter to create a personalized greeting.

+
+
@app.get
+def autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))
+print(cli.get('/autolink').text)
+
+
 <!doctype html>
+ <html>
+   <div href="/user/Alexis">Text.</div>
+ </html>
+
+
+
+

The uri function is used to generate URLs for named routes. It takes the route name as its first argument, followed by any path or query parameters needed for that route.

+

In this example, uri('gday', nm='Alexis') generates the URL for the route named ‘gday’ (which we defined earlier as ‘/user/{nm}’), with ‘Alexis’ as the value for the ‘nm’ parameter.

+

The link parameter in FT components sets the href attribute of the rendered HTML element. By using uri(), we can dynamically generate correct URLs even if the underlying route structure changes.

+

This approach promotes maintainable code by centralizing route definitions and avoiding hardcoded URLs throughout the application.

+
+
@rt('/link')
+def get(req): return f"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}"
+
+cli.get('/link').text
+
+
'http://testserver/user/Alexis; http://testserver/hostie'
+
+
+

The url_for method of the request object can be used to generate URLs for named routes. It takes the route name as its first argument, followed by any path parameters needed for that route.

+

In this example, req.url_for('gday', nm='Alexis') generates the full URL for the route named ‘gday’, including the scheme and host. Similarly, req.url_for('show_host') generates the URL for the ‘show_host’ route.

+

This method is particularly useful when you need to generate absolute URLs, such as for email links or API responses. It ensures that the correct host and scheme are included, even if the application is accessed through different domains or protocols.

+
+
app.url_path_for('gday', nm='Jeremy')
+
+
'/user/Jeremy'
+
+
+

The url_path_for method of the application can be used to generate URL paths for named routes. Unlike url_for, it returns only the path component of the URL, without the scheme or host.

+

In this example, app.url_path_for('gday', nm='Jeremy') generates the path ‘/user/Jeremy’ for the route named ‘gday’.

+

This method is useful when you need relative URLs or just the path component, such as for internal links or when constructing URLs in a host-agnostic manner.

+
+
@rt('/oops')
+def get(nope): return nope
+r = cli.get('/oops?nope=1')
+print(r)
+r.text
+
+
<Response [200 OK]>
+
+
+
/Users/iflath/git/AnswerDotAI/fasthtml/build/__editable__.python_fasthtml-0.12.1-py3-none-any/fasthtml/core.py:188: UserWarning: `nope has no type annotation and is not a recognised special name, so is ignored.
+  if arg!='resp': warn(f"`{arg} has no type annotation and is not a recognised special name, so is ignored.")
+
+
+
''
+
+
+

Handler functions can include parameters, but they must be type-annotated or have special names (like req) to be recognized. In this example, the nope parameter is not annotated, so it’s ignored, resulting in a warning.

+

When a parameter is ignored, it doesn’t receive the value from the query string. This can lead to unexpected behavior, as the function attempts to return nope, which is undefined.

+

The cli.get('/oops?nope=1') call succeeds with a 200 OK status because the handler doesn’t raise an exception, but it returns an empty response, rather than the intended value.

+

To fix this, you should either add a type annotation to the parameter (e.g., def get(nope: str):) or use a recognized special name like req.

+
+
@rt('/html/{idx}')
+def get(idx:int): return Body(H4(f'Next is {idx+1}.'))
+print(cli.get('/html/1', **hxhdr).text)
+
+
 <body>
+   <h4>Next is 2.</h4>
+ </body>
+
+
+
+

Path parameters can be type-annotated, and FastHTML will automatically convert them to the specified type if possible. In this example, idx is annotated as int, so it’s converted from the string in the URL to an integer.

+
+
reg_re_param("imgext", "ico|gif|jpg|jpeg|webm")
+
+@rt(r'/static/{path:path}{fn}.{ext:imgext}')
+def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"
+
+print(cli.get('/static/foo/jph.ico').text)
+
+
Getting jph.ico from /foo/
+
+
+

The reg_re_param function is used to register custom path parameter types using regular expressions. Here, we define a new path parameter type called “imgext” that matches common image file extensions.

+

Handler functions can use complex path patterns with multiple parameters and custom types. In this example, the route pattern r'/static/{path:path}{fn}.{ext:imgext}' uses three path parameters:

+
    +
  1. path: A Starlette built-in type that matches any path segments
  2. +
  3. fn: The filename without extension
  4. +
  5. ext: Our custom “imgext” type that matches specific image extensions
  6. +
+
+
ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet")
+
+@rt("/models/{nm}")
+def get(nm:ModelName): return nm
+
+print(cli.get('/models/alexnet').text)
+
+
alexnet
+
+
+

We define ModelName as an enum with three possible values: “alexnet”, “resnet”, and “lenet”. Handler functions can use these enum types as parameter annotations. In this example, the nm parameter is annotated with ModelName, which ensures that only valid model names are accepted.

+

When a request is made with a valid model name, the handler function returns that name. This pattern is useful for creating type-safe APIs with a predefined set of valid values.

+
+
@rt("/files/{path}")
+async def get(path: Path): return path.with_suffix('.txt')
+print(cli.get('/files/foo').text)
+
+
foo.txt
+
+
+

Handler functions can use Path objects as parameter types. The Path type is from Python’s standard library pathlib module, which provides an object-oriented interface for working with file paths. In this example, the path parameter is annotated with Path, so FastHTML automatically converts the string from the URL to a Path object.

+

This approach is particularly useful when working with file-related routes, as it provides a convenient and platform-independent way to handle file paths.

+
+
fake_db = [{"name": "Foo"}, {"name": "Bar"}]
+
+@rt("/items/")
+def get(idx:int|None = 0): return fake_db[idx]
+print(cli.get('/items/?idx=1').text)
+
+
{"name":"Bar"}
+
+
+

Handler functions can use query parameters, which are automatically parsed from the URL. In this example, idx is a query parameter with a default value of 0. It’s annotated as int|None, allowing it to be either an integer or None.

+

The function uses this parameter to index into a fake database (fake_db). When a request is made with a valid idx query parameter, the handler returns the corresponding item from the database.

+
+
print(cli.get('/items/').text)
+
+
{"name":"Foo"}
+
+
+

When no idx query parameter is provided, the handler function uses the default value of 0. This results in returning the first item from the fake_db list, which is {"name":"Foo"}.

+

This behavior demonstrates how default values for query parameters work in FastHTML. They allow the API to have a sensible default behavior when optional parameters are not provided.

+
+
print(cli.get('/items/?idx=g'))
+
+
<Response [404 Not Found]>
+
+
+

When an invalid value is provided for a typed query parameter, FastHTML returns a 404 Not Found response. In this example, ‘g’ is not a valid integer for the idx parameter, so the request fails with a 404 status.

+

This behavior ensures type safety and prevents invalid inputs from reaching the handler function.

+
+
@app.get("/booly/")
+def _(coming:bool=True): return 'Coming' if coming else 'Not coming'
+print(cli.get('/booly/?coming=true').text)
+print(cli.get('/booly/?coming=no').text)
+
+
Coming
+Not coming
+
+
+

Handler functions can use boolean query parameters. In this example, coming is a boolean parameter with a default value of True. FastHTML automatically converts string values like ‘true’, ‘false’, ‘1’, ‘0’, ‘on’, ‘off’, ‘yes’, and ‘no’ to their corresponding boolean values.

+

The underscore _ is used as the function name in this example to indicate that the function’s name is not important or won’t be referenced elsewhere. This is a common Python convention for throwaway or unused variables, and it works here because FastHTML uses the route decorator parameter, when provided, to determine the URL path, not the function name. By default, both get and post methods can be used in routes that don’t specify an http method (by either using app.get, def get, or the methods parameter to app.route).

+
+
@app.get("/datie/")
+def _(d:parsed_date): return d
+date_str = "17th of May, 2024, 2p"
+print(cli.get(f'/datie/?d={date_str}').text)
+
+
2024-05-17 14:00:00
+
+
+

Handler functions can use date objects as parameter types. FastHTML uses dateutil.parser library to automatically parse a wide variety of date string formats into date objects.

+
+
@app.get("/ua")
+async def _(user_agent:str): return user_agent
+print(cli.get('/ua', headers={'User-Agent':'FastHTML'}).text)
+
+
FastHTML
+
+
+

Handler functions can access HTTP headers by using parameter names that match the header names. In this example, user_agent is used as a parameter name, which automatically captures the value of the ‘User-Agent’ header from the request.

+

The Client instance allows setting custom headers for test requests. Here, we set the ‘User-Agent’ header to ‘FastHTML’ in the test request.

+
+
@app.get("/hxtest")
+def _(htmx): return htmx.request
+print(cli.get('/hxtest', headers={'HX-Request':'1'}).text)
+
+@app.get("/hxtest2")
+def _(foo:HtmxHeaders, req): return foo.request
+print(cli.get('/hxtest2', headers={'HX-Request':'1'}).text)
+
+
1
+1
+
+
+

Handler functions can access HTMX-specific headers using either the special htmx parameter name, or a parameter annotated with HtmxHeaders. Both approaches provide access to HTMX-related information.

+

In these examples, the htmx.request attribute returns the value of the ‘HX-Request’ header.

+
+
app.chk = 'foo'
+@app.get("/app")
+def _(app): return app.chk
+print(cli.get('/app').text)
+
+
foo
+
+
+

Handler functions can access the FastHTML application instance using the special app parameter name. This allows handlers to access application-level attributes and methods.

+

In this example, we set a custom attribute chk on the application instance. The handler function then uses the app parameter to access this attribute and return its value.

+
+
@app.get("/app2")
+def _(foo:FastHTML): return foo.chk,HttpHeader("mykey", "myval")
+r = cli.get('/app2', **hxhdr)
+print(r.text)
+print(r.headers)
+
+
foo
+Headers({'mykey': 'myval', 'content-length': '3', 'content-type': 'text/html; charset=utf-8'})
+
+
+

Handler functions can access the FastHTML application instance using a parameter annotated with FastHTML. This allows handlers to access application-level attributes and methods, just like using the special app parameter name.

+

Handlers can return tuples containing both content and HttpHeader objects. HttpHeader allows setting custom HTTP headers in the response.

+

In this example:

+
    +
  • We define a handler that returns both the chk attribute from the application and a custom header.
  • +
  • The HttpHeader("mykey", "myval") sets a custom header in the response.
  • +
  • We use the test client to make a request and examine both the response text and headers.
  • +
  • The response includes the custom header “mykey” along with standard headers like content-length and content-type.
  • +
+
+
@app.get("/app3")
+def _(foo:FastHTML): return HtmxResponseHeaders(location="http://example.org")
+r = cli.get('/app3')
+print(r.headers)
+
+
Headers({'hx-location': 'http://example.org', 'content-length': '0', 'content-type': 'text/html; charset=utf-8'})
+
+
+

Handler functions can return HtmxResponseHeaders objects to set HTMX-specific response headers. This is useful for HTMX-specific behaviors like client-side redirects.

+

In this example we define a handler that returns an HtmxResponseHeaders object with a location parameter, which sets the HX-Location header in the response. HTMX uses this for client-side redirects.

+
+
@app.get("/app4")
+def _(foo:FastHTML): return Redirect("http://example.org")
+cli.get('/app4', follow_redirects=False)
+
+
<Response [303 See Other]>
+
+
+

Handler functions can return Redirect objects to perform HTTP redirects. This is useful for redirecting users to different pages or external URLs.

+

In this example:

+
    +
  • We define a handler that returns a Redirect object with the URL “http://example.org”.
  • +
  • The cli.get('/app4', follow_redirects=False) call simulates a GET request to the ‘/app4’ route without following redirects.
  • +
  • The response has a 303 See Other status code, indicating a redirect.
  • +
+

The follow_redirects=False parameter is used to prevent the test client from automatically following the redirect, allowing us to inspect the redirect response itself.

+
+
Redirect.__response__
+
+
<function fasthtml.core.Redirect.__response__(self, req)>
+
+
+

The Redirect class in FastHTML implements a __response__ method, which is a special method recognized by the framework. When a handler returns a Redirect object, FastHTML internally calls this __response__ method to replace the original response.

+

The __response__ method takes a req parameter, which represents the incoming request. This allows the method to access request information if needed when constructing the redirect response.

+
+
@rt
+def meta(): 
+    return ((Title('hi'),H1('hi')),
+        (Meta(property='image'), Meta(property='site_name')))
+
+print(cli.post('/meta').text)
+
+
 <!doctype html>
+ <html>
+   <head>
+     <title>hi</title>
+     <meta property="image">
+     <meta property="site_name">
+     <meta charset="utf-8">
+     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
+<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script><script>
+    function sendmsg() {
+        window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
+    }
+    window.onload = function() {
+        sendmsg();
+        document.body.addEventListener('htmx:afterSettle',    sendmsg);
+        document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
+    };</script>   </head>
+   <body>
+     <h1>hi</h1>
+   </body>
+ </html>
+
+
+
+

FastHTML automatically identifies elements typically placed in the <head> (like Title and Meta) and positions them accordingly, while other elements go in the <body>.

+

In this example: - (Title('hi'), H1('hi')) defines the title and main heading. The title is placed in the head, and the H1 in the body. - (Meta(property='image'), Meta(property='site_name')) defines two meta tags, which are both placed in the head.

+
+
+

APIRouter

+

APIRouter is useful when you want to split your application routes across multiple .py files that are part of a single FastHTMl application. It accepts an optional prefix argument that will be applied to all routes within that instance of APIRouter.

+

Below we define several hypothetical product related routes in a products.py and then demonstrate how they can seamlessly be incorporated into a FastHTML app instance.

+
+
# products.py
+ar = APIRouter(prefix="/products")
+
+@ar("/all")
+def all_products(req):
+    return Div(
+        "Welcome to the Products Page! Click the button below to look at the details for product 42",
+        Div(
+            Button(
+                "Details",
+                hx_get=req.url_for("details", pid=42),
+                hx_target="#products_list",
+                hx_swap="outerHTML",
+            ),
+        ),
+        id="products_list",
+    )
+
+
+@ar.get("/{pid}", name="details")
+def details(pid: int):
+    return f"Here are the product details for ID: {pid}"
+
+

Since we specified the prefix=/products in our hypothetical products.py file, all routes defined in that file will be found under /products.

+
+
print(str(ar.rt_funcs.all_products))
+print(str(ar.rt_funcs.details))
+
+
/products/all
+/products/{pid}
+
+
+
+
# main.py
+# from products import ar
+
+app, rt = fast_app()
+ar.to_app(app)
+
+@rt
+def index():
+    return Div(
+        "Click me for a look at our products",
+        hx_get=ar.rt_funcs.all_products,
+        hx_swap="outerHTML",
+    )
+
+

Note how you can reference our python route functions via APIRouter.rt_funcs in your hx_{http_method} calls like normal.

+
+
+

Form Data and JSON Handling

+
+
app = FastHTML()
+rt = app.route
+cli = Client(app)
+
+
+
@app.post('/profile/me')
+def profile_update(username: str): return username
+
+r = cli.post('/profile/me', data={'username' : 'Alexis'}).text
+assert r == 'Alexis'
+print(r)
+
+
Alexis
+
+
+

Handler functions can accept form data parameters, without needing to manually extract it from the request. In this example, username is expected to be sent as form data.

+

The data parameter in the cli.post() method simulates sending form data in the request.

+
+
r = cli.post('/profile/me', data={})
+assert r.status_code == 400
+print(r.text)
+r
+
+
Missing required field: username
+
+
+
<Response [400 Bad Request]>
+
+
+

If required form data is missing, FastHTML automatically returns a 400 Bad Request response with an error message.

+
+
@app.post('/pet/dog')
+def pet_dog(dogname: str = None): return dogname or 'unknown name'
+r = cli.post('/pet/dog', data={}).text
+r
+
+
'unknown name'
+
+
+

Handlers can have optional form data parameters with default values. In this example, dogname is an optional parameter with a default value of None.

+

Here, if the form data doesn’t include the dogname field, the function uses the default value. The function returns either the provided dogname or ‘unknown name’ if dogname is None.

+
+
@dataclass
+class Bodie: a:int;b:str
+
+@rt("/bodie/{nm}")
+def post(nm:str, data:Bodie):
+    res = asdict(data)
+    res['nm'] = nm
+    return res
+
+print(cli.post('/bodie/me', data=dict(a=1, b='foo', nm='me')).text)
+
+
{"a":1,"b":"foo","nm":"me"}
+
+
+

You can use dataclasses to define structured form data. In this example, Bodie is a dataclass with a (int) and b (str) fields.

+

FastHTML automatically converts the incoming form data to a Bodie instance where attribute names match parameter names. Other form data elements are matched with parameters with the same names (in this case, nm).

+

Handler functions can return dictionaries, which FastHTML automatically JSON-encodes.

+
+
@app.post("/bodied/")
+def bodied(data:dict): return data
+
+d = dict(a=1, b='foo')
+print(cli.post('/bodied/', data=d).text)
+
+
{"a":"1","b":"foo"}
+
+
+

dict parameters capture all form data as a dictionary. In this example, the data parameter is annotated with dict, so FastHTML automatically converts all incoming form data into a dictionary.

+

Note that when form data is converted to a dictionary, all values become strings, even if they were originally numbers. This is why the ‘a’ key in the response has a string value “1” instead of the integer 1.

+
+
nt = namedtuple('Bodient', ['a','b'])
+
+@app.post("/bodient/")
+def bodient(data:nt): return asdict(data)
+print(cli.post('/bodient/', data=d).text)
+
+
{"a":"1","b":"foo"}
+
+
+

Handler functions can use named tuples to define structured form data. In this example, Bodient is a named tuple with a and b fields.

+

FastHTML automatically converts the incoming form data to a Bodient instance where field names match parameter names. As with the previous example, all form data values are converted to strings in the process.

+
+
class BodieTD(TypedDict): a:int;b:str='foo'
+
+@app.post("/bodietd/")
+def bodient(data:BodieTD): return data
+print(cli.post('/bodietd/', data=d).text)
+
+
{"a":1,"b":"foo"}
+
+
+

You can use TypedDict to define structured form data with type hints. In this example, BodieTD is a TypedDict with a (int) and b (str) fields, where b has a default value of ‘foo’.

+

FastHTML automatically converts the incoming form data to a BodieTD instance where keys match the defined fields. Unlike with regular dictionaries or named tuples, FastHTML respects the type hints in TypedDict, converting values to the specified types when possible (e.g., converting ‘1’ to the integer 1 for the ‘a’ field).

+
+
class Bodie2:
+    a:int|None; b:str
+    def __init__(self, a, b='foo'): store_attr()
+
+@app.post("/bodie2/")
+def bodie(d:Bodie2): return f"a: {d.a}; b: {d.b}"
+print(cli.post('/bodie2/', data={'a':1}).text)
+
+
a: 1; b: foo
+
+
+

Custom classes can be used to define structured form data. Here, Bodie2 is a custom class with a (int|None) and b (str) attributes, where b has a default value of ‘foo’. The store_attr() function (from fastcore) automatically assigns constructor parameters to instance attributes.

+

FastHTML automatically converts the incoming form data to a Bodie2 instance, matching form fields to constructor parameters. It respects type hints and default values.

+
+
@app.post("/b")
+def index(it: Bodie): return Titled("It worked!", P(f"{it.a}, {it.b}"))
+
+s = json.dumps({"b": "Lorem", "a": 15})
+print(cli.post('/b', headers={"Content-Type": "application/json", 'hx-request':"1"}, data=s).text)
+
+
 <title>It worked!</title>
+<main class="container">   <h1>It worked!</h1>
+   <p>15, Lorem</p>
+</main>
+
+
+

Handler functions can accept JSON data as input, which is automatically parsed into the specified type. In this example, it is of type Bodie, and FastHTML converts the incoming JSON data to a Bodie instance.

+

The Titled component is used to create a page with a title and main content. It automatically generates an <h1> with the provided title, wraps the content in a <main> tag with a “container” class, and adds a title to the head.

+

When making a request with JSON data: - Set the “Content-Type” header to “application/json” - Provide the JSON data as a string in the data parameter of the request

+
+
+

Cookies, Sessions, File Uploads, and more

+
+
@rt("/setcookie")
+def get(): return cookie('now', datetime.now())
+
+@rt("/getcookie")
+def get(now:parsed_date): return f'Cookie was set at time {now.time()}'
+
+print(cli.get('/setcookie').text)
+time.sleep(0.01)
+cli.get('/getcookie').text
+
+
+
+
+
'Cookie was set at time 16:19:27.811570'
+
+
+

Handler functions can set and retrieve cookies. In this example:

+
    +
  • The /setcookie route sets a cookie named ‘now’ with the current datetime.
  • +
  • The /getcookie route retrieves the ‘now’ cookie and returns its value.
  • +
+

The cookie() function is used to create a cookie response. FastHTML automatically converts the datetime object to a string when setting the cookie, and parses it back to a date object when retrieving it.

+
+
cookie('now', datetime.now())
+
+
HttpHeader(k='set-cookie', v='now="2025-01-30 16:19:29.997374"; Path=/; SameSite=lax')
+
+
+

The cookie() function returns an HttpHeader object with the ‘set-cookie’ key. You can return it in a tuple along with FT elements, along with anything else FastHTML supports in responses.

+
+
app = FastHTML(secret_key='soopersecret')
+cli = Client(app)
+rt = app.route
+
+
+
@rt("/setsess")
+def get(sess, foo:str=''):
+    now = datetime.now()
+    sess['auth'] = str(now)
+    return f'Set to {now}'
+
+@rt("/getsess")
+def get(sess): return f'Session time: {sess["auth"]}'
+
+print(cli.get('/setsess').text)
+time.sleep(0.01)
+
+cli.get('/getsess').text
+
+
Set to 2025-01-30 16:19:31.078650
+
+
+
'Session time: 2025-01-30 16:19:31.078650'
+
+
+

Sessions store and retrieve data across requests. To use sessions, you should to initialize the FastHTML application with a secret_key. This is used to cryptographically sign the cookie used by the session.

+

The sess parameter in handler functions provides access to the session data. You can set and get session variables using dictionary-style access.

+
+
@rt("/upload")
+async def post(uf:UploadFile): return (await uf.read()).decode()
+
+with open('../../CHANGELOG.md', 'rb') as f:
+    print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])
+
+
# Release notes
+
+
+

Handler functions can accept file uploads using Starlette’s UploadFile type. In this example:

+
    +
  • The /upload route accepts a file upload named uf.
  • +
  • The UploadFile object provides an asynchronous read() method to access the file contents.
  • +
  • We use await to read the file content asynchronously and decode it to a string.
  • +
+

We added async to the handler function because it uses await to read the file content asynchronously. In Python, any function that uses await must be declared as async. This allows the function to be run asynchronously, potentially improving performance by not blocking other operations while waiting for the file to be read.

+
+
app.static_route('.md', static_path='../..')
+print(cli.get('/README.md').text[:10])
+
+
# FastHTML
+
+
+

The static_route method of the FastHTML application allows serving static files with specified extensions from a given directory. In this example:

+
    +
  • .md files are served from the ../.. directory (two levels up from the current directory).
  • +
  • Accessing /README.md returns the contents of the README.md file from that directory.
  • +
+
+
help(app.static_route_exts)
+
+
Help on method static_route_exts in module fasthtml.core:
+
+static_route_exts(prefix='/', static_path='.', exts='static') method of fasthtml.core.FastHTML instance
+    Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`
+
+
+
+
+
app.static_route_exts()
+assert cli.get('/README.txt').status_code == 404
+print(cli.get('/README.txt').text[:50])
+
+
404 Not Found
+
+
+

The static_route_exts method of the FastHTML application allows serving static files with specified extensions from a given directory. By default:

+
    +
  • It serves files from the current directory (‘.’).
  • +
  • It uses the ‘static’ regex, which includes common static file extensions like ‘ico’, ‘gif’, ‘jpg’, ‘css’, ‘js’, etc.
  • +
  • The URL prefix is set to ‘/’.
  • +
+

The ‘static’ regex is defined by FastHTML using this code:

+
reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html|map")
+
+
@rt("/form-submit/{list_id}")
+def options(list_id: str):
+    headers = {
+        'Access-Control-Allow-Origin': '*',
+        'Access-Control-Allow-Methods': 'POST',
+        'Access-Control-Allow-Headers': '*',
+    }
+    return Response(status_code=200, headers=headers)
+
+print(cli.options('/form-submit/2').headers)
+
+
Headers({'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST', 'access-control-allow-headers': '*', 'content-length': '0', 'set-cookie': 'session_=eyJhdXRoIjogIjIwMjUtMDEtMzAgMTY6MTk6MzEuMDc4NjUwIn0=.Z5vtZA.1ooY2RCWopWAbLYDy6660g_LlHI; path=/; Max-Age=31536000; httponly; samesite=lax'})
+
+
+

FastHTML handlers can handle OPTIONS requests and set custom headers. In this example:

+
    +
  • The /form-submit/{list_id} route handles OPTIONS requests.
  • +
  • Custom headers are set to allow cross-origin requests (CORS).
  • +
  • The function returns a Starlette Response object with a 200 status code and the custom headers.
  • +
+

You can return any Starlette Response type from a handler function, giving you full control over the response when needed.

+
+
def _not_found(req, exc): return Div('nope')
+
+app = FastHTML(exception_handlers={404:_not_found})
+cli = Client(app)
+rt = app.route
+
+r = cli.get('/')
+print(r.text)
+
+
 <!doctype html>
+ <html>
+   <head>
+     <title>FastHTML page</title>
+     <meta charset="utf-8">
+     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
+<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script><script>
+    function sendmsg() {
+        window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
+    }
+    window.onload = function() {
+        sendmsg();
+        document.body.addEventListener('htmx:afterSettle',    sendmsg);
+        document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
+    };</script>   </head>
+   <body>
+     <div>nope</div>
+   </body>
+ </html>
+
+
+
+

FastHTML allows you to define custom exception handlers – in this case, a custom 404 (Not Found) handler function _not_found, which returns a Div component with the text ‘nope’.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/ref/handlers.html.md b/docs/ref/handlers.html.md new file mode 100644 index 0000000000000000000000000000000000000000..1887730f94e121767fa264e855402508038a92a4 --- /dev/null +++ b/docs/ref/handlers.html.md @@ -0,0 +1,1226 @@ +# Handling handlers + + + + +``` python +from fasthtml.common import * +from collections import namedtuple +from typing import TypedDict +from datetime import datetime +import json,time +``` + +``` python +app = FastHTML() +``` + +The [`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml) +class is the main application class for FastHTML apps. + +``` python +rt = app.route +``` + +`app.route` is used to register route handlers. It is a decorator, which +means we place it before a function that is used as a handler. Because +it’s used frequently in most FastHTML applications, we often alias it as +`rt`, as we do here. + +## Basic Route Handling + +``` python +@rt("/hi") +def get(): return 'Hi there' +``` + +Handler functions can return strings directly. These strings are sent as +the response body to the client. + +``` python +cli = Client(app) +``` + +[`Client`](https://www.fastht.ml/docs/api/core.html#client) is a test +client for FastHTML applications. It allows you to simulate requests to +your app without running a server. + +``` python +cli.get('/hi').text +``` + + 'Hi there' + +The `get` method on a +[`Client`](https://www.fastht.ml/docs/api/core.html#client) instance +simulates GET requests to the app. It returns a response object that has +a `.text` attribute, which you can use to access the body of the +response. It calls `httpx.get` internally – all httpx HTTP verbs are +supported. + +``` python +@rt("/hi") +def post(): return 'Postal' +cli.post('/hi').text +``` + + 'Postal' + +Handler functions can be defined for different HTTP methods on the same +route. Here, we define a +[`post`](https://www.fastht.ml/docs/explains/stripe.html#post) handler +for the `/hi` route. The +[`Client`](https://www.fastht.ml/docs/api/core.html#client) instance can +simulate different HTTP methods, including POST requests. + +## Request and Response Objects + +``` python +@app.get("/hostie") +def show_host(req): return req.headers['host'] +cli.get('/hostie').text +``` + + 'testserver' + +Handler functions can accept a `req` (or `request`) parameter, which +represents the incoming request. This object contains information about +the request, including headers. In this example, we return the `host` +header from the request. The test client uses ‘testserver’ as the +default host. + +In this example, we use `@app.get("/hostie")` instead of +`@rt("/hostie")`. The `@app.get()` decorator explicitly specifies the +HTTP method (GET) for the route, while `@rt()` by default handles both +GET and POST requests. + +``` python +@rt +def yoyo(): return 'a yoyo' +cli.post('/yoyo').text +``` + + 'a yoyo' + +If the `@rt` decorator is used without arguments, it uses the function +name as the route path. Here, the `yoyo` function becomes the handler +for the `/yoyo` route. This handler responds to GET and POST methods, +since a specific method wasn’t provided. + +``` python +@rt +def ft1(): return Html(Div('Text.')) +print(cli.get('/ft1').text) +``` + + + +
Text.
+ + +Handler functions can return +[`FT`](https://www.fastht.ml/docs/explains/explaining_xt_components.html) +objects, which are automatically converted to HTML strings. The `FT` +class can take other `FT` components as arguments, such as `Div`. This +allows for easy composition of HTML elements in your responses. + +``` python +@app.get +def autopost(): return Html(Div('Text.', hx_post=yoyo.to())) +print(cli.get('/autopost').text) +``` + + + +
Text.
+ + +The `rt` decorator modifies the `yoyo` function by adding a `to()` +method. This method returns the route path associated with the handler. +It’s a convenient way to reference the route of a handler function +dynamically. + +In the example, `yoyo.to()` is used as the value for `hx_post`. This +means when the div is clicked, it will trigger an HTMX POST request to +the route of the `yoyo` handler. This approach allows for flexible, DRY +code by avoiding hardcoded route strings and automatically updating if +the route changes. + +This pattern is particularly useful in larger applications where routes +might change, or when building reusable components that need to +reference their own routes dynamically. + +``` python +@app.get +def autoget(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b')))) +print(cli.get('/autoget').text) +``` + + + + +
Text.
+ + + +The `rt()` method of handler functions can also accept parameters. When +called with parameters, it returns the route path with a query string +appended. In this example, `show_host.to(a='b')` generates the path +`/hostie?a=b`. + +The `Body` component is used here to demonstrate nesting of FT +components. `Div` is nested inside `Body`, showcasing how you can create +more complex HTML structures. + +The `cls` parameter is used to add a CSS class to the `Div`. This +translates to the `class` attribute in the rendered HTML. (`class` can’t +be used as a parameter name directly in Python since it’s a reserved +word.) + +``` python +@rt('/ft2') +def get(): return Title('Foo'),H1('bar') +print(cli.get('/ft2').text) +``` + + + + + Foo + + + + +

bar

+ + + +Handler functions can return multiple `FT` objects as a tuple. The first +item is treated as the `Title`, and the rest are added to the `Body`. +When the request is not an HTMX request, FastHTML automatically adds +necessary HTML boilerplate, including default `head` content with +required scripts. + +When using `app.route` (or `rt`), if the function name matches an HTTP +verb (e.g., `get`, +[`post`](https://www.fastht.ml/docs/explains/stripe.html#post), `put`, +`delete`), that HTTP method is automatically used for the route. In this +case, a path must be explicitly provided as an argument to the +decorator. + +``` python +hxhdr = {'headers':{'hx-request':"1"}} +print(cli.get('/ft2', **hxhdr).text) +``` + + Foo +

bar

+ +For HTMX requests (indicated by the `hx-request` header), FastHTML +returns only the specified components without the full HTML structure. +This allows for efficient partial page updates in HTMX applications. + +``` python +@rt('/ft3') +def get(): return H1('bar') +print(cli.get('/ft3', **hxhdr).text) +``` + +

bar

+ +When a handler function returns a single `FT` object for an HTMX +request, it’s rendered as a single HTML partial. + +``` python +@rt('/ft4') +def get(): return Html(Head(Title('hi')), Body(P('there'))) + +print(cli.get('/ft4').text) +``` + + + + + hi + + +

there

+ + + +Handler functions can return a complete `Html` structure, including +`Head` and `Body` components. When a full HTML structure is returned, +FastHTML doesn’t add any additional boilerplate. This gives you full +control over the HTML output when needed. + +``` python +@rt +def index(): return "welcome!" +print(cli.get('/').text) +``` + + welcome! + +The `index` function is a special handler in FastHTML. When defined +without arguments to the `@rt` decorator, it automatically becomes the +handler for the root path (`'/'`). This is a convenient way to define +the main page or entry point of your application. + +## Path and Query Parameters + +``` python +@rt('/user/{nm}', name='gday') +def get(nm:str=''): return f"Good day to you, {nm}!" +cli.get('/user/Alexis').text +``` + + 'Good day to you, Alexis!' + +Handler functions can use path parameters, defined using curly braces in +the route – this is implemented by Starlette directly, so all Starlette +path parameters can be used. These parameters are passed as arguments to +the function. + +The `name` parameter in the decorator allows you to give the route a +name, which can be used for URL generation. + +In this example, `{nm}` in the route becomes the `nm` parameter in the +function. The function uses this parameter to create a personalized +greeting. + +``` python +@app.get +def autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis'))) +print(cli.get('/autolink').text) +``` + + + +
Text.
+ + +The [`uri`](https://www.fastht.ml/docs/api/core.html#uri) function is +used to generate URLs for named routes. It takes the route name as its +first argument, followed by any path or query parameters needed for that +route. + +In this example, `uri('gday', nm='Alexis')` generates the URL for the +route named ‘gday’ (which we defined earlier as ‘/user/{nm}’), with +‘Alexis’ as the value for the ‘nm’ parameter. + +The `link` parameter in FT components sets the `href` attribute of the +rendered HTML element. By using +[`uri()`](https://www.fastht.ml/docs/api/core.html#uri), we can +dynamically generate correct URLs even if the underlying route structure +changes. + +This approach promotes maintainable code by centralizing route +definitions and avoiding hardcoded URLs throughout the application. + +``` python +@rt('/link') +def get(req): return f"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}" + +cli.get('/link').text +``` + + 'http://testserver/user/Alexis; http://testserver/hostie' + +The `url_for` method of the request object can be used to generate URLs +for named routes. It takes the route name as its first argument, +followed by any path parameters needed for that route. + +In this example, `req.url_for('gday', nm='Alexis')` generates the full +URL for the route named ‘gday’, including the scheme and host. +Similarly, `req.url_for('show_host')` generates the URL for the +‘show_host’ route. + +This method is particularly useful when you need to generate absolute +URLs, such as for email links or API responses. It ensures that the +correct host and scheme are included, even if the application is +accessed through different domains or protocols. + +``` python +app.url_path_for('gday', nm='Jeremy') +``` + + '/user/Jeremy' + +The `url_path_for` method of the application can be used to generate URL +paths for named routes. Unlike `url_for`, it returns only the path +component of the URL, without the scheme or host. + +In this example, `app.url_path_for('gday', nm='Jeremy')` generates the +path ‘/user/Jeremy’ for the route named ‘gday’. + +This method is useful when you need relative URLs or just the path +component, such as for internal links or when constructing URLs in a +host-agnostic manner. + +``` python +@rt('/oops') +def get(nope): return nope +r = cli.get('/oops?nope=1') +print(r) +r.text +``` + + + + /Users/iflath/git/AnswerDotAI/fasthtml/build/__editable__.python_fasthtml-0.12.1-py3-none-any/fasthtml/core.py:188: UserWarning: `nope has no type annotation and is not a recognised special name, so is ignored. + if arg!='resp': warn(f"`{arg} has no type annotation and is not a recognised special name, so is ignored.") + + '' + +Handler functions can include parameters, but they must be +type-annotated or have special names (like `req`) to be recognized. In +this example, the `nope` parameter is not annotated, so it’s ignored, +resulting in a warning. + +When a parameter is ignored, it doesn’t receive the value from the query +string. This can lead to unexpected behavior, as the function attempts +to return `nope`, which is undefined. + +The `cli.get('/oops?nope=1')` call succeeds with a 200 OK status because +the handler doesn’t raise an exception, but it returns an empty +response, rather than the intended value. + +To fix this, you should either add a type annotation to the parameter +(e.g., `def get(nope: str):`) or use a recognized special name like +`req`. + +``` python +@rt('/html/{idx}') +def get(idx:int): return Body(H4(f'Next is {idx+1}.')) +print(cli.get('/html/1', **hxhdr).text) +``` + + +

Next is 2.

+ + +Path parameters can be type-annotated, and FastHTML will automatically +convert them to the specified type if possible. In this example, `idx` +is annotated as `int`, so it’s converted from the string in the URL to +an integer. + +``` python +reg_re_param("imgext", "ico|gif|jpg|jpeg|webm") + +@rt(r'/static/{path:path}{fn}.{ext:imgext}') +def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}" + +print(cli.get('/static/foo/jph.ico').text) +``` + + Getting jph.ico from /foo/ + +The +[`reg_re_param`](https://www.fastht.ml/docs/api/core.html#reg_re_param) +function is used to register custom path parameter types using regular +expressions. Here, we define a new path parameter type called “imgext” +that matches common image file extensions. + +Handler functions can use complex path patterns with multiple parameters +and custom types. In this example, the route pattern +`r'/static/{path:path}{fn}.{ext:imgext}'` uses three path parameters: + +1. `path`: A Starlette built-in type that matches any path segments +2. `fn`: The filename without extension +3. `ext`: Our custom “imgext” type that matches specific image + extensions + +``` python +ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet") + +@rt("/models/{nm}") +def get(nm:ModelName): return nm + +print(cli.get('/models/alexnet').text) +``` + + alexnet + +We define `ModelName` as an enum with three possible values: “alexnet”, +“resnet”, and “lenet”. Handler functions can use these enum types as +parameter annotations. In this example, the `nm` parameter is annotated +with `ModelName`, which ensures that only valid model names are +accepted. + +When a request is made with a valid model name, the handler function +returns that name. This pattern is useful for creating type-safe APIs +with a predefined set of valid values. + +``` python +@rt("/files/{path}") +async def get(path: Path): return path.with_suffix('.txt') +print(cli.get('/files/foo').text) +``` + + foo.txt + +Handler functions can use +[`Path`](https://www.fastht.ml/docs/api/svg.html#path) objects as +parameter types. The +[`Path`](https://www.fastht.ml/docs/api/svg.html#path) type is from +Python’s standard library `pathlib` module, which provides an +object-oriented interface for working with file paths. In this example, +the `path` parameter is annotated with +[`Path`](https://www.fastht.ml/docs/api/svg.html#path), so FastHTML +automatically converts the string from the URL to a +[`Path`](https://www.fastht.ml/docs/api/svg.html#path) object. + +This approach is particularly useful when working with file-related +routes, as it provides a convenient and platform-independent way to +handle file paths. + +``` python +fake_db = [{"name": "Foo"}, {"name": "Bar"}] + +@rt("/items/") +def get(idx:int|None = 0): return fake_db[idx] +print(cli.get('/items/?idx=1').text) +``` + + {"name":"Bar"} + +Handler functions can use query parameters, which are automatically +parsed from the URL. In this example, `idx` is a query parameter with a +default value of 0. It’s annotated as `int|None`, allowing it to be +either an integer or None. + +The function uses this parameter to index into a fake database +(`fake_db`). When a request is made with a valid `idx` query parameter, +the handler returns the corresponding item from the database. + +``` python +print(cli.get('/items/').text) +``` + + {"name":"Foo"} + +When no `idx` query parameter is provided, the handler function uses the +default value of 0. This results in returning the first item from the +`fake_db` list, which is `{"name":"Foo"}`. + +This behavior demonstrates how default values for query parameters work +in FastHTML. They allow the API to have a sensible default behavior when +optional parameters are not provided. + +``` python +print(cli.get('/items/?idx=g')) +``` + + + +When an invalid value is provided for a typed query parameter, FastHTML +returns a 404 Not Found response. In this example, ‘g’ is not a valid +integer for the `idx` parameter, so the request fails with a 404 status. + +This behavior ensures type safety and prevents invalid inputs from +reaching the handler function. + +``` python +@app.get("/booly/") +def _(coming:bool=True): return 'Coming' if coming else 'Not coming' +print(cli.get('/booly/?coming=true').text) +print(cli.get('/booly/?coming=no').text) +``` + + Coming + Not coming + +Handler functions can use boolean query parameters. In this example, +`coming` is a boolean parameter with a default value of `True`. FastHTML +automatically converts string values like ‘true’, ‘false’, ‘1’, ‘0’, +‘on’, ‘off’, ‘yes’, and ‘no’ to their corresponding boolean values. + +The underscore `_` is used as the function name in this example to +indicate that the function’s name is not important or won’t be +referenced elsewhere. This is a common Python convention for throwaway +or unused variables, and it works here because FastHTML uses the route +decorator parameter, when provided, to determine the URL path, not the +function name. By default, both `get` and +[`post`](https://www.fastht.ml/docs/explains/stripe.html#post) methods +can be used in routes that don’t specify an http method (by either using +`app.get`, `def get`, or the `methods` parameter to `app.route`). + +``` python +@app.get("/datie/") +def _(d:parsed_date): return d +date_str = "17th of May, 2024, 2p" +print(cli.get(f'/datie/?d={date_str}').text) +``` + + 2024-05-17 14:00:00 + +Handler functions can use `date` objects as parameter types. FastHTML +uses `dateutil.parser` library to automatically parse a wide variety of +date string formats into `date` objects. + +``` python +@app.get("/ua") +async def _(user_agent:str): return user_agent +print(cli.get('/ua', headers={'User-Agent':'FastHTML'}).text) +``` + + FastHTML + +Handler functions can access HTTP headers by using parameter names that +match the header names. In this example, `user_agent` is used as a +parameter name, which automatically captures the value of the +‘User-Agent’ header from the request. + +The [`Client`](https://www.fastht.ml/docs/api/core.html#client) instance +allows setting custom headers for test requests. Here, we set the +‘User-Agent’ header to ‘FastHTML’ in the test request. + +``` python +@app.get("/hxtest") +def _(htmx): return htmx.request +print(cli.get('/hxtest', headers={'HX-Request':'1'}).text) + +@app.get("/hxtest2") +def _(foo:HtmxHeaders, req): return foo.request +print(cli.get('/hxtest2', headers={'HX-Request':'1'}).text) +``` + + 1 + 1 + +Handler functions can access HTMX-specific headers using either the +special `htmx` parameter name, or a parameter annotated with +[`HtmxHeaders`](https://www.fastht.ml/docs/api/core.html#htmxheaders). +Both approaches provide access to HTMX-related information. + +In these examples, the `htmx.request` attribute returns the value of the +‘HX-Request’ header. + +``` python +app.chk = 'foo' +@app.get("/app") +def _(app): return app.chk +print(cli.get('/app').text) +``` + + foo + +Handler functions can access the +[`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml) +application instance using the special `app` parameter name. This allows +handlers to access application-level attributes and methods. + +In this example, we set a custom attribute `chk` on the application +instance. The handler function then uses the `app` parameter to access +this attribute and return its value. + +``` python +@app.get("/app2") +def _(foo:FastHTML): return foo.chk,HttpHeader("mykey", "myval") +r = cli.get('/app2', **hxhdr) +print(r.text) +print(r.headers) +``` + + foo + Headers({'mykey': 'myval', 'content-length': '3', 'content-type': 'text/html; charset=utf-8'}) + +Handler functions can access the +[`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml) +application instance using a parameter annotated with +[`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml). This +allows handlers to access application-level attributes and methods, just +like using the special `app` parameter name. + +Handlers can return tuples containing both content and +[`HttpHeader`](https://www.fastht.ml/docs/api/core.html#httpheader) +objects. +[`HttpHeader`](https://www.fastht.ml/docs/api/core.html#httpheader) +allows setting custom HTTP headers in the response. + +In this example: + +- We define a handler that returns both the `chk` attribute from the + application and a custom header. +- The `HttpHeader("mykey", "myval")` sets a custom header in the + response. +- We use the test client to make a request and examine both the response + text and headers. +- The response includes the custom header “mykey” along with standard + headers like content-length and content-type. + +``` python +@app.get("/app3") +def _(foo:FastHTML): return HtmxResponseHeaders(location="http://example.org") +r = cli.get('/app3') +print(r.headers) +``` + + Headers({'hx-location': 'http://example.org', 'content-length': '0', 'content-type': 'text/html; charset=utf-8'}) + +Handler functions can return +[`HtmxResponseHeaders`](https://www.fastht.ml/docs/api/core.html#htmxresponseheaders) +objects to set HTMX-specific response headers. This is useful for +HTMX-specific behaviors like client-side redirects. + +In this example we define a handler that returns an +[`HtmxResponseHeaders`](https://www.fastht.ml/docs/api/core.html#htmxresponseheaders) +object with a `location` parameter, which sets the `HX-Location` header +in the response. HTMX uses this for client-side redirects. + +``` python +@app.get("/app4") +def _(foo:FastHTML): return Redirect("http://example.org") +cli.get('/app4', follow_redirects=False) +``` + + + +Handler functions can return +[`Redirect`](https://www.fastht.ml/docs/api/core.html#redirect) objects +to perform HTTP redirects. This is useful for redirecting users to +different pages or external URLs. + +In this example: + +- We define a handler that returns a + [`Redirect`](https://www.fastht.ml/docs/api/core.html#redirect) object + with the URL “http://example.org”. +- The `cli.get('/app4', follow_redirects=False)` call simulates a GET + request to the ‘/app4’ route without following redirects. +- The response has a 303 See Other status code, indicating a redirect. + +The `follow_redirects=False` parameter is used to prevent the test +client from automatically following the redirect, allowing us to inspect +the redirect response itself. + +``` python +Redirect.__response__ +``` + + + +The [`Redirect`](https://www.fastht.ml/docs/api/core.html#redirect) +class in FastHTML implements a `__response__` method, which is a special +method recognized by the framework. When a handler returns a +[`Redirect`](https://www.fastht.ml/docs/api/core.html#redirect) object, +FastHTML internally calls this `__response__` method to replace the +original response. + +The `__response__` method takes a `req` parameter, which represents the +incoming request. This allows the method to access request information +if needed when constructing the redirect response. + +``` python +@rt +def meta(): + return ((Title('hi'),H1('hi')), + (Meta(property='image'), Meta(property='site_name'))) + +print(cli.post('/meta').text) +``` + + + + + hi + + + + + + +

hi

+ + + +FastHTML automatically identifies elements typically placed in the +`` (like `Title` and `Meta`) and positions them accordingly, while +other elements go in the ``. + +In this example: - `(Title('hi'), H1('hi'))` defines the title and main +heading. The title is placed in the head, and the H1 in the body. - +`(Meta(property='image'), Meta(property='site_name'))` defines two meta +tags, which are both placed in the head. + +## APIRouter + +[`APIRouter`](https://www.fastht.ml/docs/api/core.html#apirouter) is +useful when you want to split your application routes across multiple +`.py` files that are part of a single FastHTMl application. It accepts +an optional `prefix` argument that will be applied to all routes within +that instance of +[`APIRouter`](https://www.fastht.ml/docs/api/core.html#apirouter). + +Below we define several hypothetical product related routes in a +`products.py` and then demonstrate how they can seamlessly be +incorporated into a FastHTML app instance. + +``` python +# products.py +ar = APIRouter(prefix="/products") + +@ar("/all") +def all_products(req): + return Div( + "Welcome to the Products Page! Click the button below to look at the details for product 42", + Div( + Button( + "Details", + hx_get=req.url_for("details", pid=42), + hx_target="#products_list", + hx_swap="outerHTML", + ), + ), + id="products_list", + ) + + +@ar.get("/{pid}", name="details") +def details(pid: int): + return f"Here are the product details for ID: {pid}" +``` + +Since we specified the `prefix=/products` in our hypothetical +`products.py` file, all routes defined in that file will be found under +`/products`. + +``` python +print(str(ar.rt_funcs.all_products)) +print(str(ar.rt_funcs.details)) +``` + + /products/all + /products/{pid} + +``` python +# main.py +# from products import ar + +app, rt = fast_app() +ar.to_app(app) + +@rt +def index(): + return Div( + "Click me for a look at our products", + hx_get=ar.rt_funcs.all_products, + hx_swap="outerHTML", + ) +``` + +Note how you can reference our python route functions via +`APIRouter.rt_funcs` in your `hx_{http_method}` calls like normal. + +## Form Data and JSON Handling + +``` python +app = FastHTML() +rt = app.route +cli = Client(app) +``` + +``` python +@app.post('/profile/me') +def profile_update(username: str): return username + +r = cli.post('/profile/me', data={'username' : 'Alexis'}).text +assert r == 'Alexis' +print(r) +``` + + Alexis + +Handler functions can accept form data parameters, without needing to +manually extract it from the request. In this example, `username` is +expected to be sent as form data. + +The `data` parameter in the `cli.post()` method simulates sending form +data in the request. + +``` python +r = cli.post('/profile/me', data={}) +assert r.status_code == 400 +print(r.text) +r +``` + + Missing required field: username + + + +If required form data is missing, FastHTML automatically returns a 400 +Bad Request response with an error message. + +``` python +@app.post('/pet/dog') +def pet_dog(dogname: str = None): return dogname or 'unknown name' +r = cli.post('/pet/dog', data={}).text +r +``` + + 'unknown name' + +Handlers can have optional form data parameters with default values. In +this example, `dogname` is an optional parameter with a default value of +`None`. + +Here, if the form data doesn’t include the `dogname` field, the function +uses the default value. The function returns either the provided +`dogname` or ‘unknown name’ if `dogname` is `None`. + +``` python +@dataclass +class Bodie: a:int;b:str + +@rt("/bodie/{nm}") +def post(nm:str, data:Bodie): + res = asdict(data) + res['nm'] = nm + return res + +print(cli.post('/bodie/me', data=dict(a=1, b='foo', nm='me')).text) +``` + + {"a":1,"b":"foo","nm":"me"} + +You can use dataclasses to define structured form data. In this example, +`Bodie` is a dataclass with `a` (int) and `b` (str) fields. + +FastHTML automatically converts the incoming form data to a `Bodie` +instance where attribute names match parameter names. Other form data +elements are matched with parameters with the same names (in this case, +`nm`). + +Handler functions can return dictionaries, which FastHTML automatically +JSON-encodes. + +``` python +@app.post("/bodied/") +def bodied(data:dict): return data + +d = dict(a=1, b='foo') +print(cli.post('/bodied/', data=d).text) +``` + + {"a":"1","b":"foo"} + +`dict` parameters capture all form data as a dictionary. In this +example, the `data` parameter is annotated with `dict`, so FastHTML +automatically converts all incoming form data into a dictionary. + +Note that when form data is converted to a dictionary, all values become +strings, even if they were originally numbers. This is why the ‘a’ key +in the response has a string value “1” instead of the integer 1. + +``` python +nt = namedtuple('Bodient', ['a','b']) + +@app.post("/bodient/") +def bodient(data:nt): return asdict(data) +print(cli.post('/bodient/', data=d).text) +``` + + {"a":"1","b":"foo"} + +Handler functions can use named tuples to define structured form data. +In this example, `Bodient` is a named tuple with `a` and `b` fields. + +FastHTML automatically converts the incoming form data to a `Bodient` +instance where field names match parameter names. As with the previous +example, all form data values are converted to strings in the process. + +``` python +class BodieTD(TypedDict): a:int;b:str='foo' + +@app.post("/bodietd/") +def bodient(data:BodieTD): return data +print(cli.post('/bodietd/', data=d).text) +``` + + {"a":1,"b":"foo"} + +You can use `TypedDict` to define structured form data with type hints. +In this example, `BodieTD` is a `TypedDict` with `a` (int) and `b` (str) +fields, where `b` has a default value of ‘foo’. + +FastHTML automatically converts the incoming form data to a `BodieTD` +instance where keys match the defined fields. Unlike with regular +dictionaries or named tuples, FastHTML respects the type hints in +`TypedDict`, converting values to the specified types when possible +(e.g., converting ‘1’ to the integer 1 for the ‘a’ field). + +``` python +class Bodie2: + a:int|None; b:str + def __init__(self, a, b='foo'): store_attr() + +@app.post("/bodie2/") +def bodie(d:Bodie2): return f"a: {d.a}; b: {d.b}" +print(cli.post('/bodie2/', data={'a':1}).text) +``` + + a: 1; b: foo + +Custom classes can be used to define structured form data. Here, +`Bodie2` is a custom class with `a` (int|None) and `b` (str) attributes, +where `b` has a default value of ‘foo’. The `store_attr()` function +(from fastcore) automatically assigns constructor parameters to instance +attributes. + +FastHTML automatically converts the incoming form data to a `Bodie2` +instance, matching form fields to constructor parameters. It respects +type hints and default values. + +``` python +@app.post("/b") +def index(it: Bodie): return Titled("It worked!", P(f"{it.a}, {it.b}")) + +s = json.dumps({"b": "Lorem", "a": 15}) +print(cli.post('/b', headers={"Content-Type": "application/json", 'hx-request':"1"}, data=s).text) +``` + + It worked! +

It worked!

+

15, Lorem

+
+ +Handler functions can accept JSON data as input, which is automatically +parsed into the specified type. In this example, `it` is of type +`Bodie`, and FastHTML converts the incoming JSON data to a `Bodie` +instance. + +The [`Titled`](https://www.fastht.ml/docs/api/xtend.html#titled) +component is used to create a page with a title and main content. It +automatically generates an `

` with the provided title, wraps the +content in a `
` tag with a “container” class, and adds a `title` +to the head. + +When making a request with JSON data: - Set the “Content-Type” header to +“application/json” - Provide the JSON data as a string in the `data` +parameter of the request + +## Cookies, Sessions, File Uploads, and more + +``` python +@rt("/setcookie") +def get(): return cookie('now', datetime.now()) + +@rt("/getcookie") +def get(now:parsed_date): return f'Cookie was set at time {now.time()}' + +print(cli.get('/setcookie').text) +time.sleep(0.01) +cli.get('/getcookie').text +``` + + 'Cookie was set at time 16:19:27.811570' + +Handler functions can set and retrieve cookies. In this example: + +- The `/setcookie` route sets a cookie named ‘now’ with the current + datetime. +- The `/getcookie` route retrieves the ‘now’ cookie and returns its + value. + +The [`cookie()`](https://www.fastht.ml/docs/api/core.html#cookie) +function is used to create a cookie response. FastHTML automatically +converts the datetime object to a string when setting the cookie, and +parses it back to a date object when retrieving it. + +``` python +cookie('now', datetime.now()) +``` + + HttpHeader(k='set-cookie', v='now="2025-01-30 16:19:29.997374"; Path=/; SameSite=lax') + +The [`cookie()`](https://www.fastht.ml/docs/api/core.html#cookie) +function returns an +[`HttpHeader`](https://www.fastht.ml/docs/api/core.html#httpheader) +object with the ‘set-cookie’ key. You can return it in a tuple along +with `FT` elements, along with anything else FastHTML supports in +responses. + +``` python +app = FastHTML(secret_key='soopersecret') +cli = Client(app) +rt = app.route +``` + +``` python +@rt("/setsess") +def get(sess, foo:str=''): + now = datetime.now() + sess['auth'] = str(now) + return f'Set to {now}' + +@rt("/getsess") +def get(sess): return f'Session time: {sess["auth"]}' + +print(cli.get('/setsess').text) +time.sleep(0.01) + +cli.get('/getsess').text +``` + + Set to 2025-01-30 16:19:31.078650 + + 'Session time: 2025-01-30 16:19:31.078650' + +Sessions store and retrieve data across requests. To use sessions, you +should to initialize the FastHTML application with a `secret_key`. This +is used to cryptographically sign the cookie used by the session. + +The `sess` parameter in handler functions provides access to the session +data. You can set and get session variables using dictionary-style +access. + +``` python +@rt("/upload") +async def post(uf:UploadFile): return (await uf.read()).decode() + +with open('../../CHANGELOG.md', 'rb') as f: + print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15]) +``` + + # Release notes + +Handler functions can accept file uploads using Starlette’s `UploadFile` +type. In this example: + +- The `/upload` route accepts a file upload named `uf`. +- The `UploadFile` object provides an asynchronous `read()` method to + access the file contents. +- We use `await` to read the file content asynchronously and decode it + to a string. + +We added `async` to the handler function because it uses `await` to read +the file content asynchronously. In Python, any function that uses +`await` must be declared as `async`. This allows the function to be run +asynchronously, potentially improving performance by not blocking other +operations while waiting for the file to be read. + +``` python +app.static_route('.md', static_path='../..') +print(cli.get('/README.md').text[:10]) +``` + + # FastHTML + +The `static_route` method of the FastHTML application allows serving +static files with specified extensions from a given directory. In this +example: + +- `.md` files are served from the `../..` directory (two levels up from + the current directory). +- Accessing `/README.md` returns the contents of the README.md file from + that directory. + +``` python +help(app.static_route_exts) +``` + + Help on method static_route_exts in module fasthtml.core: + + static_route_exts(prefix='/', static_path='.', exts='static') method of fasthtml.core.FastHTML instance + Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()` + +``` python +app.static_route_exts() +assert cli.get('/README.txt').status_code == 404 +print(cli.get('/README.txt').text[:50]) +``` + + 404 Not Found + +The `static_route_exts` method of the FastHTML application allows +serving static files with specified extensions from a given directory. +By default: + +- It serves files from the current directory (‘.’). +- It uses the ‘static’ regex, which includes common static file + extensions like ‘ico’, ‘gif’, ‘jpg’, ‘css’, ‘js’, etc. +- The URL prefix is set to ‘/’. + +The ‘static’ regex is defined by FastHTML using this code: + +``` python +reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html|map") +``` + +``` python +@rt("/form-submit/{list_id}") +def options(list_id: str): + headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST', + 'Access-Control-Allow-Headers': '*', + } + return Response(status_code=200, headers=headers) + +print(cli.options('/form-submit/2').headers) +``` + + Headers({'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST', 'access-control-allow-headers': '*', 'content-length': '0', 'set-cookie': 'session_=eyJhdXRoIjogIjIwMjUtMDEtMzAgMTY6MTk6MzEuMDc4NjUwIn0=.Z5vtZA.1ooY2RCWopWAbLYDy6660g_LlHI; path=/; Max-Age=31536000; httponly; samesite=lax'}) + +FastHTML handlers can handle OPTIONS requests and set custom headers. In +this example: + +- The `/form-submit/{list_id}` route handles OPTIONS requests. +- Custom headers are set to allow cross-origin requests (CORS). +- The function returns a Starlette `Response` object with a 200 status + code and the custom headers. + +You can return any Starlette Response type from a handler function, +giving you full control over the response when needed. + +``` python +def _not_found(req, exc): return Div('nope') + +app = FastHTML(exception_handlers={404:_not_found}) +cli = Client(app) +rt = app.route + +r = cli.get('/') +print(r.text) +``` + + + + + FastHTML page + + + + +
nope
+ + + +FastHTML allows you to define custom exception handlers – in this case, +a custom 404 (Not Found) handler function `_not_found`, which returns a +`Div` component with the text ‘nope’. diff --git a/docs/ref/live_reload.html b/docs/ref/live_reload.html new file mode 100644 index 0000000000000000000000000000000000000000..e316a9109c124c28cfd4685917a7d93389edce3b --- /dev/null +++ b/docs/ref/live_reload.html @@ -0,0 +1,892 @@ + + + + + + + + + +Live Reloading – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Live Reloading

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

When building your app it can be useful to view your changes in a web browser as you make them. FastHTML supports live reloading which means that it watches for any changes to your code and automatically refreshes the webpage in your browser.

+

To enable live reloading simply replace FastHTML in your app with FastHTMLWithLiveReload.

+
from fasthtml.common import *
+app = FastHTMLWithLiveReload()
+

Then in your terminal run uvicorn with reloading enabled.

+
uvicorn main:app --reload
+

⚠️ Gotchas - A reload is only triggered when you save your changes. - FastHTMLWithLiveReload should only be used during development. - If your app spans multiple directories you might need to use the --reload-dir flag to watch all files in each directory. See the uvicorn docs for more info. - The live reload script is only injected into the page when rendering ft components.

+
+

Live reloading with fast_app

+

In development the fast_app function provides the same functionality. It instantiates the FastHTMLWithLiveReload class if you pass live=True:

+
+
+
main.py
+
+
from fasthtml.common import *
+
+1app, rt = fast_app(live=True)
+
+2serve()
+
+
+
1
+
+fast_app() instantiates the FastHTMLWithLiveReload class. +
+
2
+
+serve() is a wrapper around a uvicorn call. +
+
+

To run main.py in live reload mode, just do python main.py. We recommend turning off live reload when deploying your app to production.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/ref/live_reload.html.md b/docs/ref/live_reload.html.md new file mode 100644 index 0000000000000000000000000000000000000000..930c2c1c24459d223361fd9d840a8a28ac16528a --- /dev/null +++ b/docs/ref/live_reload.html.md @@ -0,0 +1,61 @@ +# Live Reloading + + + + +When building your app it can be useful to view your changes in a web +browser as you make them. FastHTML supports live reloading which means +that it watches for any changes to your code and automatically refreshes +the webpage in your browser. + +To enable live reloading simply replace +[`FastHTML`](https://www.fastht.ml/docs/api/core.html#fasthtml) in your +app with `FastHTMLWithLiveReload`. + +``` python +from fasthtml.common import * +app = FastHTMLWithLiveReload() +``` + +Then in your terminal run `uvicorn` with reloading enabled. + + uvicorn main:app --reload + +**⚠️ Gotchas** - A reload is only triggered when you save your +changes. - `FastHTMLWithLiveReload` should only be used during +development. - If your app spans multiple directories you might need to +use the `--reload-dir` flag to watch all files in each directory. See +the uvicorn [docs](https://www.uvicorn.org/settings/#development) for +more info. - The live reload script is only injected into the page when +rendering [ft +components](https://www.fastht.ml/docs/explains/explaining_xt_components.html). + +## Live reloading with `fast_app` + +In development the `fast_app` function provides the same functionality. +It instantiates the `FastHTMLWithLiveReload` class if you pass +`live=True`: + +
+ +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app(live=True) + +serve() +``` + +
+ +Line 3 +`fast_app()` instantiates the `FastHTMLWithLiveReload` class. + +Line 5 +[`serve()`](https://www.fastht.ml/docs/api/core.html#serve) is a wrapper +around a `uvicorn` call. + +To run `main.py` in live reload mode, just do `python main.py`. We +recommend turning off live reload when deploying your app to production. diff --git a/docs/ref/response_types.html b/docs/ref/response_types.html new file mode 100644 index 0000000000000000000000000000000000000000..306d8a90685027e8dfef305193d2bcd6f1caa616 --- /dev/null +++ b/docs/ref/response_types.html @@ -0,0 +1,1011 @@ + + + + + + + + + + +Response Types – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Response Types

+
+ +
+
+ A list of the different HTTP response types available to your FastHTML route handlers. +
+
+ + +
+ + + + +
+ + + +
+ + + +

FastHTML provides multiple HTTP response types that automatically set the appropriate HTTP content type and handle serialization. The main response types are:

+
    +
  • FT components
  • +
  • Redirects (HTTP 303 and other 3xx codes)
  • +
  • JSON (for API endpoints)
  • +
  • Streams (EventStream, for Server-Side Events)
  • +
  • Plaintext
  • +
+
+
+
+ +
+
+What about websockets? +
+
+
+

Websockets have their own protocol and don’t follow the HTTP request/response cycle. To learn more, check out our explanation about websockets here.

+
+
+
+

Configuration

+
+
from fasthtml.common import *
+
+
+
app,rt = fast_app()
+
+

app and rt are the common FastHTML route handler decorators. We instantiate them with the fast_app function.

+
+
cli = Client(app)
+
+

FastHTML comes with the test client named Client. It allows us to test handlers via a simple interface where .get() is a HTTP GET request, .post() is a HTTP POST request.

+
+
+

FT Component Response

+
+
@rt('/ft')
+def get(): return Html(Div('FT Component Response'))
+
+

This is the response type you’re probably most familiar with. Here the route handler returns an FT component, which FastHTML wraps in an HTML document with a head and body.

+
+
print(cli.get('/ft').text)
+
+
 <!doctype html>
+ <html>
+   <div>FT Component Response</div>
+ </html>
+
+
+
+
+
+

Redirect Response

+
+
@rt('/rr')
+def get(): return Redirect('https://fastht.ml/')
+
+

Here in this route handler, Redirect redirects the user’s browser to the new URL ‘https://fastht.ml/’

+
+
resp = cli.get('/rr')
+print(resp.url)
+print(resp.status_code)
+
+
http://testserver/rr
+303
+
+
+

You can see the URL in the response headers and url attribute, as well as a status code of 303.

+
+
+

JSON Response

+
+
@rt('/json')
+def get(): return {'hello': 'world'}
+
+

This route handler returns a JSON response, where the content-type has been set to .

+
+
resp = cli.get('/json')
+print(resp.headers)
+print(resp.json())
+
+
Headers({'content-length': '17', 'content-type': 'application/json'})
+{'hello': 'world'}
+
+
+

You can see that the Content-Type header has been set to application/json, and that the response is simply the JSON without any HTML wrapping it.

+
+
+

EventStream

+
+
from time import sleep
+
+def counter():
+    """Counter is an generator that
+        publishes a number every second.
+    """
+    for i in range(3):
+        yield sse_message(f"Event {i}")
+        sleep(1)
+
+@rt('/stream')
+def get():
+    return EventStream(counter())
+
+

With server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. Unlike WebSockets, SSE can only go in one direction: server to client. SSE is also part of the HTTP specification unlike WebSockets which uses its own specification.

+
+
resp = cli.get('/stream')
+print(resp.text)
+
+
event: message
+data: Event 0
+
+event: message
+data: Event 1
+
+event: message
+data: Event 2
+
+
+
+
+

Each one of the message events above arrived one second after the previous message event.

+
+
+

Plaintext Response

+
+
@rt('/text')
+def get(): return 'Hello world'
+
+

When you return a string from a route handler, you get a plain-text response.

+
+
print(cli.get('/text').text)
+
+
Hello world
+
+
+

Here you can see that the response text is simply the string you returned, without any HTML wrapping it.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docs/ref/response_types.html.md b/docs/ref/response_types.html.md new file mode 100644 index 0000000000000000000000000000000000000000..f77c9c87c65bcf61ce2c0466684a5cc493a2d328 --- /dev/null +++ b/docs/ref/response_types.html.md @@ -0,0 +1,172 @@ +# Response Types + + + + +FastHTML provides multiple HTTP response types that automatically set +the appropriate HTTP content type and handle serialization. The main +response types are: + +- FT components +- Redirects (HTTP 303 and other 3xx codes) +- JSON (for API endpoints) +- Streams (EventStream, for Server-Side Events) +- Plaintext + +
+ +> **What about websockets?** +> +> Websockets have their own protocol and don’t follow the HTTP +> request/response cycle. To learn more, check out our explanation about +> websockets [here](../explains/websockets.html). + +
+ +## Configuration + +``` python +from fasthtml.common import * +``` + +``` python +app,rt = fast_app() +``` + +`app` and `rt` are the common FastHTML route handler decorators. We +instantiate them with the `fast_app` function. + +``` python +cli = Client(app) +``` + +FastHTML comes with the test client named +[`Client`](https://www.fastht.ml/docs/api/core.html#client). It allows +us to test handlers via a simple interface where `.get()` is a +`HTTP GET` request, `.post()` is a `HTTP POST` request. + +## FT Component Response + +``` python +@rt('/ft') +def get(): return Html(Div('FT Component Response')) +``` + +This is the response type you’re probably most familiar with. Here the +route handler returns an FT component, which FastHTML wraps in an HTML +document with a head and body. + +``` python +print(cli.get('/ft').text) +``` + + + +
FT Component Response
+ + +## Redirect Response + +``` python +@rt('/rr') +def get(): return Redirect('https://fastht.ml/') +``` + +Here in this route handler, +[`Redirect`](https://www.fastht.ml/docs/api/core.html#redirect) +redirects the user’s browser to the new URL ‘https://fastht.ml/’ + +``` python +resp = cli.get('/rr') +print(resp.url) +print(resp.status_code) +``` + + http://testserver/rr + 303 + +You can see the URL in the response headers and `url` attribute, as well +as a status code of 303. + +## JSON Response + +``` python +@rt('/json') +def get(): return {'hello': 'world'} +``` + +This route handler returns a JSON response, where the `content-type` has +been set to . + +``` python +resp = cli.get('/json') +print(resp.headers) +print(resp.json()) +``` + + Headers({'content-length': '17', 'content-type': 'application/json'}) + {'hello': 'world'} + +You can see that the Content-Type header has been set to +application/json, and that the response is simply the JSON without any +HTML wrapping it. + +## EventStream + +``` python +from time import sleep + +def counter(): + """Counter is an generator that + publishes a number every second. + """ + for i in range(3): + yield sse_message(f"Event {i}") + sleep(1) + +@rt('/stream') +def get(): + return EventStream(counter()) +``` + +With server-sent events, it’s possible for a server to send new data to +a web page at any time, by pushing messages to the web page. Unlike +WebSockets, SSE can only go in one direction: server to client. SSE is +also part of the HTTP specification unlike WebSockets which uses its own +specification. + +``` python +resp = cli.get('/stream') +print(resp.text) +``` + + event: message + data: Event 0 + + event: message + data: Event 1 + + event: message + data: Event 2 + +Each one of the message events above arrived one second after the +previous message event. + +## Plaintext Response + +``` python +@rt('/text') +def get(): return 'Hello world' +``` + +When you return a string from a route handler, you get a plain-text +response. + +``` python +print(cli.get('/text').text) +``` + + Hello world + +Here you can see that the response text is simply the string you +returned, without any HTML wrapping it. diff --git a/docs/robots.txt b/docs/robots.txt new file mode 100644 index 0000000000000000000000000000000000000000..53bc2d5adb47eec018e26d683412f61cf01e44ed --- /dev/null +++ b/docs/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://www.fastht.ml/docs/sitemap.xml diff --git a/docs/search.json b/docs/search.json new file mode 100644 index 0000000000000000000000000000000000000000..88c2bc1d69f75dffcf97f76add5be92afdc5f23f --- /dev/null +++ b/docs/search.json @@ -0,0 +1,2272 @@ +[ + { + "objectID": "index.html", + "href": "index.html", + "title": "FastHTML", + "section": "", + "text": "Welcome to the official FastHTML documentation.\nFastHTML is a new next-generation web framework for fast, scalable web applications with minimal, compact code. It’s designed to be:\nFastHTML apps are just Python code, so you can use FastHTML with the full power of the Python language and ecosystem. FastHTML’s functionality maps 1:1 directly to HTML and HTTP, but allows them to be encapsulated using good software engineering practices—so you’ll need to understand these foundations to use this library fully. To understand how and why this works, please read this first: fastht.ml/about.", + "crumbs": [ + "Home", + "Get Started" + ] + }, + { + "objectID": "index.html#installation", + "href": "index.html#installation", + "title": "FastHTML", + "section": "Installation", + "text": "Installation\nSince fasthtml is a Python library, you can install it with:\npip install python-fasthtml\nIn the near future, we hope to add component libraries that can likewise be installed via pip.", + "crumbs": [ + "Home", + "Get Started" + ] + }, + { + "objectID": "index.html#usage", + "href": "index.html#usage", + "title": "FastHTML", + "section": "Usage", + "text": "Usage\nFor a minimal app, create a file “main.py” as follows:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\napp,rt = fast_app()\n\n@rt('/')\ndef get(): return Div(P('Hello World!'), hx_get=\"/change\")\n\nserve()\n\nRunning the app with python main.py prints out a link to your running app: http://localhost:5001. Visit that link in your browser and you should see a page with the text “Hello World!”. Congratulations, you’ve just created your first FastHTML app!\nAdding interactivity is surprisingly easy, thanks to HTMX. Modify the file to add this function:\n\n\nmain.py\n\n@rt('/change')\ndef get(): return P('Nice to be here!')\n\nYou now have a page with a clickable element that changes the text when clicked. When clicking on this link, the server will respond with an “HTML partial”—that is, just a snippet of HTML which will be inserted into the existing page. In this case, the returned element will replace the original P element (since that’s the default behavior of HTMX) with the new version returned by the second route.\nThis “hypermedia-based” approach to web development is a powerful way to build web applications.\n\nGetting help from AI\nBecause FastHTML is newer than most LLMs, AI systems like Cursor, ChatGPT, Claude, and Copilot won’t give useful answers about it. To fix that problem, we’ve provided an LLM-friendly guide that teaches them how to use FastHTML. To use it, add this link for your AI helper to use:\n\n/llms-ctx.txt\n\nThis example is in a format based on recommendations from Anthropic for use with Claude Projects. This works so well that we’ve actually found that Claude can provide even better information than our own documentation! For instance, read through this annotated Claude chat for some great getting-started information, entirely generated from a project using the above text file as context.\nIf you use Cursor, type @doc then choose “Add new doc”, and use the /llms-ctx.txt link above. The context file is auto-generated from our llms.txt (our proposed standard for providing AI-friendly information)—you can generate alternative versions suitable for other models as needed.", + "crumbs": [ + "Home", + "Get Started" + ] + }, + { + "objectID": "index.html#next-steps", + "href": "index.html#next-steps", + "title": "FastHTML", + "section": "Next Steps", + "text": "Next Steps\nStart with the official sources to learn more about FastHTML:\n\nAbout: Learn about the core ideas behind FastHTML\nDocumentation: Learn from examples how to write FastHTML code\nIdiomatic app: Heavily commented source code walking through a complete application, including custom authentication, JS library connections, and database use.\n\nWe also have a 1-hour intro video:\n\nThe capabilities of FastHTML are vast and growing, and not all the features and patterns have been documented yet. Be prepared to invest time into studying and modifying source code, such as the main FastHTML repo’s notebooks and the official FastHTML examples repo:\n\nFastHTML Examples Repo on GitHub\nFastHTML Repo on GitHub\n\nThen explore the small but growing third-party ecosystem of FastHTML tutorials, notebooks, libraries, and components:\n\nFastHTML Gallery: Learn from minimal examples of components (ie chat bubbles, click-to-edit, infinite scroll, etc)\nCreating Custom FastHTML Tags for Markdown Rendering by Isaac Flath\nHow to Build a Simple Login System in FastHTML by Marius Vach\nYour tutorial here!\n\nFinally, join the FastHTML community to ask questions, share your work, and learn from others:\n\nDiscord", + "crumbs": [ + "Home", + "Get Started" + ] + }, + { + "objectID": "index.html#other-languages-and-related-projects", + "href": "index.html#other-languages-and-related-projects", + "title": "FastHTML", + "section": "Other languages and related projects", + "text": "Other languages and related projects\nIf you’re not a Python user, or are keen to try out a new language, we’ll list here other projects that have a similar approach to FastHTML. (Please reach out if you know of any other projects that you’d like to see added.)\n\nhtmgo (Go): “htmgo is a lightweight pure go way to build interactive websites / web applications using go & htmx. By combining the speed & simplicity of go + hypermedia attributes (htmx) to add interactivity to websites, all conveniently wrapped in pure go, you can build simple, fast, interactive websites without touching javascript. All compiled to a single deployable binary”\n\nIf you’re just interested in functional HTML components, rather than a full HTMX server solution, consider:\n\nfastcore.xml.FT: This is actually what FastHTML uses behind the scenes\nhtpy: Similar to fastcore.xml.FT, but with a somewhat different syntax\nelm-html: Elm’s built-in HTML library with a type-safe functional approach\nhiccup: Popular library for representing HTML in Clojure using vectors\nhiccl: HTML generation library for Common Lisp inspired by Clojure’s Hiccup\nFalco.Markup: F# HTML DSL and web framework with type-safe HTML generation\nLucid: Type-safe HTML generation for Haskell using monad transformers\ndream-html: Part of the Dream web framework for OCaml, provides type-safe HTML templating\n\nFor other hypermedia application platforms, not based on HTMX, take a look at:\n\nHotwire/Turbo: Rails-oriented framework that similarly uses HTML-over-the-wire\nLiveView: Phoenix framework’s solution for building interactive web apps with minimal JavaScript\nUnpoly: Another HTML-over-the-wire framework with progressive enhancement\nLivewire: Laravel’s take on building dynamic interfaces with minimal JavaScript", + "crumbs": [ + "Home", + "Get Started" + ] + }, + { + "objectID": "tutorials/index.html", + "href": "tutorials/index.html", + "title": "Tutorials", + "section": "", + "text": "Click through to any of these tutorials to get started with FastHTML’s features.\n\n\n\n\n\n\n\n\n\nTitle\n\n\nDescription\n\n\n\n\n\n\nFastHTML By Example\n\n\nAn introduction to FastHTML from the ground up, with four complete examples\n\n\n\n\nWeb Devs Quickstart\n\n\nA fast introduction to FastHTML for experienced web developers.\n\n\n\n\nJS App Walkthrough\n\n\nHow to build a website with custom JavaScript in FastHTML step-by-step\n\n\n\n\nFastHTML Best Practices\n\n\n\n\n\n\n\nUsing Jupyter to write FastHTML\n\n\nWriting FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.\n\n\n\n\n\nNo matching items", + "crumbs": [ + "Home", + "Tutorials" + ] + }, + { + "objectID": "tutorials/jupyter_and_fasthtml.html", + "href": "tutorials/jupyter_and_fasthtml.html", + "title": "Using Jupyter to write FastHTML", + "section": "", + "text": "Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.\nThe first step is to import necessary libraries. As using FastHTML inside a Jupyter notebook is a special case, it remains a special import.\nfrom fasthtml.common import *\nfrom fasthtml.jupyter import JupyUvi, HTMX\nLet’s create an app with fast_app.\napp, rt = fast_app(pico=True)\nDefine a route to test the application.\n@rt\ndef index():\n return Titled('Hello, Jupyter',\n P('Welcome to the FastHTML + Jupyter example'),\n Button('Click', hx_get='/click', hx_target='#dest'),\n Div(id='dest')\n )\nCreate a server object using JupyUvi, which also starts Uvicorn. The server runs in a separate thread from Jupyter, so it can use normal HTTP client functions in a notebook.\nserver = JupyUvi(app)\nThe HTMX callable displays the server’s HTMX application in an iframe which can be displayed by Jupyter notebook. Pass in the same port variable used in the JupyUvi callable above or leave it blank to use the default (8000).\n# This doesn't display in the docs - uncomment and run it to see it in action\n# HTMX()\nWe didn’t define the /click route, but that’s fine - we can define (or change) it any time, and it’s dynamically inserted into the running app. No need to restart or reload anything!\n@rt\ndef click(): return P('You clicked me!')", + "crumbs": [ + "Home", + "Tutorials", + "Using Jupyter to write FastHTML" + ] + }, + { + "objectID": "tutorials/jupyter_and_fasthtml.html#full-screen-view", + "href": "tutorials/jupyter_and_fasthtml.html#full-screen-view", + "title": "Using Jupyter to write FastHTML", + "section": "Full screen view", + "text": "Full screen view\nYou can view your app outside of Jupyter by going to localhost:PORT, where PORT is usually the default 8000, so in most cases just click this link.", + "crumbs": [ + "Home", + "Tutorials", + "Using Jupyter to write FastHTML" + ] + }, + { + "objectID": "tutorials/jupyter_and_fasthtml.html#graceful-shutdowns", + "href": "tutorials/jupyter_and_fasthtml.html#graceful-shutdowns", + "title": "Using Jupyter to write FastHTML", + "section": "Graceful shutdowns", + "text": "Graceful shutdowns\nUse the server.stop() function displayed below. If you restart Jupyter without calling this line the thread may not be released and the HTMX callable above may throw errors. If that happens, a quick temporary fix is to specify a different port number in JupyUvi and HTMX with the port parameter.\nCleaner solutions to the dangling thread are to kill the dangling thread (dependant on each operating system) or restart the computer.\n\nserver.stop()", + "crumbs": [ + "Home", + "Tutorials", + "Using Jupyter to write FastHTML" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html", + "href": "tutorials/quickstart_for_web_devs.html", + "title": "Web Devs Quickstart", + "section": "", + "text": "pip install python-fasthtml", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#installation", + "href": "tutorials/quickstart_for_web_devs.html#installation", + "title": "Web Devs Quickstart", + "section": "", + "text": "pip install python-fasthtml", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#a-minimal-application", + "href": "tutorials/quickstart_for_web_devs.html#a-minimal-application", + "title": "Web Devs Quickstart", + "section": "A Minimal Application", + "text": "A Minimal Application\nA minimal FastHTML application looks something like this:\n\n\nmain.py\n\n1from fasthtml.common import *\n\n2app, rt = fast_app()\n\n3@rt(\"/\")\n4def get():\n5 return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n6serve()\n\n\n1\n\nWe import what we need for rapid development! A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience.\n\n2\n\nWe instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll take advantage of later in the tutorial.\n\n3\n\nWe use the rt() decorator to tell FastHTML what to return when a user visits / in their browser.\n\n4\n\nWe connect this route to HTTP GET requests by defining a view function called get().\n\n5\n\nA tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach.\n\n6\n\nThe serve() utility configures and runs FastHTML using a library called uvicorn.\n\n\nRun the code:\npython main.py\nThe terminal will look like this:\nINFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)\nINFO: Started reloader process [58058] using WatchFiles\nINFO: Started server process [58060]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nConfirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:\n\n\n\n\n\n\n\nNote\n\n\n\nWhile some linters and developers will complain about the wildcard import, it is by design here and perfectly safe. FastHTML is very deliberate about the objects it exports in fasthtml.common. If it bothers you, you can import the objects you need individually, though it will make the code more verbose and less readable.\nIf you want to learn more about how FastHTML handles imports, we cover that here.", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#a-minimal-charting-application", + "href": "tutorials/quickstart_for_web_devs.html#a-minimal-charting-application", + "title": "Web Devs Quickstart", + "section": "A Minimal Charting Application", + "text": "A Minimal Charting Application\nThe Script function allows you to include JavaScript. You can use Python to generate parts of your JS or JSON like this:\nimport json\nfrom fasthtml.common import * \n\napp, rt = fast_app(hdrs=(Script(src=\"https://cdn.plot.ly/plotly-2.32.0.min.js\"),))\n\ndata = json.dumps({\n \"data\": [{\"x\": [1, 2, 3, 4],\"type\": \"scatter\"},\n {\"x\": [1, 2, 3, 4],\"y\": [16, 5, 11, 9],\"type\": \"scatter\"}],\n \"title\": \"Plotly chart in FastHTML \",\n \"description\": \"This is a demo dashboard\",\n \"type\": \"scatter\"\n})\n\n\n@rt(\"/\")\ndef get():\n return Titled(\"Chart Demo\", Div(id=\"myDiv\"),\n Script(f\"var data = {data}; Plotly.newPlot('myDiv', data);\"))\n\nserve()", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#debug-mode", + "href": "tutorials/quickstart_for_web_devs.html#debug-mode", + "title": "Web Devs Quickstart", + "section": "Debug Mode", + "text": "Debug Mode\nWhen we can’t figure out a bug in FastHTML, we can run it in DEBUG mode. When an error is thrown, the error screen is displayed in the browser. This error setting should never be used in a deployed app.\nfrom fasthtml.common import *\n\n1app, rt = fast_app(debug=True)\n\n@rt(\"/\")\ndef get():\n2 1/0\n return Titled(\"FastHTML Error!\", P(\"Let's error!\"))\n\nserve()\n\n1\n\ndebug=True sets debug mode on.\n\n2\n\nPython throws an error when it tries to divide an integer by zero.", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#routing", + "href": "tutorials/quickstart_for_web_devs.html#routing", + "title": "Web Devs Quickstart", + "section": "Routing", + "text": "Routing\nFastHTML builds upon FastAPI’s friendly decorator pattern for specifying URLs, with extra features:\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n1@rt(\"/\")\ndef get():\n return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n2@rt(\"/hello\")\ndef get():\n return Titled(\"Hello, world!\")\n\nserve()\n\n\n1\n\nThe “/” URL on line 5 is the home of a project. This would be accessed at 127.0.0.1:5001.\n\n2\n\n“/hello” URL on line 9 will be found by the project if the user visits 127.0.0.1:5001/hello.\n\n\n\n\n\n\n\n\nTip\n\n\n\nIt looks like get() is being defined twice, but that’s not the case. Each function decorated with rt is totally separate, and is injected into the router. We’re not calling them in the module’s namespace (locals()). Rather, we’re loading them into the routing mechanism using the rt decorator.\n\n\nYou can do more! Read on to learn what we can do to make parts of the URL dynamic.", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#variables-in-urls", + "href": "tutorials/quickstart_for_web_devs.html#variables-in-urls", + "title": "Web Devs Quickstart", + "section": "Variables in URLs", + "text": "Variables in URLs\nYou can add variable sections to a URL by marking them with {variable_name}. Your function then receives the {variable_name} as a keyword argument, but only if it is the correct type. Here’s an example:\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n1@rt(\"/{name}/{age}\")\n2def get(name: str, age: int):\n3 return Titled(f\"Hello {name.title()}, age {age}\")\n\nserve()\n\n\n1\n\nWe specify two variable names, name and age.\n\n2\n\nWe define two function arguments named identically to the variables. You will note that we specify the Python types to be passed.\n\n3\n\nWe use these functions in our project.\n\n\nTry it out by going to this address: 127.0.0.1:5001/uma/5. You should get a page that says,\n\n“Hello Uma, age 5”.\n\n\nWhat happens if we enter incorrect data?\nThe 127.0.0.1:5001/uma/5 URL works because 5 is an integer. If we enter something that is not, such as 127.0.0.1:5001/uma/five, then FastHTML will return an error instead of a web page.\n\n\n\n\n\n\nFastHTML URL routing supports more complex types\n\n\n\nThe two examples we provide here use Python’s built-in str and int types, but you can use your own types, including more complex ones such as those defined by libraries like attrs, pydantic, and even sqlmodel.", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#http-methods", + "href": "tutorials/quickstart_for_web_devs.html#http-methods", + "title": "Web Devs Quickstart", + "section": "HTTP Methods", + "text": "HTTP Methods\nFastHTML matches function names to HTTP methods. So far the URL routes we’ve defined have been for HTTP GET methods, the most common method for web pages.\nForm submissions often are sent as HTTP POST. When dealing with more dynamic web page designs, also known as Single Page Apps (SPA for short), the need can arise for other methods such as HTTP PUT and HTTP DELETE. The way FastHTML handles this is by changing the function name.\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n@rt(\"/\") \n1def get():\n return Titled(\"HTTP GET\", P(\"Handle GET\"))\n\n@rt(\"/\") \n2def post():\n return Titled(\"HTTP POST\", P(\"Handle POST\"))\n\nserve()\n\n\n1\n\nOn line 6 because the get() function name is used, this will handle HTTP GETs going to the / URI.\n\n2\n\nOn line 10 because the post() function name is used, this will handle HTTP POSTs going to the / URI.", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#css-files-and-inline-styles", + "href": "tutorials/quickstart_for_web_devs.html#css-files-and-inline-styles", + "title": "Web Devs Quickstart", + "section": "CSS Files and Inline Styles", + "text": "CSS Files and Inline Styles\nHere we modify default headers to demonstrate how to use the Sakura CSS microframework instead of FastHTML’s default of Pico CSS.\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app(\n1 pico=False,\n hdrs=(\n Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),\n2 Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),\n3 Style(\"p {color: red;}\")\n))\n\n@app.get(\"/\")\ndef home():\n return Titled(\"FastHTML\",\n P(\"Let's do this!\"),\n )\n\nserve()\n\n\n1\n\nBy setting pico to False, FastHTML will not include pico.min.css.\n\n2\n\nThis will generate an HTML <link> tag for sourcing the css for Sakura.\n\n3\n\nIf you want an inline styles, the Style() function will put the result into the HTML.", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#other-static-media-file-locations", + "href": "tutorials/quickstart_for_web_devs.html#other-static-media-file-locations", + "title": "Web Devs Quickstart", + "section": "Other Static Media File Locations", + "text": "Other Static Media File Locations\nAs you saw, Script and Link are specific to the most common static media use cases in web apps: including JavaScript, CSS, and images. But it also works with videos and other static media files. The default behavior is to look for these files in the root directory - typically we don’t do anything special to include them. We can change the default directory that is looked in for files by adding the static_path parameter to the fast_app function.\napp, rt = fast_app(static_path='public')\nFastHTML also allows us to define a route that uses FileResponse to serve the file at a specified path. This is useful for serving images, videos, and other media files from a different directory without having to change the paths of many files. So if we move the directory containing the media files, we only need to change the path in one place. In the example below, we call images from a directory called public.\n@rt(\"/{fname:path}.{ext:static}\")\nasync def get(fname:str, ext:str): \n return FileResponse(f'public/{fname}.{ext}')", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#rendering-markdown", + "href": "tutorials/quickstart_for_web_devs.html#rendering-markdown", + "title": "Web Devs Quickstart", + "section": "Rendering Markdown", + "text": "Rendering Markdown\nfrom fasthtml.common import *\n\nhdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), )\n\napp, rt = fast_app(hdrs=hdrs)\n\ncontent = \"\"\"\nHere are some _markdown_ elements.\n\n- This is a list item\n- This is another list item\n- And this is a third list item\n\n**Fenced code blocks work here.**\n\"\"\"\n\n@rt('/')\ndef get(req):\n return Titled(\"Markdown rendering example\", Div(content,cls=\"marked\"))\n\nserve()", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#code-highlighting", + "href": "tutorials/quickstart_for_web_devs.html#code-highlighting", + "title": "Web Devs Quickstart", + "section": "Code highlighting", + "text": "Code highlighting\nHere’s how to highlight code without any markdown configuration.\nfrom fasthtml.common import *\n\n# Add the HighlightJS built-in header\nhdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),)\n\napp, rt = fast_app(hdrs=hdrs)\n\ncode_example = \"\"\"\nimport datetime\nimport time\n\nfor i in range(10):\n print(f\"{datetime.datetime.now()}\")\n time.sleep(1)\n\"\"\"\n\n@rt('/')\ndef get(req):\n return Titled(\"Markdown rendering example\",\n Div(\n # The code example needs to be surrounded by\n # Pre & Code elements\n Pre(Code(code_example))\n ))\n\nserve()", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#defining-new-ft-components", + "href": "tutorials/quickstart_for_web_devs.html#defining-new-ft-components", + "title": "Web Devs Quickstart", + "section": "Defining new ft components", + "text": "Defining new ft components\nWe can build our own ft components and combine them with other components. The simplest method is defining them as a function.\n\nfrom fasthtml.common import *\n\n\ndef hero(title, statement):\n return Div(H1(title),P(statement), cls=\"hero\")\n\n# usage example\nMain(\n hero(\"Hello World\", \"This is a hero statement\")\n)\n\n<main> <div class=\"hero\">\n <h1>Hello World</h1>\n <p>This is a hero statement</p>\n </div>\n</main>\n\n\n\nPass through components\nFor when we need to define a new component that allows zero-to-many components to be nested within them, we lean on Python’s *args and **kwargs mechanism. Useful for creating page layout controls.\n\ndef layout(*args, **kwargs):\n \"\"\"Dashboard layout for all our dashboard views\"\"\"\n return Main(\n H1(\"Dashboard\"),\n Div(*args, **kwargs),\n cls=\"dashboard\",\n )\n\n# usage example\nlayout(\n Ul(*[Li(o) for o in range(3)]),\n P(\"Some content\", cls=\"description\"),\n)\n\n<main class=\"dashboard\"> <h1>Dashboard</h1>\n <div>\n <ul>\n <li>0</li>\n <li>1</li>\n <li>2</li>\n </ul>\n <p class=\"description\">Some content</p>\n </div>\n</main>\n\n\n\n\nDataclasses as ft components\nWhile functions are easy to read, for more complex components some might find it easier to use a dataclass.\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Hero:\n title: str\n statement: str\n \n def __ft__(self):\n \"\"\" The __ft__ method renders the dataclass at runtime.\"\"\"\n return Div(H1(self.title),P(self.statement), cls=\"hero\")\n \n# usage example\nMain(\n Hero(\"Hello World\", \"This is a hero statement\")\n)\n\n<main> <div class=\"hero\">\n <h1>Hello World</h1>\n <p>This is a hero statement</p>\n </div>\n</main>", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#testing-views-in-notebooks", + "href": "tutorials/quickstart_for_web_devs.html#testing-views-in-notebooks", + "title": "Web Devs Quickstart", + "section": "Testing views in notebooks", + "text": "Testing views in notebooks\nBecause of the ASGI event loop it is currently impossible to run FastHTML inside a notebook. However, we can still test the output of our views. To do this, we leverage Starlette, an ASGI toolkit that FastHTML uses.\n\n# First we instantiate our app, in this case we remove the\n# default headers to reduce the size of the output.\napp, rt = fast_app(default_hdrs=False)\n\n# Setting up the Starlette test client\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\n\n# Usage example\n@rt(\"/\")\ndef get():\n return Titled(\"FastHTML is awesome\", \n P(\"The fastest way to create web apps in Python\"))\n\nprint(client.get(\"/\").text)\n\n <!doctype html>\n <html>\n <head>\n<title>FastHTML is awesome</title> </head>\n <body>\n<main class=\"container\"> <h1>FastHTML is awesome</h1>\n <p>The fastest way to create web apps in Python</p>\n</main> </body>\n </html>", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#forms", + "href": "tutorials/quickstart_for_web_devs.html#forms", + "title": "Web Devs Quickstart", + "section": "Forms", + "text": "Forms\nTo validate data coming from users, first define a dataclass representing the data you want to check. Here’s an example representing a signup form.\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Profile: email:str; phone:str; age:int\n\nCreate an FT component representing an empty version of that form. Don’t pass in any value to fill the form, that gets handled later.\n\nprofile_form = Form(method=\"post\", action=\"/profile\")(\n Fieldset(\n Label('Email', Input(name=\"email\")),\n Label(\"Phone\", Input(name=\"phone\")),\n Label(\"Age\", Input(name=\"age\")),\n ),\n Button(\"Save\", type=\"submit\"),\n )\nprofile_form\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\">\n</label><label>Phone <input name=\"phone\">\n</label><label>Age <input name=\"age\">\n</label></fieldset><button type=\"submit\">Save</button></form>\n\n\nOnce the dataclass and form function are completed, we can add data to the form. To do that, instantiate the profile dataclass:\n\nprofile = Profile(email='john@example.com', phone='123456789', age=5)\nprofile\n\nProfile(email='john@example.com', phone='123456789', age=5)\n\n\nThen add that data to the profile_form using FastHTML’s fill_form class:\n\nfill_form(profile_form, profile)\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"123456789\">\n</label><label>Age <input name=\"age\" value=\"5\">\n</label></fieldset><button type=\"submit\">Save</button></form>\n\n\n\nForms with views\nThe usefulness of FastHTML forms becomes more apparent when they are combined with FastHTML views. We’ll show how this works by using the test client from above. First, let’s create a SQlite database:\n\ndb = database(\"profiles.db\")\nprofiles = db.create(Profile, pk=\"email\")\n\nNow we insert a record into the database:\n\nprofiles.insert(profile)\n\nProfile(email='john@example.com', phone='123456789', age=5)\n\n\nAnd we can then demonstrate in the code that form is filled and displayed to the user.\n\n@rt(\"/profile/{email}\")\ndef profile(email:str):\n1 profile = profiles[email]\n2 filled_profile_form = fill_form(profile_form, profile)\n return Titled(f'Profile for {profile.email}', filled_profile_form)\n\nprint(client.get(f\"/profile/john@example.com\").text)\n\n\n1\n\nFetch the profile using the profile table’s email primary key\n\n2\n\nFill the form for display.\n\n\n\n\n <!doctype html>\n <html>\n <head>\n<title>Profile for john@example.com</title> </head>\n <body>\n<main class=\"container\"> <h1>Profile for john@example.com</h1>\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"123456789\">\n</label><label>Age <input name=\"age\" value=\"5\">\n</label></fieldset><button type=\"submit\">Save</button></form></main> </body>\n </html>\n\n\n\nAnd now let’s demonstrate making a change to the data.\n\n@rt(\"/profile\")\n1def post(profile: Profile):\n2 profiles.update(profile)\n3 return RedirectResponse(url=f\"/profile/{profile.email}\")\n\nnew_data = dict(email='john@example.com', phone='7654321', age=25)\n4print(client.post(\"/profile\", data=new_data).text)\n\n\n1\n\nWe use the Profile dataclass definition to set the type for the incoming profile content. This validates the field types for the incoming data\n\n2\n\nTaking our validated data, we updated the profiles table\n\n3\n\nWe redirect the user back to their profile view\n\n4\n\nThe display is of the profile form view showing the changes in data.\n\n\n\n\n <!doctype html>\n <html>\n <head>\n<title>Profile for john@example.com</title> </head>\n <body>\n<main class=\"container\"> <h1>Profile for john@example.com</h1>\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"7654321\">\n</label><label>Age <input name=\"age\" value=\"25\">\n</label></fieldset><button type=\"submit\">Save</button></form></main> </body>\n </html>", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#strings-and-conversion-order", + "href": "tutorials/quickstart_for_web_devs.html#strings-and-conversion-order", + "title": "Web Devs Quickstart", + "section": "Strings and conversion order", + "text": "Strings and conversion order\nThe general rules for rendering are: - __ft__ method will be called (for default components like P, H2, etc. or if you define your own components) - If you pass a string, it will be escaped - On other python objects, str() will be called\nAs a consequence, if you want to include plain HTML tags directly into e.g. a Div() they will get escaped by default (as a security measure to avoid code injections). This can be avoided by using NotStr(), a convenient way to reuse python code that returns already HTML. If you use pandas, you can use pandas.DataFrame.to_html() to get a nice table. To include the output a FastHTML, wrap it in NotStr(), like Div(NotStr(df.to_html())).\nAbove we saw how a dataclass behaves with the __ft__ method defined. On a plain dataclass, str() will be called (but not escaped).\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Hero:\n title: str\n statement: str\n \n# rendering the dataclass with the default method\nMain(\n Hero(\"<h1>Hello World</h1>\", \"This is a hero statement\")\n)\n\n<main>Hero(title='<h1>Hello World</h1>', statement='This is a hero statement')</main>\n\n\n\n# This will display the HTML as text on your page\nDiv(\"Let's include some HTML here: <div>Some HTML</div>\")\n\n<div>Let's include some HTML here: <div>Some HTML</div></div>\n\n\n\n# Keep the string untouched, will be rendered on the page\nDiv(NotStr(\"<div><h1>Some HTML</h1></div>\"))\n\n<div><div><h1>Some HTML</h1></div></div>", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#custom-exception-handlers", + "href": "tutorials/quickstart_for_web_devs.html#custom-exception-handlers", + "title": "Web Devs Quickstart", + "section": "Custom exception handlers", + "text": "Custom exception handlers\nFastHTML allows customization of exception handlers, but does so gracefully. What this means is by default it includes all the <html> tags needed to display attractive content. Try it out!\nfrom fasthtml.common import *\n\ndef not_found(req, exc): return Titled(\"404: I don't exist!\")\n\nexception_handlers = {404: not_found}\n\napp, rt = fast_app(exception_handlers=exception_handlers)\n\n@rt('/')\ndef get():\n return (Titled(\"Home page\", P(A(href=\"/oops\")(\"Click to generate 404 error\"))))\n\nserve()\nWe can also use lambda to make things more terse:\nfrom fasthtml.common import *\n\nexception_handlers={\n 404: lambda req, exc: Titled(\"404: I don't exist!\"),\n 418: lambda req, exc: Titled(\"418: I'm a teapot!\")\n}\n\napp, rt = fast_app(exception_handlers=exception_handlers)\n\n@rt('/')\ndef get():\n return (Titled(\"Home page\", P(A(href=\"/oops\")(\"Click to generate 404 error\"))))\n\nserve()", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#cookies", + "href": "tutorials/quickstart_for_web_devs.html#cookies", + "title": "Web Devs Quickstart", + "section": "Cookies", + "text": "Cookies\nWe can set cookies using the cookie() function. In our example, we’ll create a timestamp cookie.\n\nfrom datetime import datetime\nfrom IPython.display import HTML\n\n\n@rt(\"/settimestamp\")\ndef get(req):\n now = datetime.now()\n return P(f'Set to {now}'), cookie('now', datetime.now())\n\nHTML(client.get('/settimestamp').text)\n\n \n \n \nFastHTML page \n \n Set to 2024-09-26 15:33:48.141869\n \n \n\n\nNow let’s get it back using the same name for our parameter as the cookie name.\n\n@rt('/gettimestamp')\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nclient.get('/gettimestamp').text\n\n'Cookie was set at time 15:33:48.141903'", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#sessions", + "href": "tutorials/quickstart_for_web_devs.html#sessions", + "title": "Web Devs Quickstart", + "section": "Sessions", + "text": "Sessions\nFor convenience and security, FastHTML has a mechanism for storing small amounts of data in the user’s browser. We can do this by adding a session argument to routes. FastHTML sessions are Python dictionaries, and we can leverage to our benefit. The example below shows how to concisely set and get sessions.\n\n@rt('/adder/{num}')\ndef get(session, num: int):\n session.setdefault('sum', 0)\n session['sum'] = session.get('sum') + num\n return Response(f'The sum is {session[\"sum\"]}.')", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#toasts-also-known-as-messages", + "href": "tutorials/quickstart_for_web_devs.html#toasts-also-known-as-messages", + "title": "Web Devs Quickstart", + "section": "Toasts (also known as Messages)", + "text": "Toasts (also known as Messages)\nToasts, sometimes called “Messages” are small notifications usually in colored boxes used to notify users that something has happened. Toasts can be of four types:\n\ninfo\nsuccess\nwarning\nerror\n\nExamples toasts might include:\n\n“Payment accepted”\n“Data submitted”\n“Request approved”\n\nToasts require the use of the setup_toasts() function plus every view needs these two features:\n\nThe session argument\nMust return FT components\n\n1setup_toasts(app)\n\n@rt('/toasting')\n2def get(session):\n # Normally one toast is enough, this allows us to see\n # different toast types in action.\n add_toast(session, f\"Toast is being cooked\", \"info\")\n add_toast(session, f\"Toast is ready\", \"success\")\n add_toast(session, f\"Toast is getting a bit crispy\", \"warning\")\n add_toast(session, f\"Toast is burning!\", \"error\")\n3 return Titled(\"I like toast\")\n\n1\n\nsetup_toasts is a helper function that adds toast dependencies. Usually this would be declared right after fast_app()\n\n2\n\nToasts require sessions\n\n3\n\nViews with Toasts must return FT or FtResponse components.\n\n\n💡 setup_toasts takes a duration input that allows you to specify how long a toast will be visible before disappearing. For example setup_toasts(duration=5) sets the toasts duration to 5 seconds. By default toasts disappear after 10 seconds.\n⚠️ Toasts don’t work with SPA like navigation that replaces the entire body such as this navigation trigger A('About', hx_get=\"/about\", hx_swap=\"outerHTML\", hx_push_url=\"true\", hx_target=\"body\"). As an alternative, wrap the content of your route in an element containing an id and set this id as the target for your navigation trigger (i.e. hx_target='#container_id').", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#authentication-and-authorization", + "href": "tutorials/quickstart_for_web_devs.html#authentication-and-authorization", + "title": "Web Devs Quickstart", + "section": "Authentication and authorization", + "text": "Authentication and authorization\nIn FastHTML the tasks of authentication and authorization are handled with Beforeware. Beforeware are functions that run before the route handler is called. They are useful for global tasks like ensuring users are authenticated or have permissions to access a view.\nFirst, we write a function that accepts a request and session arguments:\n\n# Status code 303 is a redirect that can change POST to GET,\n# so it's appropriate for a login page.\nlogin_redir = RedirectResponse('/login', status_code=303)\n\ndef user_auth_before(req, sess):\n # The `auth` key in the request scope is automatically provided\n # to any handler which requests it, and can not be injected\n # by the user using query params, cookies, etc, so it should\n # be secure to use. \n auth = req.scope['auth'] = sess.get('auth', None)\n # If the session key is not there, it redirects to the login page.\n if not auth: return login_redir\n\nNow we pass our user_auth_before function as the first argument into a Beforeware class. We also pass a list of regular expressions to the skip argument, designed to allow users to still get to the home and login pages.\n\nbeforeware = Beforeware(\n user_auth_before,\n skip=[r'/favicon\\.ico', r'/static/.*', r'.*\\.css', r'.*\\.js', '/login', '/']\n)\n\napp, rt = fast_app(before=beforeware)", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#server-sent-events-sse", + "href": "tutorials/quickstart_for_web_devs.html#server-sent-events-sse", + "title": "Web Devs Quickstart", + "section": "Server-sent events (SSE)", + "text": "Server-sent events (SSE)\nWith server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. Unlike WebSockets, SSE can only go in one direction: server to client. SSE is also part of the HTTP specification unlike WebSockets which uses its own specification.\nFastHTML introduces several tools for working with SSE which are covered in the example below. While concise, there’s a lot going on in this function so we’ve annotated it quite a bit.\n\nimport random\nfrom asyncio import sleep\nfrom fasthtml.common import *\n\n1hdrs=(Script(src=\"https://unpkg.com/htmx-ext-sse@2.2.1/sse.js\"),)\napp,rt = fast_app(hdrs=hdrs)\n\n@rt\ndef index():\n return Titled(\"SSE Random Number Generator\",\n P(\"Generate pairs of random numbers, as the list grows scroll downwards.\"),\n2 Div(hx_ext=\"sse\",\n3 sse_connect=\"/number-stream\",\n4 hx_swap=\"beforeend show:bottom\",\n5 sse_swap=\"message\"))\n\n6shutdown_event = signal_shutdown()\n\n7async def number_generator():\n8 while not shutdown_event.is_set():\n data = Article(random.randint(1, 100))\n9 yield sse_message(data)\n await sleep(1)\n\n@rt(\"/number-stream\")\n10async def get(): return EventStream(number_generator())\n\n\n1\n\nImport the HTMX SSE extension\n\n2\n\nTell HTMX to load the SSE extension\n\n3\n\nLook at the /number-stream endpoint for SSE content\n\n4\n\nWhen new items come in from the SSE endpoint, add them at the end of the current content within the div. If they go beyond the screen, scroll downwards\n\n5\n\nSpecify the name of the event. FastHTML’s default event name is “message”. Only change if you have more than one call to SSE endpoints within a view\n\n6\n\nSet up the asyncio event loop\n\n7\n\nDon’t forget to make this an async function!\n\n8\n\nIterate through the asyncio event loop\n\n9\n\nWe yield the data. Data ideally should be comprised of FT components as that plugs nicely into HTMX in the browser\n\n10\n\nThe endpoint view needs to be an async function that returns a EventStream", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#websockets", + "href": "tutorials/quickstart_for_web_devs.html#websockets", + "title": "Web Devs Quickstart", + "section": "Websockets", + "text": "Websockets\nWith websockets we can have bi-directional communications between a browser and client. Websockets are useful for things like chat and certain types of games. While websockets can be used for single direction messages from the server (i.e. telling users that a process is finished), that task is arguably better suited for SSE.\nFastHTML provides useful tools for adding websockets to your pages.\n\nfrom fasthtml.common import *\nfrom asyncio import sleep\n\n1app, rt = fast_app(exts='ws')\n\n2def mk_inp(): return Input(id='msg', autofocus=True)\n\n@rt('/')\nasync def get(request):\n cts = Div(\n Div(id='notifications'),\n3 Form(mk_inp(), id='form', ws_send=True),\n4 hx_ext='ws', ws_connect='/ws')\n return Titled('Websocket Test', cts)\n\n5async def on_connect(send):\n print('Connected!')\n6 await send(Div('Hello, you have connected', id=\"notifications\"))\n\n7async def on_disconnect(ws):\n print('Disconnected!')\n\n8@app.ws('/ws', conn=on_connect, disconn=on_disconnect)\n9async def ws(msg:str, send):\n10 await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n11 return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()\n\n\n1\n\nTo use websockets in FastHTML, you must instantiate the app with exts set to ‘ws’\n\n2\n\nAs we want to use websockets to reset the form, we define the mk_input function that can be called from multiple locations\n\n3\n\nWe create the form and mark it with the ws_send attribute, which is documented here in the HTMX websocket specification. This tells HTMX to send a message to the nearest websocket based on the trigger for the form element, which for forms is pressing the enter key, an action considered to be a form submission\n\n4\n\nThis is where the HTMX extension is loaded (hx_ext='ws') and the nearest websocket is defined (ws_connect='/ws')\n\n5\n\nWhen a websocket first connects we can optionally have it call a function that accepts a send argument. The send argument will push a message to the browser.\n\n6\n\nHere we use the send function that was passed into the on_connect function to send a Div with an id of notifications that HTMX assigns to the element in the page that already has an id of notifications\n\n7\n\nWhen a websocket disconnects we can call a function which takes no arguments. Typically the role of this function is to notify the server to take an action. In this case, we print a simple message to the console\n\n8\n\nWe use the app.ws decorator to mark that /ws is the route for our websocket. We also pass in the two optional conn and disconn parameters to this decorator. As a fun experiment, remove the conn and disconn arguments and see what happens\n\n9\n\nDefine the ws function as async. This is necessary for ASGI to be able to serve websockets. The function accepts two arguments, a msg that is user input from the browser, and a send function for pushing data back to the browser\n\n10\n\nThe send function is used here to send HTML back to the page. As the HTML has an id of notifications, HTMX will overwrite what is already on the page with the same ID\n\n11\n\nThe websocket function can also be used to return a value. In this case, it is a tuple of two HTML elements. HTMX will take the elements and replace them where appropriate. As both have id specified (notifications and msg respectively), they will replace their predecessor on the page.", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "tutorials/quickstart_for_web_devs.html#file-uploads", + "href": "tutorials/quickstart_for_web_devs.html#file-uploads", + "title": "Web Devs Quickstart", + "section": "File Uploads", + "text": "File Uploads\nA common task in web development is uploading files. The examples below are for uploading files to the hosting server, with information about the uploaded file presented to the user.\n\n\n\n\n\n\nFile uploads in production can be dangerous\n\n\n\nFile uploads can be the target of abuse, accidental or intentional. That means users may attempt to upload files that are too large or present a security risk. This is especially of concern for public facing apps. File upload security is outside the scope of this tutorial, for now we suggest reading the OWASP File Upload Cheat Sheet.\n\n\n\nSingle File Uploads\nfrom fasthtml.common import *\nfrom pathlib import Path\n\napp, rt = fast_app()\n\nupload_dir = Path(\"filez\")\nupload_dir.mkdir(exist_ok=True)\n\n@rt('/')\ndef get():\n return Titled(\"File Upload Demo\",\n Article(\n1 Form(hx_post=upload, hx_target=\"#result-one\")(\n2 Input(type=\"file\", name=\"file\"),\n Button(\"Upload\", type=\"submit\", cls='secondary'),\n ),\n Div(id=\"result-one\")\n )\n )\n\ndef FileMetaDataCard(file):\n return Article(\n Header(H3(file.filename)),\n Ul(\n Li('Size: ', file.size), \n Li('Content Type: ', file.content_type),\n Li('Headers: ', file.headers),\n )\n ) \n\n@rt\n3async def upload(file: UploadFile):\n4 card = FileMetaDataCard(file)\n5 filebuffer = await file.read()\n6 (upload_dir / file.filename).write_bytes(filebuffer)\n return card\n\nserve()\n\n1\n\nEvery form rendered with the Form FT component defaults to enctype=\"multipart/form-data\"\n\n2\n\nDon’t forget to set the Input FT Component’s type to file\n\n3\n\nThe upload view should receive a Starlette UploadFile type. You can add other form variables\n\n4\n\nWe can access the metadata of the card (filename, size, content_type, headers), a quick and safe process. We set that to the card variable\n\n5\n\nIn order to access the contents contained within a file we use the await method to read() it. As files may be quite large or contain bad data, this is a seperate step from accessing metadata\n\n6\n\nThis step shows how to use Python’s built-in pathlib.Path library to write the file to disk.\n\n\n\n\nMultiple File Uploads\nfrom fasthtml.common import *\nfrom pathlib import Path\n\napp, rt = fast_app()\n\nupload_dir = Path(\"filez\")\nupload_dir.mkdir(exist_ok=True)\n\n@rt('/')\ndef get():\n return Titled(\"Multiple File Upload Demo\",\n Article(\n1 Form(hx_post=upload_many, hx_target=\"#result-many\")(\n2 Input(type=\"file\", name=\"files\", multiple=True),\n Button(\"Upload\", type=\"submit\", cls='secondary'),\n ),\n Div(id=\"result-many\")\n )\n )\n\ndef FileMetaDataCard(file):\n return Article(\n Header(H3(file.filename)),\n Ul(\n Li('Size: ', file.size), \n Li('Content Type: ', file.content_type),\n Li('Headers: ', file.headers),\n )\n ) \n\n@rt\n3async def upload_many(files: list[UploadFile]):\n cards = []\n4 for file in files:\n5 cards.append(FileMetaDataCard(file))\n6 filebuffer = await file.read()\n7 (upload_dir / file.filename).write_bytes(filebuffer)\n return cards\n\nserve()\n\n1\n\nEvery form rendered with the Form FT component defaults to enctype=\"multipart/form-data\"\n\n2\n\nDon’t forget to set the Input FT Component’s type to file and assign the multiple attribute to True\n\n3\n\nThe upload view should receive a list containing the Starlette UploadFile type. You can add other form variables\n\n4\n\nIterate through the files\n\n5\n\nWe can access the metadata of the card (filename, size, content_type, headers), a quick and safe process. We add that to the cards variable\n\n6\n\nIn order to access the contents contained within a file we use the await method to read() it. As files may be quite large or contain bad data, this is a seperate step from accessing metadata\n\n7\n\nThis step shows how to use Python’s built-in pathlib.Path library to write the file to disk.", + "crumbs": [ + "Home", + "Tutorials", + "Web Devs Quickstart" + ] + }, + { + "objectID": "api/components.html", + "href": "api/components.html", + "title": "Components", + "section": "", + "text": "from collections import UserDict\nfrom lxml import html as lx\nfrom pprint import pprint", + "crumbs": [ + "Home", + "Source", + "Components" + ] + }, + { + "objectID": "api/components.html#tests", + "href": "api/components.html#tests", + "title": "Components", + "section": "Tests", + "text": "Tests\n\ntest_html2ft('<input value=\"Profit\" name=\"title\" id=\"title\" class=\"char\">', attr1st=True)\ntest_html2ft('<input value=\"Profit\" name=\"title\" id=\"title\" class=\"char\">')\ntest_html2ft('<div id=\"foo\"></div>')\ntest_html2ft('<div id=\"foo\">hi</div>')\ntest_html2ft('<div x-show=\"open\" x-transition:enter=\"transition duration-300\" x-transition:enter-start=\"opacity-0 scale-90\">Hello 👋</div>')\ntest_html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>')\n\n\nassert html2ft('<div id=\"foo\">hi</div>', attr1st=True) == \"Div(id='foo')('hi')\"\nassert html2ft(\"\"\"\n <div x-show=\"open\" x-transition:enter=\"transition duration-300\" x-transition:enter-start=\"opacity-0 scale-90\">Hello 👋</div>\n\"\"\") == \"Div('Hello 👋', x_show='open', **{'x-transition:enter': 'transition duration-300', 'x-transition:enter-start': 'opacity-0 scale-90'})\"\nassert html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>') == \"Div('hello', **{'x-transition:enter.scale.80': True, 'x-transition:leave.scale.90': True})\"\nassert html2ft(\"<img alt=' ' />\") == \"Img(alt=' ')\"", + "crumbs": [ + "Home", + "Source", + "Components" + ] + }, + { + "objectID": "api/js.html", + "href": "api/js.html", + "title": "Javascript examples", + "section": "", + "text": "To expedite fast development, FastHTML comes with several built-in Javascript and formatting components. These are largely provided to demonstrate FastHTML JS patterns. There’s far too many JS libs for FastHTML to wrap them all, and as shown here the code to add FastHTML support is very simple anyway.\n\nsource\n\nlight_media\n\n light_media (css:str)\n\nRender light media for day mode views\n\n\n\n\nType\nDetails\n\n\n\n\ncss\nstr\nCSS to be included in the light media query\n\n\n\n\nlight_media('.body {color: green;}')\n\n<style>@media (prefers-color-scheme: light) {.body {color: green;}}</style>\n\n\n\nsource\n\n\ndark_media\n\n dark_media (css:str)\n\nRender dark media for night mode views\n\n\n\n\nType\nDetails\n\n\n\n\ncss\nstr\nCSS to be included in the dark media query\n\n\n\n\ndark_media('.body {color: white;}')\n\n<style>@media (prefers-color-scheme: dark) {.body {color: white;}}</style>\n\n\n\nsource\n\n\nMarkdownJS\n\n MarkdownJS (sel='.marked')\n\nImplements browser-based markdown rendering.\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.marked\nCSS selector for markdown elements\n\n\n\nUsage example here.\n\n__file__ = '../../fasthtml/katex.js'\n\n\nsource\n\n\nKatexMarkdownJS\n\n KatexMarkdownJS (sel='.marked', inline_delim='$', display_delim='$$',\n math_envs=None)\n\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.marked\nCSS selector for markdown elements\n\n\ninline_delim\nstr\n$\nDelimiter for inline math\n\n\ndisplay_delim\nstr\n$$\nDelimiter for long math\n\n\nmath_envs\nNoneType\nNone\nList of environments to render as display math\n\n\n\nKatexMarkdown usage example:\nlongexample = r\"\"\"\nLong example:\n\n$$\\begin{array}{c}\n\n\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} &\n= \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n\n\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n\n\\nabla \\cdot \\vec{\\mathbf{B}} & = 0\n\n\\end{array}$$\n\"\"\"\n\napp, rt = fast_app(hdrs=[KatexMarkdownJS()])\n\n@rt('/')\ndef get():\n return Titled(\"Katex Examples\", \n # Assigning 'marked' class to components renders content as markdown\n P(cls='marked')(\"Inline example: $\\sqrt{3x-1}+(1+x)^2$\"),\n Div(cls='marked')(longexample)\n )\n\nsource\n\n\nHighlightJS\n\n HighlightJS (sel='pre code:not([data-highlighted=\"yes\"])',\n langs:str|list|tuple='python', light='atom-one-light',\n dark='atom-one-dark')\n\nImplements browser-based syntax highlighting. Usage example here.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\npre code:not([data-highlighted=“yes”])\nCSS selector for code elements. Default is industry standard, be careful before adjusting it\n\n\nlangs\nstr | list | tuple\npython\nLanguage(s) to highlight\n\n\nlight\nstr\natom-one-light\nLight theme\n\n\ndark\nstr\natom-one-dark\nDark theme\n\n\n\n\nsource\n\n\nSortableJS\n\n SortableJS (sel='.sortable', ghost_class='blue-background-class')\n\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.sortable\nCSS selector for sortable elements\n\n\nghost_class\nstr\nblue-background-class\nWhen an element is being dragged, this is the class used to distinguish it from the rest\n\n\n\n\nsource\n\n\nMermaidJS\n\n MermaidJS (sel='.language-mermaid', theme='base')\n\nImplements browser-based Mermaid diagram rendering.\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.language-mermaid\nCSS selector for mermaid elements\n\n\ntheme\nstr\nbase\nMermaid theme to use\n\n\n\napp, rt = fast_app(hdrs=[MermaidJS()])\n@rt('/')\ndef get():\n return Titled(\"Mermaid Examples\", \n # Assigning 'marked' class to components renders content as markdown\n Pre(Code(cls =\"language-mermaid\")('''flowchart TD\n A[main] --> B[\"fact(5)\"] --> C[\"fact(4)\"] --> D[\"fact(3)\"] --> E[\"fact(2)\"] --> F[\"fact(1)\"] --> G[\"fact(0)\"]\n ''')))\nIn a markdown file, just like a code cell you can define\n```mermaid\n graph TD\n A --> B \n B --> C \n C --> E\n```", + "crumbs": [ + "Home", + "Source", + "Javascript examples" + ] + }, + { + "objectID": "api/cli.html", + "href": "api/cli.html", + "title": "Command Line Tools", + "section": "", + "text": "source\n\nrailway_link\n\n railway_link ()\n\nLink the current directory to the current project’s Railway service\n\nsource\n\n\nrailway_deploy\n\n railway_deploy (name:str, mount:<function bool_arg>=True)\n\nDeploy a FastHTML app to Railway\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nname\nstr\n\nThe project name to deploy\n\n\nmount\nbool_arg\nTrue\nCreate a mounted volume at /app/data?", + "crumbs": [ + "Home", + "Source", + "Command Line Tools" + ] + }, + { + "objectID": "api/xtend.html", + "href": "api/xtend.html", + "title": "Component extensions", + "section": "", + "text": "from pprint import pprint\nsource\npara", + "crumbs": [ + "Home", + "Source", + "Component extensions" + ] + }, + { + "objectID": "api/xtend.html#forms", + "href": "api/xtend.html#forms", + "title": "Component extensions", + "section": "Forms", + "text": "Forms\n\nsource\n\nForm\n\n Form (*c, enctype='multipart/form-data', target_id=None, hx_vals=None,\n hx_target=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA Form tag; identical to plain ft_hx version except default enctype='multipart/form-data'\n\nsource\n\n\nHidden\n\n Hidden (value:Any='', id:Any=None, target_id=None, hx_vals=None,\n hx_target=None, cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nAn Input of type ‘hidden’\n\nsource\n\n\nCheckboxX\n\n CheckboxX (checked:bool=False, label=None, value='1', id=None, name=None,\n target_id=None, hx_vals=None, hx_target=None, cls=None,\n title=None, style=None, accesskey=None, contenteditable=None,\n dir=None, draggable=None, enterkeyhint=None, hidden=None,\n inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None,\n hx_history_elt=None, hx_inherit=None, hx_params=None,\n hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)\n\nA Checkbox optionally inside a Label, preceded by a Hidden with matching name\n\nshow(CheckboxX(True, 'Check me out!'))\n\n\n \nCheck me out!\n\n\n\nsource\n\n\nScript\n\n Script (code:str='', id=None, cls=None, title=None, style=None,\n attrmap=None, valmap=None, ft_cls=None, **kwargs)\n\nA Script tag that doesn’t escape its code\n\nsource\n\n\nStyle\n\n Style (*c, id=None, cls=None, title=None, style=None, attrmap=None,\n valmap=None, ft_cls=None, **kwargs)\n\nA Style tag that doesn’t escape its code", + "crumbs": [ + "Home", + "Source", + "Component extensions" + ] + }, + { + "objectID": "api/xtend.html#style-and-script-templates", + "href": "api/xtend.html#style-and-script-templates", + "title": "Component extensions", + "section": "Style and script templates", + "text": "Style and script templates\n\nsource\n\ndouble_braces\n\n double_braces (s)\n\nConvert single braces to double braces if next to special chars or newline\n\nsource\n\n\nundouble_braces\n\n undouble_braces (s)\n\nConvert double braces to single braces if next to special chars or newline\n\nsource\n\n\nloose_format\n\n loose_format (s, **kw)\n\nString format s using kw, without being strict about braces outside of template params\n\nsource\n\n\nScriptX\n\n ScriptX (fname, src=None, nomodule=None, type=None, _async=None,\n defer=None, charset=None, crossorigin=None, integrity=None,\n **kw)\n\nA script element with contents read from fname\n\nsource\n\n\nreplace_css_vars\n\n replace_css_vars (css, pre='tpl', **kwargs)\n\nReplace var(--) CSS variables with kwargs if name prefix matches pre\n\nsource\n\n\nStyleX\n\n StyleX (fname, **kw)\n\nA style element with contents read from fname and variables replaced from kw\n\nsource\n\n\nNbsp\n\n Nbsp ()\n\nA non-breaking space", + "crumbs": [ + "Home", + "Source", + "Component extensions" + ] + }, + { + "objectID": "api/xtend.html#surreal-and-js", + "href": "api/xtend.html#surreal-and-js", + "title": "Component extensions", + "section": "Surreal and JS", + "text": "Surreal and JS\n\nsource\n\nSurreal\n\n Surreal (code:str)\n\nWrap code in domReadyExecute and set m=me() and p=me('-')\n\nsource\n\n\nOn\n\n On (code:str, event:str='click', sel:str='', me=True)\n\nAn async surreal.js script block event handler for event on selector sel,p, making available parent p, event ev, and target e\n\nsource\n\n\nPrev\n\n Prev (code:str, event:str='click')\n\nAn async surreal.js script block event handler for event on previous sibling, with same vars as On\n\nsource\n\n\nNow\n\n Now (code:str, sel:str='')\n\nAn async surreal.js script block on selector me(sel)\n\nsource\n\n\nAnyNow\n\n AnyNow (sel:str, code:str)\n\nAn async surreal.js script block on selector any(sel)\n\nsource\n\n\nrun_js\n\n run_js (js, id=None, **kw)\n\nRun js script, auto-generating id based on name of caller if needed, and js-escaping any kw params\n\nsource\n\n\nHtmxOn\n\n HtmxOn (eventname:str, code:str)\n\n\nsource\n\n\njsd\n\n jsd (org, repo, root, path, prov='gh', typ='script', ver=None, esm=False,\n **kwargs)\n\njsdelivr Script or CSS Link tag, or URL", + "crumbs": [ + "Home", + "Source", + "Component extensions" + ] + }, + { + "objectID": "api/xtend.html#other-helpers", + "href": "api/xtend.html#other-helpers", + "title": "Component extensions", + "section": "Other helpers", + "text": "Other helpers\n\nsource\n\nFragment\n\n Fragment (*c)\n\nAn empty tag, used as a container\n\nfts = Fragment(P('1st'), P('2nd'))\nprint(to_xml(fts))\n\n <p>1st</p>\n <p>2nd</p>\n\n\n\n\nsource\n\n\nTitled\n\n Titled (title:str='FastHTML app', *args, cls='container', target_id=None,\n hx_vals=None, hx_target=None, id=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nAn HTML partial containing a Title, and H1, and any provided children\n\nshow(Titled('my page', P('para')))\n\nmy page\n my page\n \n\n\n\n\nsource\n\n\nSocials\n\n Socials (title, site_name, description, image, url=None, w=1200, h=630,\n twitter_site=None, creator=None, card='summary')\n\nOG and Twitter social card headers\n\nsource\n\n\nYouTubeEmbed\n\n YouTubeEmbed (video_id:str, width:int=560, height:int=315,\n start_time:int=0, no_controls:bool=False,\n title:str='YouTube video player', cls:str='', **kwargs)\n\nEmbed a YouTube video\n\nsource\n\n\nFavicon\n\n Favicon (light_icon, dark_icon)\n\nLight and dark favicon headers\n\nsource\n\n\nclear\n\n clear (id)\n\n\nsource\n\n\nwith_sid\n\n with_sid (app, dest, path='/')", + "crumbs": [ + "Home", + "Source", + "Component extensions" + ] + }, + { + "objectID": "explains/websockets.html", + "href": "explains/websockets.html", + "title": "WebSockets", + "section": "", + "text": "Websockets are a protocol for two-way, persistent communication between a client and server. This is different from HTTP, which uses a request/response model where the client sends a request and the server responds. With websockets, either party can send messages at any time, and the other party can respond.\nThis allows for different applications to be built, including things like chat apps, live-updating dashboards, and real-time collaborative tools, which would require constant polling of the server for updates with HTTP.\nIn FastHTML, you can create a websocket route using the @app.ws decorator. This decorator takes a route path, and optional conn and disconn parameters representing the on_connect and on_disconnect callbacks in websockets, respectively. The function decorated by @app.ws is the main function that is called when a message is received.\nHere’s an example of a basic websocket route:\nThe on_message function is the main function that is called when a message is received and can be named however you like. Similar to standard routes, the arguments to on_message are automatically parsed from the websocket payload for you, so you don’t need to manually parse the message content. However, certain argument names are reserved for special purposes. Here are the most important ones:\nFor example, we can send a message to the client that just connected like this:\nOr if we receive a message from the client, we can send a message back to them:\nOn the client side, we can use HTMX’s websocket extension to open a websocket connection and send/receive messages. For example:\nThis will create a websocket connection to the server on route /ws, and send any form submissions to the server via the websocket. The server will then respond by sending a message back to the client. The client will then update the message div with the message from the server using Out of Band Swaps, which means that the content is swapped with the same id without reloading the page.\nPutting it all together, the code for the client and server should look like this:\nThis is a fairly simple example and could be done just as easily with standard HTTP requests, but it illustrates the basic idea of how websockets work. Let’s look at a more complex example next.", + "crumbs": [ + "Home", + "Explanations", + "WebSockets" + ] + }, + { + "objectID": "explains/websockets.html#session-data-in-websockets", + "href": "explains/websockets.html#session-data-in-websockets", + "title": "WebSockets", + "section": "Session data in Websockets", + "text": "Session data in Websockets\nSession data is shared between standard HTTP routes and Websockets. This means you can access, for example, logged in user ID inside websocket handler:\nfrom fasthtml.common import *\n\napp = FastHTML(exts='ws')\nrt = app.route\n\n@rt('/login')\ndef get(session):\n session[\"person\"] = \"Bob\"\n return \"ok\"\n\n@app.ws('/ws')\nasync def ws(msg:str, send, session):\n await send(Div(f'Hello {session.get(\"person\")}' + msg, id='notifications'))\n\nserve()", + "crumbs": [ + "Home", + "Explanations", + "WebSockets" + ] + }, + { + "objectID": "explains/websockets.html#real-time-chat-app", + "href": "explains/websockets.html#real-time-chat-app", + "title": "WebSockets", + "section": "Real-Time Chat App", + "text": "Real-Time Chat App\nLet’s put our new websocket knowledge to use by building a simple chat app. We will create a chat app where multiple users can send and receive messages in real time.\nLet’s start by defining the app and the home page:\nfrom fasthtml.common import *\n\napp = FastHTML(exts='ws')\nrt = app.route\n\nmsgs = []\n@rt('/')\ndef home(): return Div(\n Div(Ul(*[Li(m) for m in msgs], id='msg-list')),\n Form(Input(id='msg'), id='form', ws_send=True),\n hx_ext='ws', ws_connect='/ws')\nNow, let’s handle the websocket connection. We’ll add a new route for this along with an on_conn and on_disconn function to keep track of the users currently connected to the websocket. Finally, we will handle the logic for sending messages to all connected users.\nusers = {}\ndef on_conn(ws, send): users[str(id(ws))] = send\ndef on_disconn(ws): users.pop(str(id(ws)), None)\n\n@app.ws('/ws', conn=on_conn, disconn=on_disconn)\nasync def ws(msg:str):\n msgs.append(msg)\n # Use associated `send` function to send message to each user\n for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list'))\n\nserve()\nWe can now run this app with python chat_ws.py and open multiple browser tabs to http://localhost:5001. You should be able to send messages in one tab and see them appear in the other tabs.\n\nA Work in Progress\nThis page (and Websocket support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!", + "crumbs": [ + "Home", + "Explanations", + "WebSockets" + ] + }, + { + "objectID": "explains/explaining_xt_components.html", + "href": "explains/explaining_xt_components.html", + "title": "FT Components", + "section": "", + "text": "FT, or ‘FastTags’, are the display components of FastHTML. In fact, the word “components” in the context of FastHTML is often synonymous with FT.\nFor example, when we look at a FastHTML app, in particular the views, as well as various functions and other objects, we see something like the code snippet below. It’s the return statement that we want to pay attention to:\nfrom fasthtml.common import *\n\ndef example():\n # The code below is a set of ft components\n return Div(\n H1(\"FastHTML APP\"),\n P(\"Let's do this\"),\n cls=\"go\"\n )\nLet’s go ahead and call our function and print the result:\nexample()\n\n<div class=\"go\">\n <h1>FastHTML APP</h1>\n <p>Let's do this</p>\n</div>\nAs you can see, when returned to the user from a Python callable, like a function, the ft components are transformed into their string representations of XML or XML-like content such as HTML. More concisely, ft turns Python objects into HTML.\nNow that we know what ft components look and behave like we can begin to understand them. At their most fundamental level, ft components:", + "crumbs": [ + "Home", + "Explanations", + "**FT** Components" + ] + }, + { + "objectID": "explains/explaining_xt_components.html#how-fasthtml-names-ft-components", + "href": "explains/explaining_xt_components.html#how-fasthtml-names-ft-components", + "title": "FT Components", + "section": "How FastHTML names ft components", + "text": "How FastHTML names ft components\nWhen it comes to naming ft components, FastHTML appears to break from PEP8. Specifically, PEP8 specifies that when naming variables, functions and instantiated classes we use the snake_case_pattern. That is to say, lowercase with words separated by underscores. However, FastHTML uses PascalCase for ft components.\nThere’s a couple of reasons for this:\n\nft components can be made from any callable type, so adhering to any one pattern doesn’t make much sense\nIt makes for easier reading of FastHTML code, as anything that is PascalCase is probably an ft component", + "crumbs": [ + "Home", + "Explanations", + "**FT** Components" + ] + }, + { + "objectID": "explains/explaining_xt_components.html#default-ft-components", + "href": "explains/explaining_xt_components.html#default-ft-components", + "title": "FT Components", + "section": "Default FT components", + "text": "Default FT components\nFastHTML has over 150 FT components designed to accelerate web development. Most of these mirror HTML tags such as <div>, <p>, <a>, <title>, and more. However, there are some extra tags added, including:\n\nTitled, a combination of the Title() and H1() tags\nSocials, renders popular social media tags", + "crumbs": [ + "Home", + "Explanations", + "**FT** Components" + ] + }, + { + "objectID": "explains/explaining_xt_components.html#the-fasthtml.ft-namespace", + "href": "explains/explaining_xt_components.html#the-fasthtml.ft-namespace", + "title": "FT Components", + "section": "The fasthtml.ft Namespace", + "text": "The fasthtml.ft Namespace\nSome people prefer to write code using namespaces while adhering to PEP8. If that’s a preference, projects can be coded using the fasthtml.ft namespace.\n\nfrom fasthtml import ft\n\nft.Ul(\n ft.Li(\"one\"),\n ft.Li(\"two\"),\n ft.Li(\"three\")\n)\n\n<ul>\n <li>one</li>\n <li>two</li>\n <li>three</li>\n</ul>", + "crumbs": [ + "Home", + "Explanations", + "**FT** Components" + ] + }, + { + "objectID": "explains/explaining_xt_components.html#attributes", + "href": "explains/explaining_xt_components.html#attributes", + "title": "FT Components", + "section": "Attributes", + "text": "Attributes\nThis example demonstrates many important things to know about how ft components handle attributes.\n#| echo: False\n1Label(\n \"Choose an option\", \n Select(\n2 Option(\"one\", value=\"1\", selected=True),\n3 Option(\"two\", value=\"2\", selected=False),\n4 Option(\"three\", value=3),\n5 cls=\"selector\",\n6 _id=\"counter\",\n7 **{'@click':\"alert('Clicked');\"},\n ),\n8 _for=\"counter\",\n)\n\n1\n\nLine 2 demonstrates that FastHTML appreciates Labels surrounding their fields.\n\n2\n\nOn line 5, we can see that attributes set to the boolean value of True are rendered with just the name of the attribute.\n\n3\n\nOn line 6, we demonstrate that attributes set to the boolean value of False do not appear in the rendered output.\n\n4\n\nLine 7 is an example of how integers and other non-string values in the rendered output are converted to strings.\n\n5\n\nLine 8 is where we set the HTML class using the cls argument. We use cls here as class is a reserved word in Python. During the rendering process this will be converted to the word “class”.\n\n6\n\nLine 9 demonstrates that any named argument passed into an ft component will have the leading underscore stripped away before rendering. Useful for handling reserved words in Python.\n\n7\n\nOn line 10 we have an attribute name that cannot be represented as a python variable. In cases like these, we can use an unpacked dict to represent these values.\n\n8\n\nThe use of _for on line 12 is another demonstration of an argument having the leading underscore stripped during render. We can also use fr as that will be expanded to for.\n\n\nThis renders the following HTML snippet:\n\nLabel(\n \"Choose an option\", \n Select(\n Option(\"one\", value=\"1\", selected=True),\n Option(\"two\", value=\"2\", selected=False),\n Option(\"three\", value=3), # <4>,\n cls=\"selector\",\n _id=\"counter\",\n **{'@click':\"alert('Clicked');\"},\n ),\n _for=\"counter\",\n)\n\n<label for=\"counter\">\nChoose an option\n <select id=\"counter\" @click=\"alert('Clicked');\" class=\"selector\" name=\"counter\">\n <option value=\"1\" selected>one</option>\n <option value=\"2\" >two</option>\n <option value=\"3\">three</option>\n </select>\n</label>", + "crumbs": [ + "Home", + "Explanations", + "**FT** Components" + ] + }, + { + "objectID": "explains/explaining_xt_components.html#defining-new-ft-components", + "href": "explains/explaining_xt_components.html#defining-new-ft-components", + "title": "FT Components", + "section": "Defining new ft components", + "text": "Defining new ft components\nIt is possible and sometimes useful to create your own ft components that generate non-standard tags that are not in the FastHTML library. FastHTML supports created and defining those new tags flexibly.\nFor more information, see the Defining new ft components reference page.", + "crumbs": [ + "Home", + "Explanations", + "**FT** Components" + ] + }, + { + "objectID": "explains/explaining_xt_components.html#ft-components-and-type-hints", + "href": "explains/explaining_xt_components.html#ft-components-and-type-hints", + "title": "FT Components", + "section": "FT components and type hints", + "text": "FT components and type hints\nIf you use type hints, we strongly suggest that FT components be treated as the Any type.\nThe reason is that FastHTML leverages python’s dynamic features to a great degree. Especially when it comes to FT components, which can evaluate out to be FT|str|None|tuple as well as anything that supports the __ft__, __html__, and __str__ method. That’s enough of the Python stack that assigning anything but Any to be the FT type will prove an exercise in frustation.", + "crumbs": [ + "Home", + "Explanations", + "**FT** Components" + ] + }, + { + "objectID": "explains/stripe.html", + "href": "explains/stripe.html", + "title": "Stripe", + "section": "", + "text": "This guide will walk through a minimal example of working with a Stripe one-time payment link and webhook for secure reconciliation of payments.\nTo get started we can import the stripe library and authenticate with a Stripe API key that you can get from the stripe web UI.\nExported source\nfrom fasthtml.common import *\nimport os", + "crumbs": [ + "Home", + "Explanations", + "Stripe" + ] + }, + { + "objectID": "explains/stripe.html#stripe-authentication", + "href": "explains/stripe.html#stripe-authentication", + "title": "Stripe", + "section": "Stripe Authentication", + "text": "Stripe Authentication\nYou can install stripe python sdk directly from pypi:\npip install stripe\nAdditionally, you need to install the stripe cli. You can find how to install it on your specific system in their docs here\n\n# uncomment and execute if needed\n#!pip install stripe\n\n\n\nExported source\nimport stripe\n\n\n\n\nExported source\nstripe.api_key = os.environ.get(\"STRIPE_SECRET_KEY\")\nDOMAIN_URL = os.environ.get(\"DOMAIN_URL\", \"http://localhost:5001\")\n\n\nYou can get this API key from the Stripe Dashboard by going to this url.\n\n\n\n\n\n\nNote\n\n\n\nNote: Make sure you have Test mode turned on in the dashboard.\n\n\n\nMake sure you are using a test key for this tutorial\n\nassert 'test_' in stripe.api_key", + "crumbs": [ + "Home", + "Explanations", + "Stripe" + ] + }, + { + "objectID": "explains/stripe.html#pre-app-setup", + "href": "explains/stripe.html#pre-app-setup", + "title": "Stripe", + "section": "Pre-app setup", + "text": "Pre-app setup\n\n\n\n\n\n\nTip\n\n\n\nEverything in the pre-app setup sections is a run once and not to be included in your web-app.\n\n\n\nCreate a product\nYou can run this to programatically create a Stripe Product with a Price. Typically, this is not something you do dynamically in your FastHTML app, but rather something you set up one time. You can also optionally do this on the Stripe Dashboard UI.\n\n\nExported source\ndef _search_app(app_nm:str, limit=1): \n \"Checks for product based on app_nm and returns the product if it exists\"\n return stripe.Product.search(query=f\"name:'{app_nm}' AND active:'True'\", limit=limit).data\n\ndef create_price(app_nm:str, amt:int, currency=\"usd\") -> list[stripe.Price]:\n \"Create a product and bind it to a price object. If product already exist just return the price list.\"\n existing_product = _search_app(app_nm)\n if existing_product: \n return stripe.Price.list(product=existing_product[0].id).data\n else:\n product = stripe.Product.create(name=f\"{app_nm}\")\n return [stripe.Price.create(product=product.id, unit_amount=amt, currency=currency)]\n\ndef archive_price(app_nm:str):\n \"Archive a price - useful for cleanup if testing.\"\n existing_products = _search_app(app_nm, limit=50)\n for product in existing_products:\n for price in stripe.Price.list(product=product.id).data: \n stripe.Price.modify(price.id, active=False)\n stripe.Product.modify(product.id, active=False)\n\n\n\n\n\n\n\n\nTip\n\n\n\nTo do recurring payment, you would use recurring={\"interval\": \"year\"} or recurring={\"interval\": \"month\"} when creating your stripe price.\n\n\n\n\nExported source\napp_nm = \"[FastHTML Docs] Demo Product\"\nprice_list = create_price(app_nm, amt=1999)\nassert len(price_list) == 1, 'For this tutorial, we only have one price bound to our product.'\nprice = price_list[0]\n\n\n\nprint(f\"Price ID = {price.id}\")\n\nPrice ID = price_1R1ZzcFrdmWPkpOp9M28ykjy\n\n\n\n\nCreate a webook\nA webhook is simply a URL where your app listens for messages from Stripe. It provides a way for Stripe, the payment processor, to notify your application when something happens with a payment. Think of it like a delivery notification: when a customer completes a payment, Stripe needs to tell your application so you can update your records, send confirmation emails, or provide access to purchased content. It is simply a URL,\nBut your app needs to be sure every webhook event is actually coming from Stripe. That is, it needs to authenticate the notification. To do that, your app will need a webhook signing secret, which it uses to confirm that the notifications were signed by Stripe.\nThis secret is different from your Stripe API key. The Stripe API key lets you prove who you are to Stripe. The webhook signing secret lets you be sure messages from Stripe are coming from Stripe.\nYou will need a webhook signing secret whether your app is is running locally in test mode, or whether it is a real production app on running on a server. Here is how you get the webhook signing secret in these two cases.\n\nLocal Webhook\nWhen your application runs locally during development it can be reached only from your computer, so Stripe can’t make an HTTP request against the webhook. To workaround this in development, the Stripe CLI tool creates a secure tunnel which forwards these webhook notifications from Stripe’s servers to your local application.\nRun this command to start that tunnel:\nstripe listen --forward-to http://localhost:5001/webhook\nOn success, that command will also tell you the webhook signing secret. Take the secret it gives you and set it as an environment variable.\nexport STRIPE_LOCAL_TEST_WEBHOOK_SECRET=<your-secret>\n\n\nProduction Webhook\nFor a deployed app, you configure a permanent webhook connection in your Stripe Dashboard. This establishes an official notification channel where Stripe will send real-time updates about payments to your application’s /webhook URL.\nOn the dashboard, you can configure which specific payment event notifications will go to this webhook (e.g., completed checkouts, successful payments, failed payments, etc..). Your app provides the webhook signing secret to the Stripe library, to authenticate that these notifications come from the Stripe service. This is essential for production environments where your app needs to automatically respond to payment activities without manual intervention.\nTo configure the permanent webhook connection, you need to do the following steps:\n\nMake sure you are in Test mode like before\nGo to https://dashboard.stripe.com/test/webhooks\nClick “+ Add endpoint” to create create a new webhook (or, if that is missing, click “Create an event destination”).\nOn the primary screen shown below, “Listen to Stripe events”, fill out the details. Your Endpoint URL will be https://YOURDOMAIN/webhook\nSave your webhook signing scret. On the “Listen to Stripe events” screen, you can find it in the app sample code on the right hand side as the “endpoint secret”. You can also retrieve it later from the dashboard.\n\n\nYou also need to configure which events should generate webhook notifications:\n\nClick “+ Select events” to open the secondary control screen, “Select events to send”, which is shown below. In on our case we want to listen for checkout.session.completed.\nClick the “Add Events” button, to confirm which events to send.\n\n\n\n\n\n\n\n\nTip\n\n\n\nFor subscriptions you may also want to enable additional events for your webhook such as: customer.subscription.created, customer.subscription.deleted, and others based on your use-case.\n\n\n\nFinally, click “Add Endpoint”, to finish configuring the endpoint.", + "crumbs": [ + "Home", + "Explanations", + "Stripe" + ] + }, + { + "objectID": "explains/stripe.html#app", + "href": "explains/stripe.html#app", + "title": "Stripe", + "section": "App", + "text": "App\n\n\n\n\n\n\nTip\n\n\n\nEverything after this point is going to be included in your actual application. The application created in this tutorial can be found here\n\n\n\nSetup to have the right information\nIn order to accept a payment, you need to know who is making the payment.\nThere are many ways to accomplish this, for example using oauth or a form. For this example we will start by hardcoding an email address into a session to simulate what it would look like with oauth.\nWe save the email address into the session object, under the key auth. By putting this logic into beforeware, which runs before every request is processed, we ensure that every route handler will be able to read that address from the session object.\n\n\nExported source\ndef before(sess): sess['auth'] = 'hamel@hamel.com'\nbware = Beforeware(before, skip=['/webhook'])\napp, rt = fast_app(before=bware)\n\n\nWe will need our webhook secret that was created. For this tutorial, we will be using the local development environment variable that was created above. For your deployed production environment, you will need to get the secret for your webhook from the Stripe Dashboard.\n\n\nExported source\nWEBHOOK_SECRET = os.getenv(\"STRIPE_LOCAL_TEST_WEBHOOK_SECRET\")\n\n\n\n\nPayment Setup\nWe need 2 things first:\n\nA button for users to click to pay\nA route that gives stripe the information it needs to process the payment\n\n\n\nExported source\n@rt(\"/\")\ndef home(sess):\n auth = sess['auth']\n return Titled(\n \"Buy Now\", \n Div(H2(\"Demo Product - $19.99\"),\n P(f\"Welcome, {auth}\"),\n Button(\"Buy Now\", hx_post=\"/create-checkout-session\", hx_swap=\"none\"),\n A(\"View Account\", href=\"/account\")))\n\n\nWe are only allowing card payments (payment_method_types=['card']). For additional options see the Stripe docs.\n\n\nExported source\n@rt(\"/create-checkout-session\", methods=[\"POST\"])\nasync def create_checkout_session(sess):\n checkout_session = stripe.checkout.Session.create(\n line_items=[{'price': price.id, 'quantity': 1}],\n mode='payment',\n payment_method_types=['card'],\n customer_email=sess['auth'],\n metadata={'app_name': app_nm, \n 'AnyOther': 'Metadata',},\n # CHECKOUT_SESSION_ID is a special variable Stripe fills in for you\n success_url=DOMAIN_URL + '/success?checkout_sid={CHECKOUT_SESSION_ID}',\n cancel_url=DOMAIN_URL + '/cancel')\n return Redirect(checkout_session.url)\n\n\n\n\n\n\n\n\nTip\n\n\n\nFor subscriptions the mode would typically be subscription instead of payment\n\n\nThis section creates two key components: a simple webpage with a “Buy Now” button, and a function that handles what happens when that button is clicked.\nWhen a customer clicks “Buy Now,” the app creates a Stripe checkout session (essentially a payment page) with product details, price, and customer information. Stripe then takes over the payment process, showing the customer a secure payment form. After payment is completed or canceled, Stripe redirects the customer back to your app using the success or cancel URLs you specified. This approach keeps sensitive payment details off your server, as Stripe handles the actual transaction.\n\n\nPost-Payment Processing\nAfter a customer initiates payment, there are two parallel processes:\n\nUser Experience Flow: The customer is redirected to Stripe’s checkout page, completes payment, and is then redirected back to your application (either the success or cancel page).\nBackend Processing Flow: Stripe sends webhook notifications to your server about payment events, allowing your application to update records, provision access, or trigger other business logic.\n\nThis dual-track approach ensures both a smooth user experience and reliable payment processing.\nThe webhook notification is critical as it’s a reliable way to confirm payment completion.\n\nBackend Processing Flow\nCreate a database schema with the information you’d like to store.\n\n\nExported source\n# Database Table\nclass Payment:\n checkout_session_id: str # Stripe checkout session ID (primary key)\n email: str\n amount: int # Amount paid in cents\n payment_status: str # paid, pending, failed\n created_at: int # Unix timestamp\n metadata: str # Additional payment metadata as JSON\n\n\nConnect to the database\n\n\nExported source\ndb = Database(\"stripe_payments.db\")\npayments = db.create(Payment, pk='checkout_session_id', transform=True)\n\n\nIn our webhook we can execute any business logic and database updating we need to.\n\n\nExported source\n@rt(\"/webhook\")\nasync def post(req):\n payload = await req.body()\n # Verify the event came from Stripe\n try:\n event = stripe.Webhook.construct_event(\n payload, req.headers.get(\"stripe-signature\"), WEBHOOK_SECRET)\n except Exception as e:\n print(f\"Webhook error: {e}\")\n return\n if event and event.type == \"checkout.session.completed\":\n event_data = event.data.object\n if event_data.metadata.get('app_name') == app_nm:\n payment = Payment(\n checkout_session_id=event_data.id,\n email=event_data.customer_email,\n amount=event_data.amount_total,\n payment_status=event_data.payment_status,\n created_at=event_data.created,\n metadata=str(event_data.metadata))\n payments.insert(payment)\n print(f\"Payment recorded for user: {event_data.customer_email}\")\n \n # Do not worry about refunds yet, we will cover how to do this later in the tutorial\n elif event and event.type == \"charge.refunded\":\n event_data = event.data.object\n payment_intent_id = event_data.payment_intent\n sessions = stripe.checkout.Session.list(payment_intent=payment_intent_id)\n if sessions and sessions.data:\n checkout_sid = sessions.data[0].id\n payments.update(Payment(checkout_session_id= checkout_sid, payment_status=\"refunded\"))\n print(f\"Refund recorded for payment: {checkout_sid}\")\n\n\nThe webhook route is where Stripe sends automated notifications about payment events. When a payment is completed, Stripe sends a secure notification to this endpoint. The code verifies this notification is legitimate using the webhook secret, then processes the event data - extracting information like the customer’s email and payment status. This allows your application to automatically update user accounts, trigger fulfillment processes, or record transaction details without manual intervention.\nNote that in this route, our code extracts the user’s email address from the Stripe event, not from the session object. That is the because this route will be hit by a request from Stripe’s servers, not from the user’s browser.\n\n\n\n\n\n\nTip\n\n\n\nWhen doing a subscription, often you would add additional event types in an if statement to update your database appropriately with the subscription status.\nif event.type == \"payment_intent.succeeded\":\n ...\nelif event.type == \"customer.subscription.created\":\n ...\nelif event.type == \"customer.subscription.deleted\":\n ...\n\n\n\n\nUser Experience Flow\nThe /success route is where Stripe will redirect the user after the payment completes successfully, which will also be after Stripe has called the webhook to inform your app of the transaction.\nStripe knows to send the user here, because you provided Stripe with this route when you created a checkout session.\nBut you want to verify this is the case. So in this route, you should verify the user’s payment status, by checking your database for the entry which your app saved when it received that webhook notification.\n\n\nExported source\n@rt(\"/success\")\ndef success(sess, checkout_sid:str): \n # Get payment record from database (saved in the webhook)\n payment = payments[checkout_sid]\n\n if not payment or payment.payment_status != 'paid': \n return Titled(\"Error\", P(\"Payment not found\"))\n\n return Titled(\n \"Success\",\n Div(H2(\"Payment Successful!\"),\n P(f\"Thank you for your purchase, {sess['auth']}\"),\n P(f\"Amount Paid: ${payment.amount / 100:.2f}\"),\n P(f\"Status: {payment.payment_status}\"),\n P(f\"Transaction ID: {payment.checkout_session_id}\"),\n A(\"Back to Home\", href=\"/\")))\n\n\nThere is also a /cancel route, where Stripe will redirect the user if they canceled the checkout.\n\n\nExported source\n@rt(\"/cancel\")\ndef cancel():\n return Titled(\n \"Cancelled\",\n Div(H2(\"Payment Cancelled\"),\n P(\"Your payment was cancelled.\"),\n A(\"Back to Home\", href=\"/\")))\n\n\nThis image shows Stripe’s payment page that customers see after clicking the “Buy Now” button. When your app redirects to the Stripe checkout URL, Stripe displays this secure payment form where customers enter their card details. For testing purposes, you can use Stripe’s test card number (4242 4242 4242 4242) with any future expiration date and any 3-digit CVC code. This test card will successfully process payments in test mode without charging real money. The form shows the product name and price that were configured in your Stripe session, providing a seamless transition from your app to the payment processor and back again after completion.\n\nOnce you have processed the payments you can see each record in the sqlite database that was stored in the webhook.\nNext, we can see how to add the refund route\nIn order to use a refund capability we need an account management page where users can request refunds for their payments.\nWhen you initiate a refund, you can see the status of the refund in your Stripe dasbhoard at https://dashboard.stripe.com/payments, or https://dashboard.stripe.com/test/payments if you are in Test mode\nIt will look like this with a Refunded icon:", + "crumbs": [ + "Home", + "Explanations", + "Stripe" + ] + }, + { + "objectID": "explains/faq.html", + "href": "explains/faq.html", + "title": "FAQ", + "section": "", + "text": "Many editors, including Visual Studio Code, use PyLance to provide error checking for Python. However, PyLance’s error checking is just a guess – it can’t actually know whether your code is correct or not. PyLance particularly struggles with FastHTML’s syntax, which leads to it often reporting false error messages in FastHTML projects.\nTo avoid these misleading error messages, it’s best to disable some PyLance error checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the same approach should also work in other editors based on vscode, such as Cursor and GitHub Codespaces):\n\nOpen your FastHTML project\nPress Ctrl+Shift+P (or Cmd+Shift+P on Mac) to open the Command Palette\nType “Preferences: Open Workspace Settings (JSON)” and select it\nIn the JSON file that opens, add the following lines:\n\n{\n \"python.analysis.diagnosticSeverityOverrides\": {\n \"reportGeneralTypeIssues\": \"none\",\n \"reportOptionalMemberAccess\": \"none\",\n \"reportWildcardImportFromLibrary\": \"none\",\n \"reportRedeclaration\": \"none\",\n \"reportAttributeAccessIssue\": \"none\",\n \"reportInvalidTypeForm\": \"none\",\n \"reportAssignmentType\": \"none\",\n }\n}\n\nSave the file\n\nEven with PyLance diagnostics turned off, your FastHTML code will still run correctly. If you’re still seeing some false errors from PyLance, you can disable it entirely by adding this to your settings:\n{\n \"python.analysis.ignore\": [ \"*\" ]\n}", + "crumbs": [ + "Home", + "Explanations", + "FAQ" + ] + }, + { + "objectID": "explains/faq.html#why-does-my-editor-say-that-i-have-errors-in-my-fasthtml-code", + "href": "explains/faq.html#why-does-my-editor-say-that-i-have-errors-in-my-fasthtml-code", + "title": "FAQ", + "section": "", + "text": "Many editors, including Visual Studio Code, use PyLance to provide error checking for Python. However, PyLance’s error checking is just a guess – it can’t actually know whether your code is correct or not. PyLance particularly struggles with FastHTML’s syntax, which leads to it often reporting false error messages in FastHTML projects.\nTo avoid these misleading error messages, it’s best to disable some PyLance error checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the same approach should also work in other editors based on vscode, such as Cursor and GitHub Codespaces):\n\nOpen your FastHTML project\nPress Ctrl+Shift+P (or Cmd+Shift+P on Mac) to open the Command Palette\nType “Preferences: Open Workspace Settings (JSON)” and select it\nIn the JSON file that opens, add the following lines:\n\n{\n \"python.analysis.diagnosticSeverityOverrides\": {\n \"reportGeneralTypeIssues\": \"none\",\n \"reportOptionalMemberAccess\": \"none\",\n \"reportWildcardImportFromLibrary\": \"none\",\n \"reportRedeclaration\": \"none\",\n \"reportAttributeAccessIssue\": \"none\",\n \"reportInvalidTypeForm\": \"none\",\n \"reportAssignmentType\": \"none\",\n }\n}\n\nSave the file\n\nEven with PyLance diagnostics turned off, your FastHTML code will still run correctly. If you’re still seeing some false errors from PyLance, you can disable it entirely by adding this to your settings:\n{\n \"python.analysis.ignore\": [ \"*\" ]\n}", + "crumbs": [ + "Home", + "Explanations", + "FAQ" + ] + }, + { + "objectID": "explains/faq.html#why-the-distinctive-coding-style", + "href": "explains/faq.html#why-the-distinctive-coding-style", + "title": "FAQ", + "section": "Why the distinctive coding style?", + "text": "Why the distinctive coding style?\nFastHTML coding style is the fastai coding style.\nIf you are coming from a data science background the fastai coding style may already be your preferred style.\nIf you are coming from a PEP-8 background where the use of ruff is encouraged, there is a learning curve. However, once you get used to the fastai coding style you may discover yourself appreciating the concise nature of this style. It also encourages using more functional programming tooling, which is both productive and fun. Having said that, it’s entirely optional!", + "crumbs": [ + "Home", + "Explanations", + "FAQ" + ] + }, + { + "objectID": "explains/faq.html#why-not-jsx", + "href": "explains/faq.html#why-not-jsx", + "title": "FAQ", + "section": "Why not JSX?", + "text": "Why not JSX?\nMany have asked! We think there’s no benefit… Python’s positional and kw args precisely 1:1 map already to html/xml children and attrs, so there’s no need for a new syntax.\nWe wrote some more thoughts on Why Python HTML components over Jinja2, Mako, or JSX here.", + "crumbs": [ + "Home", + "Explanations", + "FAQ" + ] + }, + { + "objectID": "explains/faq.html#why-use-import", + "href": "explains/faq.html#why-use-import", + "title": "FAQ", + "section": "Why use import *", + "text": "Why use import *\nFirst, through the use of the __all__ attribute in our Python modules we control what actually gets imported. So there’s no risk of namespace pollution.\nSecond, our style lends itself to working in rather compact Jupyter notebooks and small Python modules. Hence we know about the source code whose libraries we import * from. This terseness means we can develop faster. We’re a small team, and any edge we can gain is important to us.\nThird, for external libraries, be it core Python, SQLAlchemy, or other things we do tend to use explicit imports. In part to avoid namespace collisions, and also as reference to know where things are coming from.\nWe’ll finish by saying a lot of our users employ explicit imports. If that’s the path you want to take, we encourage the use of from fasthtml import common as fh. The acronym of fh makes it easy to recognize that a symbol is from the FastHTML library.", + "crumbs": [ + "Home", + "Explanations", + "FAQ" + ] + }, + { + "objectID": "explains/faq.html#can-fasthtml-be-used-for-dashboards", + "href": "explains/faq.html#can-fasthtml-be-used-for-dashboards", + "title": "FAQ", + "section": "Can FastHTML be used for dashboards?", + "text": "Can FastHTML be used for dashboards?\nYes it can. In fact, it excels at building dashboards. In addition to being great for building static dashboards, because of its foundation in ASGI and tech stack, FastHTML natively supports Websockets. That means using FastHTML we can create dashboards that autoupdate.", + "crumbs": [ + "Home", + "Explanations", + "FAQ" + ] + }, + { + "objectID": "explains/faq.html#why-is-fasthtml-developed-using-notebooks", + "href": "explains/faq.html#why-is-fasthtml-developed-using-notebooks", + "title": "FAQ", + "section": "Why is FastHTML developed using notebooks?", + "text": "Why is FastHTML developed using notebooks?\nSome people are under the impression that writing software in notebooks is bad.\nWatch this video. We’ve used Jupyter notebooks exported via nbdev to write a wide range of “very serious” software projects over the last three years. This includes deep learning libraries, API clients, Python language extensions, terminal user interfaces, web frameworks, and more!\nnbdev is a Jupyter-powered tool for writing software. Traditional programming environments throw away the result of your exploration in REPLs or notebooks. nbdev makes exploration an integral part of your workflow, all while promoting software engineering best practices.", + "crumbs": [ + "Home", + "Explanations", + "FAQ" + ] + }, + { + "objectID": "explains/faq.html#why-not-pyproject.toml-for-packaging", + "href": "explains/faq.html#why-not-pyproject.toml-for-packaging", + "title": "FAQ", + "section": "Why not pyproject.toml for packaging?", + "text": "Why not pyproject.toml for packaging?\nFastHTML uses a setup.py module instead of a pyproject.toml file to configure itself for installation. The reason for this is pyproject.toml is not compatible with nbdev, which is what is used to write and build FastHTML.\nThe nbdev project spent around a year trying to move to pyproject.toml but there was insufficient functionality in the toml-based approach to complete the transition.", + "crumbs": [ + "Home", + "Explanations", + "FAQ" + ] + }, + { + "objectID": "unpublished/tutorial_for_web_devs.html", + "href": "unpublished/tutorial_for_web_devs.html", + "title": "BYO Blog", + "section": "", + "text": "Caution\n\n\n\nThis document is a work in progress.\nIn this tutorial we’re going to write a blog by example. Blogs are a good way to learn a web framework as they start simple yet can get surprisingly sophistated. The wikipedia definition of a blog is “an informational website consisting of discrete, often informal diary-style text entries (posts) informal diary-style text entries (posts)”, which means we need to provide these basic features:\nWe’ll also add in these features, so the blog can become a working site:" + }, + { + "objectID": "unpublished/tutorial_for_web_devs.html#how-to-best-use-this-tutorial", + "href": "unpublished/tutorial_for_web_devs.html#how-to-best-use-this-tutorial", + "title": "BYO Blog", + "section": "How to best use this tutorial", + "text": "How to best use this tutorial\nWe could copy/paste every code example in sequence and have a finished blog at the end. However, it’s debatable how much we will learn through the copy/paste method. We’re not saying its impossible to learn through copy/paste, we’re just saying it’s not that of an efficient way to learn. It’s analogous to learning how to play a musical instrument or sport or video game by watching other people do it - you can learn some but its not the same as doing.\nA better approach is to type out every line of code in this tutorial. This forces us to run the code through our brains, giving us actual practice in how to write FastHTML and Pythoncode and forcing us to debug our own mistakes. In some cases we’ll repeat similar tasks - a key component in achieving mastery in anything. Coming back to the instrument/sport/video game analogy, it’s exactly like actually practicing an instrument, sport, or video game. Through practice and repetition we eventually achieve mastery." + }, + { + "objectID": "unpublished/tutorial_for_web_devs.html#installing-fasthtml", + "href": "unpublished/tutorial_for_web_devs.html#installing-fasthtml", + "title": "BYO Blog", + "section": "Installing FastHTML", + "text": "Installing FastHTML\nFastHTML is just Python. Installation is often done with pip:\npip install python-fasthtml" + }, + { + "objectID": "unpublished/tutorial_for_web_devs.html#a-minimal-fasthtml-app", + "href": "unpublished/tutorial_for_web_devs.html#a-minimal-fasthtml-app", + "title": "BYO Blog", + "section": "A minimal FastHTML app", + "text": "A minimal FastHTML app\nFirst, create the directory for our project using Python’s pathlib module:\nimport pathlib\npathlib.Path('blog-system').mkdir()\nNow that we have our directory, let’s create a minimal FastHTML site in it.\n\n\nblog-system/minimal.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app() \n\n@rt(\"/\") \ndef get():\n return Titled(\"FastHTML\", P(\"Let's do this!\")) \n\nserve()\n\nRun that with python minimal.py and you should get something like this:\npython minimal.py \nLink: http://localhost:5001\nINFO: Will watch for changes in these directories: ['/Users/pydanny/projects/blog-system']\nINFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)\nINFO: Started reloader process [46572] using WatchFiles\nINFO: Started server process [46576]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nConfirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:\n\n\n\n\n\n\n\nWhat about the import *?\n\n\n\nFor those worried about the use of import * rather than a PEP8-style declared namespace, understand that __all__ is defined in FastHTML’s common module. That means that only the symbols (functions, classes, and other things) the framework wants us to have will be brought into our own code via import *. Read importing from a package) for more information.\nNevertheless, if we want to use a defined namespace we can do so. Here’s an example:\nfrom fasthtml import common as fh\n\n\napp, rt = fh.fast_app() \n\n@rt(\"/\") \ndef get():\n return fh.Titled(\"FastHTML\", fh.P(\"Let's do this!\")) \n\nfh.serve()" + }, + { + "objectID": "unpublished/tutorial_for_web_devs.html#looking-more-closely-at-our-app", + "href": "unpublished/tutorial_for_web_devs.html#looking-more-closely-at-our-app", + "title": "BYO Blog", + "section": "Looking more closely at our app", + "text": "Looking more closely at our app\nLet’s look more closely at our application. Every line is packed with powerful features of FastHTML:\n\n\nblog-system/minimal.py\n\n1from fasthtml.common import *\n\n2app, rt = fast_app()\n\n3@rt(\"/\")\n4def get():\n5 return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n6serve()\n\n\n1\n\nThe top level namespace of Fast HTML (fasthtml.common) contains everything we need from FastHTML to build applications. A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience.\n\n2\n\nWe instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll modify or take advantage of later in the tutorial.\n\n3\n\nWe use the rt() decorator to tell FastHTML what to return when a user visits / in their browser.\n\n4\n\nWe connect this route to HTTP GET requests by defining a view function called get().\n\n5\n\nA tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach.\n\n6\n\nThe serve() utility configures and runs FastHTML using a library called uvicorn. Any changes to this module will be reloaded into the browser." + }, + { + "objectID": "unpublished/tutorial_for_web_devs.html#adding-dynamic-content-to-our-minimal-app", + "href": "unpublished/tutorial_for_web_devs.html#adding-dynamic-content-to-our-minimal-app", + "title": "BYO Blog", + "section": "Adding dynamic content to our minimal app", + "text": "Adding dynamic content to our minimal app\nOur page is great, but we’ll make it better. Let’s add a randomized list of letters to the page. Every time the page reloads, a new list of varying length will be generated.\n\n\nblog-system/random_letters.py\n\nfrom fasthtml.common import *\n1import string, random\n\napp, rt = fast_app()\n\n@rt(\"/\")\ndef get():\n2 letters = random.choices(string.ascii_uppercase, k=random.randint(5, 20))\n3 items = [Li(c) for c in letters]\n return Titled(\"Random lists of letters\",\n4 Ul(*items)\n ) \n\nserve()\n\n\n1\n\nThe string and random libraries are part of Python’s standard library\n\n2\n\nWe use these libraries to generate a random length list of random letters called letters\n\n3\n\nUsing letters as the base we use list comprehension to generate a list of Li ft display components, each with their own letter and save that to the variable items\n\n4\n\nInside a call to the Ul() ft component we use Python’s *args special syntax on the items variable. Therefore *list is treated not as one argument but rather a set of them.\n\n\nWhen this is run, it will generate something like this with a different random list of letters for each page load:" + }, + { + "objectID": "unpublished/tutorial_for_web_devs.html#storing-the-articles", + "href": "unpublished/tutorial_for_web_devs.html#storing-the-articles", + "title": "BYO Blog", + "section": "Storing the articles", + "text": "Storing the articles\nThe most basic component of a blog is a series of articles sorted by date authored. Rather than a database we’re going to use our computer’s harddrive to store a set of markdown files in a directory within our blog called posts. First, let’s create the directory and some test files we can use to search for:\n\nfrom fastcore.utils import *\n\n\n# Create some dummy posts\nposts = Path(\"posts\")\nposts.mkdir(exist_ok=True)\nfor i in range(10): (posts/f\"article_{i}.md\").write_text(f\"This is article {i}\")\n\nSearching for these files can be done with pathlib.\n\nimport pathlib\nposts.ls()\n\n(#10) [Path('posts/article_5.md'),Path('posts/article_1.md'),Path('posts/article_0.md'),Path('posts/article_4.md'),Path('posts/article_3.md'),Path('posts/article_7.md'),Path('posts/article_6.md'),Path('posts/article_2.md'),Path('posts/article_9.md'),Path('posts/article_8.md')]\n\n\n\n\n\n\n\n\nTip\n\n\n\nPython’s pathlib library is quite useful and makes file search and manipulation much easier. There’s many uses for it and is compatible across operating systems." + }, + { + "objectID": "unpublished/tutorial_for_web_devs.html#creating-the-blog-home-page", + "href": "unpublished/tutorial_for_web_devs.html#creating-the-blog-home-page", + "title": "BYO Blog", + "section": "Creating the blog home page", + "text": "Creating the blog home page\nWe now have enough tools that we can create the home page. Let’s create a new Python file and write out our simple view to list the articles in our blog.\n\n\nblog-system/main.py\n\nfrom fasthtml.common import *\nimport pathlib\n\napp, rt = fast_app()\n\n@rt(\"/\")\ndef get():\n fnames = pathlib.Path(\"posts\").rglob(\"*.md\")\n items = [Li(A(fname, href=fname)) for fname in fnames] \n return Titled(\"My Blog\",\n Ul(*items)\n ) \n\nserve()\n\n\nfor p in posts.ls(): p.unlink()" + }, + { + "objectID": "ref/response_types.html", + "href": "ref/response_types.html", + "title": "Response Types", + "section": "", + "text": "FastHTML provides multiple HTTP response types that automatically set the appropriate HTTP content type and handle serialization. The main response types are:", + "crumbs": [ + "Home", + "Reference", + "Response Types" + ] + }, + { + "objectID": "ref/response_types.html#configuration", + "href": "ref/response_types.html#configuration", + "title": "Response Types", + "section": "Configuration", + "text": "Configuration\n\nfrom fasthtml.common import *\n\n\napp,rt = fast_app()\n\napp and rt are the common FastHTML route handler decorators. We instantiate them with the fast_app function.\n\ncli = Client(app)\n\nFastHTML comes with the test client named Client. It allows us to test handlers via a simple interface where .get() is a HTTP GET request, .post() is a HTTP POST request.", + "crumbs": [ + "Home", + "Reference", + "Response Types" + ] + }, + { + "objectID": "ref/response_types.html#ft-component-response", + "href": "ref/response_types.html#ft-component-response", + "title": "Response Types", + "section": "FT Component Response", + "text": "FT Component Response\n\n@rt('/ft')\ndef get(): return Html(Div('FT Component Response'))\n\nThis is the response type you’re probably most familiar with. Here the route handler returns an FT component, which FastHTML wraps in an HTML document with a head and body.\n\nprint(cli.get('/ft').text)\n\n <!doctype html>\n <html>\n <div>FT Component Response</div>\n </html>", + "crumbs": [ + "Home", + "Reference", + "Response Types" + ] + }, + { + "objectID": "ref/response_types.html#redirect-response", + "href": "ref/response_types.html#redirect-response", + "title": "Response Types", + "section": "Redirect Response", + "text": "Redirect Response\n\n@rt('/rr')\ndef get(): return Redirect('https://fastht.ml/')\n\nHere in this route handler, Redirect redirects the user’s browser to the new URL ‘https://fastht.ml/’\n\nresp = cli.get('/rr')\nprint(resp.url)\nprint(resp.status_code)\n\nhttp://testserver/rr\n303\n\n\nYou can see the URL in the response headers and url attribute, as well as a status code of 303.", + "crumbs": [ + "Home", + "Reference", + "Response Types" + ] + }, + { + "objectID": "ref/response_types.html#json-response", + "href": "ref/response_types.html#json-response", + "title": "Response Types", + "section": "JSON Response", + "text": "JSON Response\n\n@rt('/json')\ndef get(): return {'hello': 'world'}\n\nThis route handler returns a JSON response, where the content-type has been set to .\n\nresp = cli.get('/json')\nprint(resp.headers)\nprint(resp.json())\n\nHeaders({'content-length': '17', 'content-type': 'application/json'})\n{'hello': 'world'}\n\n\nYou can see that the Content-Type header has been set to application/json, and that the response is simply the JSON without any HTML wrapping it.", + "crumbs": [ + "Home", + "Reference", + "Response Types" + ] + }, + { + "objectID": "ref/response_types.html#eventstream", + "href": "ref/response_types.html#eventstream", + "title": "Response Types", + "section": "EventStream", + "text": "EventStream\n\nfrom time import sleep\n\ndef counter():\n \"\"\"Counter is an generator that\n publishes a number every second.\n \"\"\"\n for i in range(3):\n yield sse_message(f\"Event {i}\")\n sleep(1)\n\n@rt('/stream')\ndef get():\n return EventStream(counter())\n\nWith server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. Unlike WebSockets, SSE can only go in one direction: server to client. SSE is also part of the HTTP specification unlike WebSockets which uses its own specification.\n\nresp = cli.get('/stream')\nprint(resp.text)\n\nevent: message\ndata: Event 0\n\nevent: message\ndata: Event 1\n\nevent: message\ndata: Event 2\n\n\n\n\nEach one of the message events above arrived one second after the previous message event.", + "crumbs": [ + "Home", + "Reference", + "Response Types" + ] + }, + { + "objectID": "ref/response_types.html#plaintext-response", + "href": "ref/response_types.html#plaintext-response", + "title": "Response Types", + "section": "Plaintext Response", + "text": "Plaintext Response\n\n@rt('/text')\ndef get(): return 'Hello world'\n\nWhen you return a string from a route handler, you get a plain-text response.\n\nprint(cli.get('/text').text)\n\nHello world\n\n\nHere you can see that the response text is simply the string you returned, without any HTML wrapping it.", + "crumbs": [ + "Home", + "Reference", + "Response Types" + ] + }, + { + "objectID": "ref/live_reload.html", + "href": "ref/live_reload.html", + "title": "Live Reloading", + "section": "", + "text": "When building your app it can be useful to view your changes in a web browser as you make them. FastHTML supports live reloading which means that it watches for any changes to your code and automatically refreshes the webpage in your browser.\nTo enable live reloading simply replace FastHTML in your app with FastHTMLWithLiveReload.\nThen in your terminal run uvicorn with reloading enabled.\n⚠️ Gotchas - A reload is only triggered when you save your changes. - FastHTMLWithLiveReload should only be used during development. - If your app spans multiple directories you might need to use the --reload-dir flag to watch all files in each directory. See the uvicorn docs for more info. - The live reload script is only injected into the page when rendering ft components.", + "crumbs": [ + "Home", + "Reference", + "Live Reloading" + ] + }, + { + "objectID": "ref/live_reload.html#live-reloading-with-fast_app", + "href": "ref/live_reload.html#live-reloading-with-fast_app", + "title": "Live Reloading", + "section": "Live reloading with fast_app", + "text": "Live reloading with fast_app\nIn development the fast_app function provides the same functionality. It instantiates the FastHTMLWithLiveReload class if you pass live=True:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\n1app, rt = fast_app(live=True)\n\n2serve()\n\n\n1\n\nfast_app() instantiates the FastHTMLWithLiveReload class.\n\n2\n\nserve() is a wrapper around a uvicorn call.\n\n\nTo run main.py in live reload mode, just do python main.py. We recommend turning off live reload when deploying your app to production.", + "crumbs": [ + "Home", + "Reference", + "Live Reloading" + ] + }, + { + "objectID": "ref/best_practice.html", + "href": "ref/best_practice.html", + "title": "FastHTML Best Practices", + "section": "", + "text": "FastHTML applications are different to applications using FastAPI/react, Django, etc. Don’t assume that FastHTML best practices are the same as those for other frameworks. Best practices embody the fast.ai philosophy: remove ceremony, leverage smart defaults, and write code that’s both concise and clear. The following are some particular opportunities that both humans and language models sometimes miss:", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#database-table-creation", + "href": "ref/best_practice.html#database-table-creation", + "title": "FastHTML Best Practices", + "section": "Database Table Creation", + "text": "Database Table Creation\nBefore:\ntodos = db.t.todos\nif not todos.exists():\ntodos.create(id=int, task=str, completed=bool, created=str, pk='id')\nAfter:\nclass Todo: id:int; task:str; completed:bool; created:str\ntodos = db.create(Todo)\nFastLite’s create() is idempotent - it creates the table if needed and returns the table object either way. Using a dataclass-style definition is cleaner and more Pythonic. The id field is automatically the primary key.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#route-naming-conventions", + "href": "ref/best_practice.html#route-naming-conventions", + "title": "FastHTML Best Practices", + "section": "Route Naming Conventions", + "text": "Route Naming Conventions\nBefore:\n@rt(\"/\")\ndef get(): return Titled(\"Todo List\", ...)\n\n@rt(\"/add\")\ndef post(task: str): ...\nAfter:\n@rt\ndef index(): return Titled(\"Todo List\", ...) # Special name for \"/\"\n@rt\ndef add(task: str): ... # Function name becomes route\nUse @rt without arguments and let the function name define the route. The special name index maps to /.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#query-parameters-over-path-parameters", + "href": "ref/best_practice.html#query-parameters-over-path-parameters", + "title": "FastHTML Best Practices", + "section": "Query Parameters over Path Parameters", + "text": "Query Parameters over Path Parameters\nBefore:\n@rt(\"/toggle/{todo_id}\")\ndef post(todo_id: int): ...\n# URL: /toggle/123\nAfter:\n@rt\ndef toggle(id: int): ...\n# URL: /toggle?id=123\nQuery parameters are more idiomatic in FastHTML and avoid duplicating param names in the path.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#leverage-return-values", + "href": "ref/best_practice.html#leverage-return-values", + "title": "FastHTML Best Practices", + "section": "Leverage Return Values", + "text": "Leverage Return Values\n\nBefore:\n@rt\ndef add(task: str):\n new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat())\n return todo_item(todos[new_todo])\n\n@rt\ndef toggle(id: int):\n todo = todos[id]\n todos.update(completed=not todo.completed, id=id)\n return todo_item(todos[id])\nAfter:\n@rt\ndef add(task: str):\n return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat()))\n\n@rt\ndef toggle(id: int):\n return todo_item(todos.update(completed=not todos[id].completed, id=id))\nBoth insert() and update() return the affected object, enabling functional chaining.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#use-.to-for-url-generation", + "href": "ref/best_practice.html#use-.to-for-url-generation", + "title": "FastHTML Best Practices", + "section": "Use .to() for URL Generation", + "text": "Use .to() for URL Generation\nBefore:\nhx_post=f\"/toggle?id={todo.id}\"\nAfter:\nhx_post=toggle.to(id=todo.id)\nThe .to() method generates URLs with type safety and is refactoring-friendly.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#picocss-comes-free", + "href": "ref/best_practice.html#picocss-comes-free", + "title": "FastHTML Best Practices", + "section": "PicoCSS comes free", + "text": "PicoCSS comes free\nBefore:\nstyle = Style(\"\"\"\n.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; }\n/* ... many more lines ... */\n\"\"\")\nAfter:\n# Just use semantic HTML - Pico styles it automatically\nContainer(...), Article(...), Card(...), Group(...)\nfast_app() includes PicoCSS by default. Use semantic HTML elements that Pico styles automatically. Use MonsterUI (like shadcn, but for FastHTML) for more complex UI needs.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#smart-defaults", + "href": "ref/best_practice.html#smart-defaults", + "title": "FastHTML Best Practices", + "section": "Smart Defaults", + "text": "Smart Defaults\nBefore:\nreturn Titled(\"Todo List\", Container(...))\n\nif __name__ == \"__main__\":\n serve()\nAfter:\nreturn Titled(\"Todo List\", ...) # Container is automatic\n\nserve() # No need for if __name__ guard\nTitled already wraps content in a Container, and serve() handles the main check internally.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#fasthtml-handles-iterables", + "href": "ref/best_practice.html#fasthtml-handles-iterables", + "title": "FastHTML Best Practices", + "section": "FastHTML Handles Iterables", + "text": "FastHTML Handles Iterables\nBefore:\nSection(*[todo_item(todo) for todo in all_todos], id=\"todo-list\")\nAfter:\nSection(map(todo_item, all_todos), id=\"todo-list\")\nFastHTML components accept iterables directly - no need to unpack with *.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#functional-patterns", + "href": "ref/best_practice.html#functional-patterns", + "title": "FastHTML Best Practices", + "section": "Functional Patterns", + "text": "Functional Patterns\nList comprehensions are great, but map() is often cleaner for simple transformations, especially when combined with FastHTML’s iterable handling.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#minimal-code", + "href": "ref/best_practice.html#minimal-code", + "title": "FastHTML Best Practices", + "section": "Minimal Code", + "text": "Minimal Code\nBefore:\n@rt\ndef delete(id: int):\n # Delete from database\n todos.delete(id)\n # Return empty response\n return \"\"\nAfter:\n@rt\ndef delete(id: int): todos.delete(id)\n\nSkip comments when code is self-documenting\nDon’t return empty strings - None is returned by default\nUse a single line for a single idea.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#use-post-for-all-mutations", + "href": "ref/best_practice.html#use-post-for-all-mutations", + "title": "FastHTML Best Practices", + "section": "Use POST for All Mutations", + "text": "Use POST for All Mutations\nBefore:\nhx_delete=f\"/delete?id={todo.id}\"\nAfter:\nhx_post=delete.to(id=todo.id)\nFastHTML routes handle only GET and POST by default. Using only these two verbs is more idiomatic and simpler.", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/best_practice.html#modern-htmx-event-syntax", + "href": "ref/best_practice.html#modern-htmx-event-syntax", + "title": "FastHTML Best Practices", + "section": "Modern HTMX Event Syntax", + "text": "Modern HTMX Event Syntax\nBefore:\nhx_on=\"htmx:afterRequest: this.reset()\"\nAfter:\nhx_on__after_request=\"this.reset()\"\nThis works because:\n\nhx-on=\"event: code\" is deprecated; hx-on-event=\"code\" is preferred\nFastHTML converts _ to - (so hx_on__after_request becomes hx-on--after-request)\n:: in HTMX can be used as a shortcut for :htmx:.\nHTMX natively accepts - instead of : (so -htmx- works like :htmx:)\nHTMX accepts e.g after-request as an alternative to camelCase afterRequest", + "crumbs": [ + "Home", + "Reference", + "FastHTML Best Practices" + ] + }, + { + "objectID": "ref/concise_guide.html", + "href": "ref/concise_guide.html", + "title": "Concise reference", + "section": "", + "text": "from fasthtml.common import *\n\nFastHTML is a python library which brings together Starlette, Uvicorn, HTMX, and fastcore’s FT “FastTags” into a library for creating server-rendered hypermedia applications. The FastHTML class itself inherits from Starlette, and adds decorator-based routing with many additions, Beforeware, automatic FT to HTML rendering, and much more.\nThings to remember when writing FastHTML apps:\n\nNot compatible with FastAPI syntax; FastHTML is for HTML-first apps, not API services (although it can implement APIs too)\nFastHTML includes support for Pico CSS and the fastlite sqlite library, although using both are optional; sqlalchemy can be used directly or via the fastsql library, and any CSS framework can be used. MonsterUI is a richer FastHTML-first component framework with similar capabilities to shadcn\nFastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte\nUse serve() for running uvicorn (if __name__ == \"__main__\" is not needed since it’s automatic)\nWhen a title is needed with a response, use Titled; note that that already wraps children in Container, and already includes both the meta title as well as the H1 element.", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#about-fasthtml", + "href": "ref/concise_guide.html#about-fasthtml", + "title": "Concise reference", + "section": "", + "text": "from fasthtml.common import *\n\nFastHTML is a python library which brings together Starlette, Uvicorn, HTMX, and fastcore’s FT “FastTags” into a library for creating server-rendered hypermedia applications. The FastHTML class itself inherits from Starlette, and adds decorator-based routing with many additions, Beforeware, automatic FT to HTML rendering, and much more.\nThings to remember when writing FastHTML apps:\n\nNot compatible with FastAPI syntax; FastHTML is for HTML-first apps, not API services (although it can implement APIs too)\nFastHTML includes support for Pico CSS and the fastlite sqlite library, although using both are optional; sqlalchemy can be used directly or via the fastsql library, and any CSS framework can be used. MonsterUI is a richer FastHTML-first component framework with similar capabilities to shadcn\nFastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte\nUse serve() for running uvicorn (if __name__ == \"__main__\" is not needed since it’s automatic)\nWhen a title is needed with a response, use Titled; note that that already wraps children in Container, and already includes both the meta title as well as the H1 element.", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#minimal-app", + "href": "ref/concise_guide.html#minimal-app", + "title": "Concise reference", + "section": "Minimal App", + "text": "Minimal App\nThe code examples here use fast.ai style: prefer ternary op, 1-line docstring, minimize vertical space, etc. (Normally fast.ai style uses few if any comments, but they’re added here as documentation.)\nA minimal FastHTML app looks something like this:\n# Meta-package with all key symbols from FastHTML and Starlette. Import it like this at the start of every FastHTML app.\nfrom fasthtml.common import *\n# The FastHTML app object and shortcut to `app.route`\napp,rt = fast_app()\n\n# Enums constrain the values accepted for a route parameter\nname = str_enum('names', 'Alice', 'Bev', 'Charlie')\n\n# Passing a path to `rt` is optional. If not passed (recommended), the function name is the route ('/foo')\n# Both GET and POST HTTP methods are handled by default\n# Type-annotated params are passed as query params (recommended) unless a path param is defined (which it isn't here)\n@rt\ndef foo(nm: name):\n # `Title` and `P` here are FastTags: direct m-expression mappings of HTML tags to Python functions with positional and named parameters. All standard HTML tags are included in the common wildcard import.\n # When a tuple is returned, this returns concatenated HTML partials. HTMX by default will use a title HTML partial to set the current page name. HEAD tags (e.g. Meta, Link, etc) in the returned tuple are automatically placed in HEAD; everything else is placed in BODY.\n # FastHTML will automatically return a complete HTML document with appropriate headers if a normal HTTP request is received. For an HTMX request, however, just the partials are returned.\n return Title(\"FastHTML\"), H1(\"My web app\"), P(f\"Hello, {name}!\")\n# By default `serve` runs uvicorn on port 5001. Never write `if __name__ == \"__main__\"` since `serve` checks it internally.\nserve()\nTo run this web app:\npython main.py # access via localhost:5001", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#fasttags-aka-ft-components-or-fts", + "href": "ref/concise_guide.html#fasttags-aka-ft-components-or-fts", + "title": "Concise reference", + "section": "FastTags (aka FT Components or FTs)", + "text": "FastTags (aka FT Components or FTs)\nFTs are m-expressions plus simple sugar. Positional params map to children. Named parameters map to attributes. Aliases must be used for Python reserved words.\n\ntags = Title(\"FastHTML\"), H1(\"My web app\"), P(f\"Let's do this!\", cls=\"myclass\")\ntags\n\n(title(('FastHTML',),{}),\n h1(('My web app',),{}),\n p((\"Let's do this!\",),{'class': 'myclass'}))\n\n\nThis example shows key aspects of how FTs handle attributes:\nLabel(\n \"Choose an option\", \n Select(\n Option(\"one\", value=\"1\", selected=True), # True renders just the attribute name\n Option(\"two\", value=2, selected=False), # Non-string values are converted to strings. False omits the attribute entirely\n cls=\"selector\", id=\"counter\", # 'cls' becomes 'class'\n **{'@click':\"alert('Clicked');\"}, # Dict unpacking for attributes with special chars\n ),\n _for=\"counter\", # '_for' becomes 'for' (can also use 'fr')\n)\nClasses with __ft__ defined are rendered using that method.\n\nclass FtTest:\n def __ft__(self): return P('test')\n \nto_xml(FtTest())\n\n'<p>test</p>\\n'\n\n\nYou can create new FTs by importing the new component from fasthtml.components. If the FT doesn’t exist within that module, FastHTML will create it.\n\nfrom fasthtml.components import Some_never_before_used_tag\n\nSome_never_before_used_tag()\n\n<some-never-before-used-tag></some-never-before-used-tag>\n\n\nFTs can be combined by defining them as a function.\n\ndef Hero(title, statement): return Div(H1(title),P(statement), cls=\"hero\")\nto_xml(Hero(\"Hello World\", \"This is a hero statement\"))\n\n'<div class=\"hero\">\\n <h1>Hello World</h1>\\n <p>This is a hero statement</p>\\n</div>\\n'\n\n\nWhen handling a response, FastHTML will automatically render FTs using the to_xml function.\n\nto_xml(tags)\n\n'<title>FastHTML</title>\\n<h1>My web app</h1>\\n<p class=\"myclass\">Let's do this!</p>\\n'", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#js", + "href": "ref/concise_guide.html#js", + "title": "Concise reference", + "section": "JS", + "text": "JS\nThe Script function allows you to include JavaScript. You can use Python to generate parts of your JS or JSON like this:\n# In future snippets this import will not be shown, but is required\nfrom fasthtml.common import * \napp,rt = fast_app(hdrs=[Script(src=\"https://cdn.plot.ly/plotly-2.32.0.min.js\")])\n# `index` is a special function name which maps to the `/` route. \n@rt\ndef index():\n data = {'somedata':'fill me in…'}\n # `Titled` returns a title tag and an h1 tag with the 1st param, with remaining params as children in a `Main` parent.\n return Titled(\"Chart Demo\", Div(id=\"myDiv\"), Script(f\"var data = {data}; Plotly.newPlot('myDiv', data);\"))\n# In future snippets `serve() will not be shown, but is required\nserve()\nPrefer Python whenever possible over JS. Never use React or shadcn.", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#fast_app-hdrs", + "href": "ref/concise_guide.html#fast_app-hdrs", + "title": "Concise reference", + "section": "fast_app hdrs", + "text": "fast_app hdrs\n# In future snippets we'll skip showing the `fast_app` call if it has no params\napp, rt = fast_app(\n pico=False, # The Pico CSS framework is included by default, so pass `False` to disable it if needed. No other CSS frameworks are included.\n # These are added to the `head` part of the page for non-HTMX requests.\n hdrs=(\n Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),\n Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),\n Style(\"p {color: red;}\"),\n # `MarkdownJS` and `HighlightJS` are available via concise functions\n MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']),\n # by default, all standard static extensions are served statically from the web app dir,\n # which can be modified using e.g `static_path='public'`\n )\n)\n\n@rt\ndef index(req): return Titled(\"Markdown rendering example\",\n # This will be client-side rendered to HTML with highlight-js\n Div(\"*hi* there\",cls=\"marked\"),\n # This will be syntax highlighted\n Pre(Code(\"def foo(): pass\")))", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#responses", + "href": "ref/concise_guide.html#responses", + "title": "Concise reference", + "section": "Responses", + "text": "Responses\nRoutes can return various types:\n\nFastTags or tuples of FastTags (automatically rendered to HTML)\nStandard Starlette responses (used directly)\nJSON-serializable types (returned as JSON in a plain text response)\n\n@rt(\"/{fname:path}.{ext:static}\")\nasync def serve_static_file(fname:str, ext:str): return FileResponse(f'public/{fname}.{ext}')\n\napp, rt = fast_app(hdrs=(MarkdownJS(), HighlightJS(langs=['python', 'javascript'])))\n@rt\ndef index(): \n return Titled(\"Example\",\n Div(\"*markdown* here\", cls=\"marked\"),\n Pre(Code(\"def foo(): pass\")))\nRoute functions can be used in attributes like href or action and will be converted to paths. Use .to() to generate paths with query parameters.\n@rt\ndef profile(email:str): return fill_form(profile_form, profiles[email])\n\nprofile_form = Form(action=profile)(\n Label(\"Email\", Input(name=\"email\")),\n Button(\"Save\", type=\"submit\")\n)\n\nuser_profile_path = profile.to(email=\"user@example.com\") # '/profile?email=user%40example.com'\n\nfrom dataclasses import dataclass\n\napp,rt = fast_app()\n\nWhen a route handler function is used as a fasttag attribute (such as href, hx_get, or action) it is converted to that route’s path. fill_form is used to copy an object’s matching attrs into matching-name form fields.\n\n@dataclass\nclass Profile: email:str; phone:str; age:int\nemail = 'john@example.com'\nprofiles = {email: Profile(email=email, phone='123456789', age=5)}\n@rt\ndef profile(email:str): return fill_form(profile_form, profiles[email])\n\nprofile_form = Form(method=\"post\", action=profile)(\n Fieldset(\n Label('Email', Input(name=\"email\")),\n Label(\"Phone\", Input(name=\"phone\")),\n Label(\"Age\", Input(name=\"age\"))),\n Button(\"Save\", type=\"submit\"))", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#testing", + "href": "ref/concise_guide.html#testing", + "title": "Concise reference", + "section": "Testing", + "text": "Testing\nWe can use TestClient for testing.\n\nfrom starlette.testclient import TestClient\n\n\npath = \"/profile?email=john@example.com\"\nclient = TestClient(app)\nhtmx_req = {'HX-Request':'1'}\nprint(client.get(path, headers=htmx_req).text)\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"123456789\">\n</label><label>Age <input name=\"age\" value=\"5\">\n</label></fieldset><button type=\"submit\">Save</button></form>", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#form-handling-and-data-binding", + "href": "ref/concise_guide.html#form-handling-and-data-binding", + "title": "Concise reference", + "section": "Form Handling and Data Binding", + "text": "Form Handling and Data Binding\nWhen a dataclass, namedtuple, etc. is used as a type annotation, the form body will be unpacked into matching attribute names automatically.\n\n@rt\ndef edit_profile(profile: Profile):\n profiles[email]=profile\n return RedirectResponse(url=path)\n\nnew_data = dict(email='john@example.com', phone='7654321', age=25)\nprint(client.post(\"/edit_profile\", data=new_data, headers=htmx_req).text)\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"7654321\">\n</label><label>Age <input name=\"age\" value=\"25\">\n</label></fieldset><button type=\"submit\">Save</button></form>", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#fasttag-rendering-rules", + "href": "ref/concise_guide.html#fasttag-rendering-rules", + "title": "Concise reference", + "section": "fasttag Rendering Rules", + "text": "fasttag Rendering Rules\nThe general rules for rendering children inside tuples or fasttag children are: - __ft__ method will be called (for default components like P, H2, etc. or if you define your own components) - If you pass a string, it will be escaped - On other python objects, str() will be called\nIf you want to include plain HTML tags directly into e.g. a Div() they will get escaped by default (as a security measure to avoid code injections). This can be avoided by using Safe(...), e.g to show a data frame use Div(NotStr(df.to_html())).", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#exceptions", + "href": "ref/concise_guide.html#exceptions", + "title": "Concise reference", + "section": "Exceptions", + "text": "Exceptions\nFastHTML allows customization of exception handlers.\ndef not_found(req, exc): return Titled(\"404: I don't exist!\")\nexception_handlers = {404: not_found}\napp, rt = fast_app(exception_handlers=exception_handlers)", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#cookies", + "href": "ref/concise_guide.html#cookies", + "title": "Concise reference", + "section": "Cookies", + "text": "Cookies\nWe can set cookies using the cookie() function.\n\n@rt\ndef setcook(): return P(f'Set'), cookie('mycookie', 'foobar')\nprint(client.get('/setcook', headers=htmx_req).text)\n\n <p>Set</p>\n\n\n\n\n@rt\ndef getcook(mycookie:str): return f'Got {mycookie}'\n# If handlers return text instead of FTs, then a plaintext response is automatically created\nprint(client.get('/getcook').text)\n\nGot foobar\n\n\nFastHTML provide access to Starlette’s request object automatically using special request parameter name (or any prefix of that name).\n\n@rt\ndef headers(req): return req.headers['host']", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#request-and-session-objects", + "href": "ref/concise_guide.html#request-and-session-objects", + "title": "Concise reference", + "section": "Request and Session Objects", + "text": "Request and Session Objects\nFastHTML provides access to Starlette’s session middleware automatically using the special session parameter name (or any prefix of that name).\n\n@rt\ndef profile(req, sess, user_id: int=None):\n ip = req.client.host\n sess['last_visit'] = datetime.now().isoformat()\n visits = sess.setdefault('visit_count', 0) + 1\n sess['visit_count'] = visits\n user = get_user(user_id or sess.get('user_id'))\n return Titled(f\"Profile: {user.name}\", \n P(f\"Visits: {visits}\"), \n P(f\"IP: {ip}\"),\n Button(\"Logout\", hx_post=logout))\n\nHandler functions can return the HtmxResponseHeaders object to set HTMX-specific response headers.\n\n@rt\ndef htmlredirect(app): return HtmxResponseHeaders(location=\"http://example.org\")", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#apirouter", + "href": "ref/concise_guide.html#apirouter", + "title": "Concise reference", + "section": "APIRouter", + "text": "APIRouter\nAPIRouter lets you organize routes across multiple files in a FastHTML app.\n# products.py\nar = APIRouter()\n\n@ar\ndef details(pid: int): return f\"Here are the product details for ID: {pid}\"\n\n@ar\ndef all_products(req):\n return Div(\n Div(\n Button(\"Details\",hx_get=details.to(pid=42),hx_target=\"#products_list\",hx_swap=\"outerHTML\",),\n ), id=\"products_list\")\n# main.py\nfrom products import ar,all_products\n\napp, rt = fast_app()\nar.to_app(app)\n\n@rt\ndef index():\n return Div(\n \"Products\",\n hx_get=all_products, hx_swap=\"outerHTML\")", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#toasts", + "href": "ref/concise_guide.html#toasts", + "title": "Concise reference", + "section": "Toasts", + "text": "Toasts\nToasts can be of four types:\n\ninfo\nsuccess\nwarning\nerror\n\nToasts require the use of the setup_toasts() function, plus every handler needs:\n\nThe session argument\nMust return FT components\n\nsetup_toasts(app)\n\n@rt\ndef toasting(session):\n add_toast(session, f\"cooked\", \"info\")\n add_toast(session, f\"ready\", \"success\")\n return Titled(\"toaster\")\nsetup_toasts(duration) allows you to specify how long a toast will be visible before disappearing.10 seconds.\nAuthentication and authorization are handled with Beforeware, which functions that run before the route handler is called.", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#auth", + "href": "ref/concise_guide.html#auth", + "title": "Concise reference", + "section": "Auth", + "text": "Auth\n\ndef user_auth_before(req, sess):\n # `auth` key in the request scope is automatically provided to any handler which requests it and can not be injected\n auth = req.scope['auth'] = sess.get('auth', None)\n if not auth: return RedirectResponse('/login', status_code=303)\n\nbeforeware = Beforeware(\n user_auth_before,\n skip=[r'/favicon\\.ico', r'/static/.*', r'.*\\.css', r'.*\\.js', '/login', '/']\n)\n\napp, rt = fast_app(before=beforeware)", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#server-side-events-sse", + "href": "ref/concise_guide.html#server-side-events-sse", + "title": "Concise reference", + "section": "Server-Side Events (SSE)", + "text": "Server-Side Events (SSE)\nFastHTML supports the HTMX SSE extension.\n\nimport random\nhdrs=(Script(src=\"https://unpkg.com/htmx-ext-sse@2.2.3/sse.js\"),)\napp,rt = fast_app(hdrs=hdrs)\n\n@rt\ndef index(): return Div(hx_ext=\"sse\", sse_connect=\"/numstream\", hx_swap=\"beforeend show:bottom\", sse_swap=\"message\")\n\n# `signal_shutdown()` gets an event that is set on shutdown\nshutdown_event = signal_shutdown()\n\nasync def number_generator():\n while not shutdown_event.is_set():\n data = Article(random.randint(1, 100))\n yield sse_message(data)\n\n@rt\nasync def numstream(): return EventStream(number_generator())", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#websockets", + "href": "ref/concise_guide.html#websockets", + "title": "Concise reference", + "section": "Websockets", + "text": "Websockets\nFastHTML provides useful tools for HTMX’s websockets extension.\n\n# These HTMX extensions are available through `exts`:\n# head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer\napp, rt = fast_app(exts='ws')\n\ndef mk_inp(): return Input(id='msg', autofocus=True)\n\n@rt\nasync def index(request):\n # `ws_send` tells HTMX to send a message to the nearest websocket based on the trigger for the form element\n cts = Div(\n Div(id='notifications'),\n Form(mk_inp(), id='form', ws_send=True),\n hx_ext='ws', ws_connect='/ws')\n return Titled('Websocket Test', cts)\n\nasync def on_connect(send): await send(Div('Hello, you have connected', id=\"notifications\"))\nasync def on_disconnect(ws): print('Disconnected!')\n\n@app.ws('/ws', conn=on_connect, disconn=on_disconnect)\nasync def ws(msg:str, send):\n # websocket hander returns/sends are treated as OOB swaps\n await send(Div('Hello ' + msg, id=\"notifications\"))\n return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()\n\nSample chatbot that uses FastHTML’s setup_ws function:\napp = FastHTML(exts='ws')\nrt = app.route\nmsgs = []\n\n@rt('/')\ndef home():\n return Div(hx_ext='ws', ws_connect='/ws')(\n Div(Ul(*[Li(m) for m in msgs], id='msg-list')),\n Form(Input(id='msg'), id='form', ws_send=True)\n )\n\nasync def ws(msg:str):\n msgs.append(msg)\n await send(Ul(*[Li(m) for m in msgs], id='msg-list'))\n\nsend = setup_ws(app, ws)\n\nSingle File Uploads\nForm defaults to “multipart/form-data”. A Starlette UploadFile is passed to the handler.\nupload_dir = Path(\"filez\")\n\n@rt\ndef index():\n return (\n Form(hx_post=upload, hx_target=\"#result\")(\n Input(type=\"file\", name=\"file\"),\n Button(\"Upload\", type=\"submit\")),\n Div(id=\"result\")\n )\n\n# Use `async` handlers where IO is used to avoid blocking other clients\n@rt\nasync def upload(file: UploadFile):\n filebuffer = await file.read()\n (upload_dir / file.filename).write_bytes(filebuffer)\n return P('Size: ', file.size)\nFor multi-file, use Input(..., multiple=True), and a type annotation of list[UploadFile] in the handler.", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#fastlite", + "href": "ref/concise_guide.html#fastlite", + "title": "Concise reference", + "section": "Fastlite", + "text": "Fastlite\nFastlite and the MiniDataAPI specification it’s built on are a CRUD-oriented API for working with SQLite. APSW and apswutils is used to connect to SQLite, optimized for speed and clean error handling.\n\nfrom fastlite import *\n\n\ndb = database(':memory:') # or database('data/app.db')\n\nTables are normally constructed with classes, field types are specified as type hints.\n\nclass Book: isbn: str; title: str; pages: int; userid: int\n# The transform arg instructs fastlite to change the db schema when fields change.\n# Create only creates a table if the table doesn't exist.\nbooks = db.create(Book, pk='isbn', transform=True)\n \nclass User: id: int; name: str; active: bool = True\n# If no pk is provided, id is used as the primary key.\nusers = db.create(User, transform=True)\nusers\n\n<Table user (id, name, active)>\n\n\n\nFastlite CRUD operations\nEvery operation in fastlite returns a full superset of dataclass functionality.\n\nuser = users.insert(name='Alex',active=False)\nuser\n\nUser(id=1, name='Alex', active=0)\n\n\n\n# List all records\nusers()\n\n[User(id=1, name='Alex', active=0)]\n\n\n\n# Limit, offset, and order results:\nusers(order_by='name', limit=2, offset=1)\n\n# Filter on the results\nusers(where=\"name='Alex'\")\n\n# Placeholder for avoiding injection attacks\nusers(\"name=?\", ('Alex',))\n\n# A single record by pk\nusers[user.id]\n\nUser(id=1, name='Alex', active=0)\n\n\nTest if a record exists by using in keyword on primary key:\n\n1 in users\n\nTrue\n\n\nUpdates (which take a dict or a typed object) return the updated record.\n\nuser.name='Lauren'\nuser.active=True\nusers.update(user)\n\nUser(id=1, name='Lauren', active=1)\n\n\n.xtra() to automatically constrain queries, updates, and inserts from there on:\n\nusers.xtra(active=True)\nusers()\n\n[User(id=1, name='Lauren', active=1)]\n\n\nDeleting by pk:\n\nusers.delete(user.id)\n\n<Table user (id, name, active)>\n\n\nNotFoundError is raised by pk [], updates, and deletes.\n\ntry: users['Amy']\nexcept NotFoundError: print('User not found')\n\nUser not found", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/concise_guide.html#monsterui", + "href": "ref/concise_guide.html#monsterui", + "title": "Concise reference", + "section": "MonsterUI", + "text": "MonsterUI\nMonsterUI is a shadcn-like component library for FastHTML. It adds the Tailwind-based libraries FrankenUI and DaisyUI to FastHTML, as well as Python’s mistletoe for Markdown, HighlightJS for code highlighting, and Katex for latex support, following semantic HTML patterns when possible. It is recommended for when you wish to go beyond the basics provided by FastHTML’s built-in pico support.\nA minimal app:\n\nfrom fasthtml.common import *\nfrom monsterui.all import *\n\napp, rt = fast_app(hdrs=Theme.blue.headers(highlightjs=True)) # Use MonsterUI blue theme and highlight code in markdown\n\n@rt\ndef index():\n socials = (('github','https://github.com/AnswerDotAI/MonsterUI'),)\n return Titled(\"App\",\n Card(\n P(\"App\", cls=TextPresets.muted_sm),\n # LabelInput, DivLAigned, and UkIconLink are non-semantic MonsterUI FT Components,\n LabelInput('Email', type='email', required=True),\n footer=DivLAligned(*[UkIconLink(icon,href=url) for icon,url in socials])))\n\nMonsterUI recommendations:\n\nUse defaults as much as possible, for example Container in monsterui already has defaults for margins\nUse *T for button styling consistency, for example cls=ButtonT.destructive for a red delete button or cls=ButtonT.primary for a CTA button\nUse Label* functions for forms as much as possible (e.g. LabelInput, LabelRange) which creates and links both the FormLabel and user input appropriately to avoid boiler plate.\n\nFlex Layout Elements (such as DivLAligned and DivFullySpaced) can be used to create layouts concisely\n\ndef TeamCard(name, role, location=\"Remote\"):\n icons = (\"mail\", \"linkedin\", \"github\")\n return Card(\n DivLAligned(\n DiceBearAvatar(name, h=24, w=24),\n Div(H3(name), P(role))),\n footer=DivFullySpaced(\n DivHStacked(UkIcon(\"map-pin\", height=16), P(location)),\n DivHStacked(*(UkIconLink(icon, height=16) for icon in icons))))\n\nForms are styled and spaced for you without significant additional classes.\n\ndef MonsterForm():\n relationship = [\"Parent\",'Sibling', \"Friend\", \"Spouse\", \"Significant Other\", \"Relative\", \"Child\", \"Other\"]\n return Div(\n DivCentered(\n H3(\"Emergency Contact Form\"),\n P(\"Please fill out the form completely\", cls=TextPresets.muted_sm)),\n Form(\n Grid(LabelInput(\"Name\",id='name'),LabelInput(\"Email\", id='email')),\n H3(\"Relationship to patient\"),\n Grid(*[LabelCheckboxX(o) for o in relationship], cols=4, cls='space-y-3'),\n DivCentered(Button(\"Submit Form\", cls=ButtonT.primary))),\n cls='space-y-4')\n\nText can be styled with markdown automatically with MonsterUI\n\nrender_md(\"\"\"\n# My Document\n\n> Important note here\n\n+ List item with **bold**\n+ Another with `code`\n\n```python\ndef hello():\n print(\"world\")\n```\n\"\"\")\n\n'<div><h1 class=\"uk-h1 text-4xl font-bold mt-12 mb-6\">My Document</h1>\\n<blockquote class=\"uk-blockquote pl-4 border-l-4 border-primary italic mb-6\">\\n<p class=\"text-lg leading-relaxed mb-6\">Important note here</p>\\n</blockquote>\\n<ul class=\"uk-list uk-list-bullet space-y-2 mb-6 ml-6 text-lg\">\\n<li class=\"leading-relaxed\">List item with <strong>bold</strong></li>\\n<li class=\"leading-relaxed\">Another with <code class=\"uk-codespan px-1\">code</code></li>\\n</ul>\\n<pre class=\"bg-base-200 rounded-lg p-4 mb-6\"><code class=\"language-python uk-codespan px-1 uk-codespan px-1 block overflow-x-auto\">def hello():\\n print(\"world\")\\n</code></pre>\\n</div>'\n\n\nOr using semantic HTML:\n\ndef SemanticText():\n return Card(\n H1(\"MonsterUI's Semantic Text\"),\n P(\n Strong(\"MonsterUI\"), \" brings the power of semantic HTML to life with \",\n Em(\"beautiful styling\"), \" and \", Mark(\"zero configuration\"), \".\"),\n Blockquote(\n P(\"Write semantic HTML in pure Python, get modern styling for free.\"),\n Cite(\"MonsterUI Team\")),\n footer=Small(\"Released February 2025\"),)", + "crumbs": [ + "Home", + "Reference", + "Concise reference" + ] + }, + { + "objectID": "ref/handlers.html", + "href": "ref/handlers.html", + "title": "Handling handlers", + "section": "", + "text": "from fasthtml.common import *\nfrom collections import namedtuple\nfrom typing import TypedDict\nfrom datetime import datetime\nimport json,time\napp = FastHTML()\nThe FastHTML class is the main application class for FastHTML apps.\nrt = app.route\napp.route is used to register route handlers. It is a decorator, which means we place it before a function that is used as a handler. Because it’s used frequently in most FastHTML applications, we often alias it as rt, as we do here.", + "crumbs": [ + "Home", + "Reference", + "Handling handlers" + ] + }, + { + "objectID": "ref/handlers.html#basic-route-handling", + "href": "ref/handlers.html#basic-route-handling", + "title": "Handling handlers", + "section": "Basic Route Handling", + "text": "Basic Route Handling\n\n@rt(\"/hi\")\ndef get(): return 'Hi there'\n\nHandler functions can return strings directly. These strings are sent as the response body to the client.\n\ncli = Client(app)\n\nClient is a test client for FastHTML applications. It allows you to simulate requests to your app without running a server.\n\ncli.get('/hi').text\n\n'Hi there'\n\n\nThe get method on a Client instance simulates GET requests to the app. It returns a response object that has a .text attribute, which you can use to access the body of the response. It calls httpx.get internally – all httpx HTTP verbs are supported.\n\n@rt(\"/hi\")\ndef post(): return 'Postal'\ncli.post('/hi').text\n\n'Postal'\n\n\nHandler functions can be defined for different HTTP methods on the same route. Here, we define a post handler for the /hi route. The Client instance can simulate different HTTP methods, including POST requests.", + "crumbs": [ + "Home", + "Reference", + "Handling handlers" + ] + }, + { + "objectID": "ref/handlers.html#request-and-response-objects", + "href": "ref/handlers.html#request-and-response-objects", + "title": "Handling handlers", + "section": "Request and Response Objects", + "text": "Request and Response Objects\n\n@app.get(\"/hostie\")\ndef show_host(req): return req.headers['host']\ncli.get('/hostie').text\n\n'testserver'\n\n\nHandler functions can accept a req (or request) parameter, which represents the incoming request. This object contains information about the request, including headers. In this example, we return the host header from the request. The test client uses ‘testserver’ as the default host.\nIn this example, we use @app.get(\"/hostie\") instead of @rt(\"/hostie\"). The @app.get() decorator explicitly specifies the HTTP method (GET) for the route, while @rt() by default handles both GET and POST requests.\n\n@rt\ndef yoyo(): return 'a yoyo'\ncli.post('/yoyo').text\n\n'a yoyo'\n\n\nIf the @rt decorator is used without arguments, it uses the function name as the route path. Here, the yoyo function becomes the handler for the /yoyo route. This handler responds to GET and POST methods, since a specific method wasn’t provided.\n\n@rt\ndef ft1(): return Html(Div('Text.'))\nprint(cli.get('/ft1').text)\n\n <!doctype html>\n <html>\n <div>Text.</div>\n </html>\n\n\n\nHandler functions can return FT objects, which are automatically converted to HTML strings. The FT class can take other FT components as arguments, such as Div. This allows for easy composition of HTML elements in your responses.\n\n@app.get\ndef autopost(): return Html(Div('Text.', hx_post=yoyo.to()))\nprint(cli.get('/autopost').text)\n\n <!doctype html>\n <html>\n <div hx-post=\"/yoyo\">Text.</div>\n </html>\n\n\n\nThe rt decorator modifies the yoyo function by adding a to() method. This method returns the route path associated with the handler. It’s a convenient way to reference the route of a handler function dynamically.\nIn the example, yoyo.to() is used as the value for hx_post. This means when the div is clicked, it will trigger an HTMX POST request to the route of the yoyo handler. This approach allows for flexible, DRY code by avoiding hardcoded route strings and automatically updating if the route changes.\nThis pattern is particularly useful in larger applications where routes might change, or when building reusable components that need to reference their own routes dynamically.\n\n@app.get\ndef autoget(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b'))))\nprint(cli.get('/autoget').text)\n\n <!doctype html>\n <html>\n <body>\n <div hx-post=\"/hostie?a=b\" class=\"px-2\">Text.</div>\n </body>\n </html>\n\n\n\nThe rt() method of handler functions can also accept parameters. When called with parameters, it returns the route path with a query string appended. In this example, show_host.to(a='b') generates the path /hostie?a=b.\nThe Body component is used here to demonstrate nesting of FT components. Div is nested inside Body, showcasing how you can create more complex HTML structures.\nThe cls parameter is used to add a CSS class to the Div. This translates to the class attribute in the rendered HTML. (class can’t be used as a parameter name directly in Python since it’s a reserved word.)\n\n@rt('/ft2')\ndef get(): return Title('Foo'),H1('bar')\nprint(cli.get('/ft2').text)\n\n <!doctype html>\n <html>\n <head>\n <title>Foo</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<script src=\"https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script>\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };</script> </head>\n <body>\n <h1>bar</h1>\n </body>\n </html>\n\n\n\nHandler functions can return multiple FT objects as a tuple. The first item is treated as the Title, and the rest are added to the Body. When the request is not an HTMX request, FastHTML automatically adds necessary HTML boilerplate, including default head content with required scripts.\nWhen using app.route (or rt), if the function name matches an HTTP verb (e.g., get, post, put, delete), that HTTP method is automatically used for the route. In this case, a path must be explicitly provided as an argument to the decorator.\n\nhxhdr = {'headers':{'hx-request':\"1\"}}\nprint(cli.get('/ft2', **hxhdr).text)\n\n <title>Foo</title>\n <h1>bar</h1>\n\n\n\nFor HTMX requests (indicated by the hx-request header), FastHTML returns only the specified components without the full HTML structure. This allows for efficient partial page updates in HTMX applications.\n\n@rt('/ft3')\ndef get(): return H1('bar')\nprint(cli.get('/ft3', **hxhdr).text)\n\n <h1>bar</h1>\n\n\n\nWhen a handler function returns a single FT object for an HTMX request, it’s rendered as a single HTML partial.\n\n@rt('/ft4')\ndef get(): return Html(Head(Title('hi')), Body(P('there')))\n\nprint(cli.get('/ft4').text)\n\n <!doctype html>\n <html>\n <head>\n <title>hi</title>\n </head>\n <body>\n <p>there</p>\n </body>\n </html>\n\n\n\nHandler functions can return a complete Html structure, including Head and Body components. When a full HTML structure is returned, FastHTML doesn’t add any additional boilerplate. This gives you full control over the HTML output when needed.\n\n@rt\ndef index(): return \"welcome!\"\nprint(cli.get('/').text)\n\nwelcome!\n\n\nThe index function is a special handler in FastHTML. When defined without arguments to the @rt decorator, it automatically becomes the handler for the root path ('/'). This is a convenient way to define the main page or entry point of your application.", + "crumbs": [ + "Home", + "Reference", + "Handling handlers" + ] + }, + { + "objectID": "ref/handlers.html#path-and-query-parameters", + "href": "ref/handlers.html#path-and-query-parameters", + "title": "Handling handlers", + "section": "Path and Query Parameters", + "text": "Path and Query Parameters\n\n@rt('/user/{nm}', name='gday')\ndef get(nm:str=''): return f\"Good day to you, {nm}!\"\ncli.get('/user/Alexis').text\n\n'Good day to you, Alexis!'\n\n\nHandler functions can use path parameters, defined using curly braces in the route – this is implemented by Starlette directly, so all Starlette path parameters can be used. These parameters are passed as arguments to the function.\nThe name parameter in the decorator allows you to give the route a name, which can be used for URL generation.\nIn this example, {nm} in the route becomes the nm parameter in the function. The function uses this parameter to create a personalized greeting.\n\n@app.get\ndef autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))\nprint(cli.get('/autolink').text)\n\n <!doctype html>\n <html>\n <div href=\"/user/Alexis\">Text.</div>\n </html>\n\n\n\nThe uri function is used to generate URLs for named routes. It takes the route name as its first argument, followed by any path or query parameters needed for that route.\nIn this example, uri('gday', nm='Alexis') generates the URL for the route named ‘gday’ (which we defined earlier as ‘/user/{nm}’), with ‘Alexis’ as the value for the ‘nm’ parameter.\nThe link parameter in FT components sets the href attribute of the rendered HTML element. By using uri(), we can dynamically generate correct URLs even if the underlying route structure changes.\nThis approach promotes maintainable code by centralizing route definitions and avoiding hardcoded URLs throughout the application.\n\n@rt('/link')\ndef get(req): return f\"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}\"\n\ncli.get('/link').text\n\n'http://testserver/user/Alexis; http://testserver/hostie'\n\n\nThe url_for method of the request object can be used to generate URLs for named routes. It takes the route name as its first argument, followed by any path parameters needed for that route.\nIn this example, req.url_for('gday', nm='Alexis') generates the full URL for the route named ‘gday’, including the scheme and host. Similarly, req.url_for('show_host') generates the URL for the ‘show_host’ route.\nThis method is particularly useful when you need to generate absolute URLs, such as for email links or API responses. It ensures that the correct host and scheme are included, even if the application is accessed through different domains or protocols.\n\napp.url_path_for('gday', nm='Jeremy')\n\n'/user/Jeremy'\n\n\nThe url_path_for method of the application can be used to generate URL paths for named routes. Unlike url_for, it returns only the path component of the URL, without the scheme or host.\nIn this example, app.url_path_for('gday', nm='Jeremy') generates the path ‘/user/Jeremy’ for the route named ‘gday’.\nThis method is useful when you need relative URLs or just the path component, such as for internal links or when constructing URLs in a host-agnostic manner.\n\n@rt('/oops')\ndef get(nope): return nope\nr = cli.get('/oops?nope=1')\nprint(r)\nr.text\n\n<Response [200 OK]>\n\n\n/Users/iflath/git/AnswerDotAI/fasthtml/build/__editable__.python_fasthtml-0.12.1-py3-none-any/fasthtml/core.py:188: UserWarning: `nope has no type annotation and is not a recognised special name, so is ignored.\n if arg!='resp': warn(f\"`{arg} has no type annotation and is not a recognised special name, so is ignored.\")\n\n\n''\n\n\nHandler functions can include parameters, but they must be type-annotated or have special names (like req) to be recognized. In this example, the nope parameter is not annotated, so it’s ignored, resulting in a warning.\nWhen a parameter is ignored, it doesn’t receive the value from the query string. This can lead to unexpected behavior, as the function attempts to return nope, which is undefined.\nThe cli.get('/oops?nope=1') call succeeds with a 200 OK status because the handler doesn’t raise an exception, but it returns an empty response, rather than the intended value.\nTo fix this, you should either add a type annotation to the parameter (e.g., def get(nope: str):) or use a recognized special name like req.\n\n@rt('/html/{idx}')\ndef get(idx:int): return Body(H4(f'Next is {idx+1}.'))\nprint(cli.get('/html/1', **hxhdr).text)\n\n <body>\n <h4>Next is 2.</h4>\n </body>\n\n\n\nPath parameters can be type-annotated, and FastHTML will automatically convert them to the specified type if possible. In this example, idx is annotated as int, so it’s converted from the string in the URL to an integer.\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm\")\n\n@rt(r'/static/{path:path}{fn}.{ext:imgext}')\ndef get(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\nprint(cli.get('/static/foo/jph.ico').text)\n\nGetting jph.ico from /foo/\n\n\nThe reg_re_param function is used to register custom path parameter types using regular expressions. Here, we define a new path parameter type called “imgext” that matches common image file extensions.\nHandler functions can use complex path patterns with multiple parameters and custom types. In this example, the route pattern r'/static/{path:path}{fn}.{ext:imgext}' uses three path parameters:\n\npath: A Starlette built-in type that matches any path segments\nfn: The filename without extension\next: Our custom “imgext” type that matches specific image extensions\n\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\n\n@rt(\"/models/{nm}\")\ndef get(nm:ModelName): return nm\n\nprint(cli.get('/models/alexnet').text)\n\nalexnet\n\n\nWe define ModelName as an enum with three possible values: “alexnet”, “resnet”, and “lenet”. Handler functions can use these enum types as parameter annotations. In this example, the nm parameter is annotated with ModelName, which ensures that only valid model names are accepted.\nWhen a request is made with a valid model name, the handler function returns that name. This pattern is useful for creating type-safe APIs with a predefined set of valid values.\n\n@rt(\"/files/{path}\")\nasync def get(path: Path): return path.with_suffix('.txt')\nprint(cli.get('/files/foo').text)\n\nfoo.txt\n\n\nHandler functions can use Path objects as parameter types. The Path type is from Python’s standard library pathlib module, which provides an object-oriented interface for working with file paths. In this example, the path parameter is annotated with Path, so FastHTML automatically converts the string from the URL to a Path object.\nThis approach is particularly useful when working with file-related routes, as it provides a convenient and platform-independent way to handle file paths.\n\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n@rt(\"/items/\")\ndef get(idx:int|None = 0): return fake_db[idx]\nprint(cli.get('/items/?idx=1').text)\n\n{\"name\":\"Bar\"}\n\n\nHandler functions can use query parameters, which are automatically parsed from the URL. In this example, idx is a query parameter with a default value of 0. It’s annotated as int|None, allowing it to be either an integer or None.\nThe function uses this parameter to index into a fake database (fake_db). When a request is made with a valid idx query parameter, the handler returns the corresponding item from the database.\n\nprint(cli.get('/items/').text)\n\n{\"name\":\"Foo\"}\n\n\nWhen no idx query parameter is provided, the handler function uses the default value of 0. This results in returning the first item from the fake_db list, which is {\"name\":\"Foo\"}.\nThis behavior demonstrates how default values for query parameters work in FastHTML. They allow the API to have a sensible default behavior when optional parameters are not provided.\n\nprint(cli.get('/items/?idx=g'))\n\n<Response [404 Not Found]>\n\n\nWhen an invalid value is provided for a typed query parameter, FastHTML returns a 404 Not Found response. In this example, ‘g’ is not a valid integer for the idx parameter, so the request fails with a 404 status.\nThis behavior ensures type safety and prevents invalid inputs from reaching the handler function.\n\n@app.get(\"/booly/\")\ndef _(coming:bool=True): return 'Coming' if coming else 'Not coming'\nprint(cli.get('/booly/?coming=true').text)\nprint(cli.get('/booly/?coming=no').text)\n\nComing\nNot coming\n\n\nHandler functions can use boolean query parameters. In this example, coming is a boolean parameter with a default value of True. FastHTML automatically converts string values like ‘true’, ‘false’, ‘1’, ‘0’, ‘on’, ‘off’, ‘yes’, and ‘no’ to their corresponding boolean values.\nThe underscore _ is used as the function name in this example to indicate that the function’s name is not important or won’t be referenced elsewhere. This is a common Python convention for throwaway or unused variables, and it works here because FastHTML uses the route decorator parameter, when provided, to determine the URL path, not the function name. By default, both get and post methods can be used in routes that don’t specify an http method (by either using app.get, def get, or the methods parameter to app.route).\n\n@app.get(\"/datie/\")\ndef _(d:parsed_date): return d\ndate_str = \"17th of May, 2024, 2p\"\nprint(cli.get(f'/datie/?d={date_str}').text)\n\n2024-05-17 14:00:00\n\n\nHandler functions can use date objects as parameter types. FastHTML uses dateutil.parser library to automatically parse a wide variety of date string formats into date objects.\n\n@app.get(\"/ua\")\nasync def _(user_agent:str): return user_agent\nprint(cli.get('/ua', headers={'User-Agent':'FastHTML'}).text)\n\nFastHTML\n\n\nHandler functions can access HTTP headers by using parameter names that match the header names. In this example, user_agent is used as a parameter name, which automatically captures the value of the ‘User-Agent’ header from the request.\nThe Client instance allows setting custom headers for test requests. Here, we set the ‘User-Agent’ header to ‘FastHTML’ in the test request.\n\n@app.get(\"/hxtest\")\ndef _(htmx): return htmx.request\nprint(cli.get('/hxtest', headers={'HX-Request':'1'}).text)\n\n@app.get(\"/hxtest2\")\ndef _(foo:HtmxHeaders, req): return foo.request\nprint(cli.get('/hxtest2', headers={'HX-Request':'1'}).text)\n\n1\n1\n\n\nHandler functions can access HTMX-specific headers using either the special htmx parameter name, or a parameter annotated with HtmxHeaders. Both approaches provide access to HTMX-related information.\nIn these examples, the htmx.request attribute returns the value of the ‘HX-Request’ header.\n\napp.chk = 'foo'\n@app.get(\"/app\")\ndef _(app): return app.chk\nprint(cli.get('/app').text)\n\nfoo\n\n\nHandler functions can access the FastHTML application instance using the special app parameter name. This allows handlers to access application-level attributes and methods.\nIn this example, we set a custom attribute chk on the application instance. The handler function then uses the app parameter to access this attribute and return its value.\n\n@app.get(\"/app2\")\ndef _(foo:FastHTML): return foo.chk,HttpHeader(\"mykey\", \"myval\")\nr = cli.get('/app2', **hxhdr)\nprint(r.text)\nprint(r.headers)\n\nfoo\nHeaders({'mykey': 'myval', 'content-length': '3', 'content-type': 'text/html; charset=utf-8'})\n\n\nHandler functions can access the FastHTML application instance using a parameter annotated with FastHTML. This allows handlers to access application-level attributes and methods, just like using the special app parameter name.\nHandlers can return tuples containing both content and HttpHeader objects. HttpHeader allows setting custom HTTP headers in the response.\nIn this example:\n\nWe define a handler that returns both the chk attribute from the application and a custom header.\nThe HttpHeader(\"mykey\", \"myval\") sets a custom header in the response.\nWe use the test client to make a request and examine both the response text and headers.\nThe response includes the custom header “mykey” along with standard headers like content-length and content-type.\n\n\n@app.get(\"/app3\")\ndef _(foo:FastHTML): return HtmxResponseHeaders(location=\"http://example.org\")\nr = cli.get('/app3')\nprint(r.headers)\n\nHeaders({'hx-location': 'http://example.org', 'content-length': '0', 'content-type': 'text/html; charset=utf-8'})\n\n\nHandler functions can return HtmxResponseHeaders objects to set HTMX-specific response headers. This is useful for HTMX-specific behaviors like client-side redirects.\nIn this example we define a handler that returns an HtmxResponseHeaders object with a location parameter, which sets the HX-Location header in the response. HTMX uses this for client-side redirects.\n\n@app.get(\"/app4\")\ndef _(foo:FastHTML): return Redirect(\"http://example.org\")\ncli.get('/app4', follow_redirects=False)\n\n<Response [303 See Other]>\n\n\nHandler functions can return Redirect objects to perform HTTP redirects. This is useful for redirecting users to different pages or external URLs.\nIn this example:\n\nWe define a handler that returns a Redirect object with the URL “http://example.org”.\nThe cli.get('/app4', follow_redirects=False) call simulates a GET request to the ‘/app4’ route without following redirects.\nThe response has a 303 See Other status code, indicating a redirect.\n\nThe follow_redirects=False parameter is used to prevent the test client from automatically following the redirect, allowing us to inspect the redirect response itself.\n\nRedirect.__response__\n\n<function fasthtml.core.Redirect.__response__(self, req)>\n\n\nThe Redirect class in FastHTML implements a __response__ method, which is a special method recognized by the framework. When a handler returns a Redirect object, FastHTML internally calls this __response__ method to replace the original response.\nThe __response__ method takes a req parameter, which represents the incoming request. This allows the method to access request information if needed when constructing the redirect response.\n\n@rt\ndef meta(): \n return ((Title('hi'),H1('hi')),\n (Meta(property='image'), Meta(property='site_name')))\n\nprint(cli.post('/meta').text)\n\n <!doctype html>\n <html>\n <head>\n <title>hi</title>\n <meta property=\"image\">\n <meta property=\"site_name\">\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<script src=\"https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script>\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };</script> </head>\n <body>\n <h1>hi</h1>\n </body>\n </html>\n\n\n\nFastHTML automatically identifies elements typically placed in the <head> (like Title and Meta) and positions them accordingly, while other elements go in the <body>.\nIn this example: - (Title('hi'), H1('hi')) defines the title and main heading. The title is placed in the head, and the H1 in the body. - (Meta(property='image'), Meta(property='site_name')) defines two meta tags, which are both placed in the head.", + "crumbs": [ + "Home", + "Reference", + "Handling handlers" + ] + }, + { + "objectID": "ref/handlers.html#apirouter", + "href": "ref/handlers.html#apirouter", + "title": "Handling handlers", + "section": "APIRouter", + "text": "APIRouter\nAPIRouter is useful when you want to split your application routes across multiple .py files that are part of a single FastHTMl application. It accepts an optional prefix argument that will be applied to all routes within that instance of APIRouter.\nBelow we define several hypothetical product related routes in a products.py and then demonstrate how they can seamlessly be incorporated into a FastHTML app instance.\n\n# products.py\nar = APIRouter(prefix=\"/products\")\n\n@ar(\"/all\")\ndef all_products(req):\n return Div(\n \"Welcome to the Products Page! Click the button below to look at the details for product 42\",\n Div(\n Button(\n \"Details\",\n hx_get=req.url_for(\"details\", pid=42),\n hx_target=\"#products_list\",\n hx_swap=\"outerHTML\",\n ),\n ),\n id=\"products_list\",\n )\n\n\n@ar.get(\"/{pid}\", name=\"details\")\ndef details(pid: int):\n return f\"Here are the product details for ID: {pid}\"\n\nSince we specified the prefix=/products in our hypothetical products.py file, all routes defined in that file will be found under /products.\n\nprint(str(ar.rt_funcs.all_products))\nprint(str(ar.rt_funcs.details))\n\n/products/all\n/products/{pid}\n\n\n\n# main.py\n# from products import ar\n\napp, rt = fast_app()\nar.to_app(app)\n\n@rt\ndef index():\n return Div(\n \"Click me for a look at our products\",\n hx_get=ar.rt_funcs.all_products,\n hx_swap=\"outerHTML\",\n )\n\nNote how you can reference our python route functions via APIRouter.rt_funcs in your hx_{http_method} calls like normal.", + "crumbs": [ + "Home", + "Reference", + "Handling handlers" + ] + }, + { + "objectID": "ref/handlers.html#form-data-and-json-handling", + "href": "ref/handlers.html#form-data-and-json-handling", + "title": "Handling handlers", + "section": "Form Data and JSON Handling", + "text": "Form Data and JSON Handling\n\napp = FastHTML()\nrt = app.route\ncli = Client(app)\n\n\n@app.post('/profile/me')\ndef profile_update(username: str): return username\n\nr = cli.post('/profile/me', data={'username' : 'Alexis'}).text\nassert r == 'Alexis'\nprint(r)\n\nAlexis\n\n\nHandler functions can accept form data parameters, without needing to manually extract it from the request. In this example, username is expected to be sent as form data.\nThe data parameter in the cli.post() method simulates sending form data in the request.\n\nr = cli.post('/profile/me', data={})\nassert r.status_code == 400\nprint(r.text)\nr\n\nMissing required field: username\n\n\n<Response [400 Bad Request]>\n\n\nIf required form data is missing, FastHTML automatically returns a 400 Bad Request response with an error message.\n\n@app.post('/pet/dog')\ndef pet_dog(dogname: str = None): return dogname or 'unknown name'\nr = cli.post('/pet/dog', data={}).text\nr\n\n'unknown name'\n\n\nHandlers can have optional form data parameters with default values. In this example, dogname is an optional parameter with a default value of None.\nHere, if the form data doesn’t include the dogname field, the function uses the default value. The function returns either the provided dogname or ‘unknown name’ if dogname is None.\n\n@dataclass\nclass Bodie: a:int;b:str\n\n@rt(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\nprint(cli.post('/bodie/me', data=dict(a=1, b='foo', nm='me')).text)\n\n{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}\n\n\nYou can use dataclasses to define structured form data. In this example, Bodie is a dataclass with a (int) and b (str) fields.\nFastHTML automatically converts the incoming form data to a Bodie instance where attribute names match parameter names. Other form data elements are matched with parameters with the same names (in this case, nm).\nHandler functions can return dictionaries, which FastHTML automatically JSON-encodes.\n\n@app.post(\"/bodied/\")\ndef bodied(data:dict): return data\n\nd = dict(a=1, b='foo')\nprint(cli.post('/bodied/', data=d).text)\n\n{\"a\":\"1\",\"b\":\"foo\"}\n\n\ndict parameters capture all form data as a dictionary. In this example, the data parameter is annotated with dict, so FastHTML automatically converts all incoming form data into a dictionary.\nNote that when form data is converted to a dictionary, all values become strings, even if they were originally numbers. This is why the ‘a’ key in the response has a string value “1” instead of the integer 1.\n\nnt = namedtuple('Bodient', ['a','b'])\n\n@app.post(\"/bodient/\")\ndef bodient(data:nt): return asdict(data)\nprint(cli.post('/bodient/', data=d).text)\n\n{\"a\":\"1\",\"b\":\"foo\"}\n\n\nHandler functions can use named tuples to define structured form data. In this example, Bodient is a named tuple with a and b fields.\nFastHTML automatically converts the incoming form data to a Bodient instance where field names match parameter names. As with the previous example, all form data values are converted to strings in the process.\n\nclass BodieTD(TypedDict): a:int;b:str='foo'\n\n@app.post(\"/bodietd/\")\ndef bodient(data:BodieTD): return data\nprint(cli.post('/bodietd/', data=d).text)\n\n{\"a\":1,\"b\":\"foo\"}\n\n\nYou can use TypedDict to define structured form data with type hints. In this example, BodieTD is a TypedDict with a (int) and b (str) fields, where b has a default value of ‘foo’.\nFastHTML automatically converts the incoming form data to a BodieTD instance where keys match the defined fields. Unlike with regular dictionaries or named tuples, FastHTML respects the type hints in TypedDict, converting values to the specified types when possible (e.g., converting ‘1’ to the integer 1 for the ‘a’ field).\n\nclass Bodie2:\n a:int|None; b:str\n def __init__(self, a, b='foo'): store_attr()\n\n@app.post(\"/bodie2/\")\ndef bodie(d:Bodie2): return f\"a: {d.a}; b: {d.b}\"\nprint(cli.post('/bodie2/', data={'a':1}).text)\n\na: 1; b: foo\n\n\nCustom classes can be used to define structured form data. Here, Bodie2 is a custom class with a (int|None) and b (str) attributes, where b has a default value of ‘foo’. The store_attr() function (from fastcore) automatically assigns constructor parameters to instance attributes.\nFastHTML automatically converts the incoming form data to a Bodie2 instance, matching form fields to constructor parameters. It respects type hints and default values.\n\n@app.post(\"/b\")\ndef index(it: Bodie): return Titled(\"It worked!\", P(f\"{it.a}, {it.b}\"))\n\ns = json.dumps({\"b\": \"Lorem\", \"a\": 15})\nprint(cli.post('/b', headers={\"Content-Type\": \"application/json\", 'hx-request':\"1\"}, data=s).text)\n\n <title>It worked!</title>\n<main class=\"container\"> <h1>It worked!</h1>\n <p>15, Lorem</p>\n</main>\n\n\nHandler functions can accept JSON data as input, which is automatically parsed into the specified type. In this example, it is of type Bodie, and FastHTML converts the incoming JSON data to a Bodie instance.\nThe Titled component is used to create a page with a title and main content. It automatically generates an <h1> with the provided title, wraps the content in a <main> tag with a “container” class, and adds a title to the head.\nWhen making a request with JSON data: - Set the “Content-Type” header to “application/json” - Provide the JSON data as a string in the data parameter of the request", + "crumbs": [ + "Home", + "Reference", + "Handling handlers" + ] + }, + { + "objectID": "ref/handlers.html#cookies-sessions-file-uploads-and-more", + "href": "ref/handlers.html#cookies-sessions-file-uploads-and-more", + "title": "Handling handlers", + "section": "Cookies, Sessions, File Uploads, and more", + "text": "Cookies, Sessions, File Uploads, and more\n\n@rt(\"/setcookie\")\ndef get(): return cookie('now', datetime.now())\n\n@rt(\"/getcookie\")\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nprint(cli.get('/setcookie').text)\ntime.sleep(0.01)\ncli.get('/getcookie').text\n\n\n\n\n'Cookie was set at time 16:19:27.811570'\n\n\nHandler functions can set and retrieve cookies. In this example:\n\nThe /setcookie route sets a cookie named ‘now’ with the current datetime.\nThe /getcookie route retrieves the ‘now’ cookie and returns its value.\n\nThe cookie() function is used to create a cookie response. FastHTML automatically converts the datetime object to a string when setting the cookie, and parses it back to a date object when retrieving it.\n\ncookie('now', datetime.now())\n\nHttpHeader(k='set-cookie', v='now=\"2025-01-30 16:19:29.997374\"; Path=/; SameSite=lax')\n\n\nThe cookie() function returns an HttpHeader object with the ‘set-cookie’ key. You can return it in a tuple along with FT elements, along with anything else FastHTML supports in responses.\n\napp = FastHTML(secret_key='soopersecret')\ncli = Client(app)\nrt = app.route\n\n\n@rt(\"/setsess\")\ndef get(sess, foo:str=''):\n now = datetime.now()\n sess['auth'] = str(now)\n return f'Set to {now}'\n\n@rt(\"/getsess\")\ndef get(sess): return f'Session time: {sess[\"auth\"]}'\n\nprint(cli.get('/setsess').text)\ntime.sleep(0.01)\n\ncli.get('/getsess').text\n\nSet to 2025-01-30 16:19:31.078650\n\n\n'Session time: 2025-01-30 16:19:31.078650'\n\n\nSessions store and retrieve data across requests. To use sessions, you should to initialize the FastHTML application with a secret_key. This is used to cryptographically sign the cookie used by the session.\nThe sess parameter in handler functions provides access to the session data. You can set and get session variables using dictionary-style access.\n\n@rt(\"/upload\")\nasync def post(uf:UploadFile): return (await uf.read()).decode()\n\nwith open('../../CHANGELOG.md', 'rb') as f:\n print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])\n\n# Release notes\n\n\nHandler functions can accept file uploads using Starlette’s UploadFile type. In this example:\n\nThe /upload route accepts a file upload named uf.\nThe UploadFile object provides an asynchronous read() method to access the file contents.\nWe use await to read the file content asynchronously and decode it to a string.\n\nWe added async to the handler function because it uses await to read the file content asynchronously. In Python, any function that uses await must be declared as async. This allows the function to be run asynchronously, potentially improving performance by not blocking other operations while waiting for the file to be read.\n\napp.static_route('.md', static_path='../..')\nprint(cli.get('/README.md').text[:10])\n\n# FastHTML\n\n\nThe static_route method of the FastHTML application allows serving static files with specified extensions from a given directory. In this example:\n\n.md files are served from the ../.. directory (two levels up from the current directory).\nAccessing /README.md returns the contents of the README.md file from that directory.\n\n\nhelp(app.static_route_exts)\n\nHelp on method static_route_exts in module fasthtml.core:\n\nstatic_route_exts(prefix='/', static_path='.', exts='static') method of fasthtml.core.FastHTML instance\n Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`\n\n\n\n\napp.static_route_exts()\nassert cli.get('/README.txt').status_code == 404\nprint(cli.get('/README.txt').text[:50])\n\n404 Not Found\n\n\nThe static_route_exts method of the FastHTML application allows serving static files with specified extensions from a given directory. By default:\n\nIt serves files from the current directory (‘.’).\nIt uses the ‘static’ regex, which includes common static file extensions like ‘ico’, ‘gif’, ‘jpg’, ‘css’, ‘js’, etc.\nThe URL prefix is set to ‘/’.\n\nThe ‘static’ regex is defined by FastHTML using this code:\nreg_re_param(\"static\", \"ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html|map\")\n\n@rt(\"/form-submit/{list_id}\")\ndef options(list_id: str):\n headers = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'POST',\n 'Access-Control-Allow-Headers': '*',\n }\n return Response(status_code=200, headers=headers)\n\nprint(cli.options('/form-submit/2').headers)\n\nHeaders({'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST', 'access-control-allow-headers': '*', 'content-length': '0', 'set-cookie': 'session_=eyJhdXRoIjogIjIwMjUtMDEtMzAgMTY6MTk6MzEuMDc4NjUwIn0=.Z5vtZA.1ooY2RCWopWAbLYDy6660g_LlHI; path=/; Max-Age=31536000; httponly; samesite=lax'})\n\n\nFastHTML handlers can handle OPTIONS requests and set custom headers. In this example:\n\nThe /form-submit/{list_id} route handles OPTIONS requests.\nCustom headers are set to allow cross-origin requests (CORS).\nThe function returns a Starlette Response object with a 200 status code and the custom headers.\n\nYou can return any Starlette Response type from a handler function, giving you full control over the response when needed.\n\ndef _not_found(req, exc): return Div('nope')\n\napp = FastHTML(exception_handlers={404:_not_found})\ncli = Client(app)\nrt = app.route\n\nr = cli.get('/')\nprint(r.text)\n\n <!doctype html>\n <html>\n <head>\n <title>FastHTML page</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<script src=\"https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script>\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };</script> </head>\n <body>\n <div>nope</div>\n </body>\n </html>\n\n\n\nFastHTML allows you to define custom exception handlers – in this case, a custom 404 (Not Found) handler function _not_found, which returns a Div component with the text ‘nope’.", + "crumbs": [ + "Home", + "Reference", + "Handling handlers" + ] + }, + { + "objectID": "ref/defining_xt_component.html", + "href": "ref/defining_xt_component.html", + "title": "Custom Components", + "section": "", + "text": "The majority of the time the default ft components are all you need (for example Div, P, H1, etc.).\nHowever, there are many situations where you need a custom ft component that creates a unique HTML tag (for example <zero-md></zero-md>). There are many options in FastHTML to do this, and this section will walk through them. Generally you want to use the highest level option that fits your needs.", + "crumbs": [ + "Home", + "Reference", + "Custom Components" + ] + }, + { + "objectID": "ref/defining_xt_component.html#notstr", + "href": "ref/defining_xt_component.html#notstr", + "title": "Custom Components", + "section": "NotStr", + "text": "NotStr\nThe first way is to use the NotStr class to use an HTML tag as a string. It works as a one-off but quickly becomes harder to work with as complexity grows. However we can see that you can genenrate the same xml using NotStr as the out-of-the-box components.\n\nfrom fasthtml.common import NotStr,Div, to_xml\n\n\ndiv_NotStr = NotStr('<div></div>') \nprint(div_NotStr)\n\n<div></div>", + "crumbs": [ + "Home", + "Reference", + "Custom Components" + ] + }, + { + "objectID": "ref/defining_xt_component.html#automatic-creation", + "href": "ref/defining_xt_component.html#automatic-creation", + "title": "Custom Components", + "section": "Automatic Creation", + "text": "Automatic Creation\nThe next (and better) approach is to let FastHTML generate the component function for you. As you can see in our assert this creates a function that creates the HTML just as we wanted. This works even though there is not a Some_never_before_used_tag function in the fasthtml.components source code (you can verify this yourself by looking at the source code).\n\n\n\n\n\n\nTip\n\n\n\nTypically these tags are needed because a CSS or Javascript library created a new XML tag that isn’t default HTML. For example the zero-md javascript library looks for a <zero-md></zero-md> tag to know what to run its javascript code on. Most CSS libraries work by creating styling based on the class attribute, but they can also apply styling to an arbitrary HTML tag that they made up.\n\n\n\nfrom fasthtml.components import Some_never_before_used_tag\n\nSome_never_before_used_tag()\n\n<some-never-before-used-tag></some-never-before-used-tag>", + "crumbs": [ + "Home", + "Reference", + "Custom Components" + ] + }, + { + "objectID": "ref/defining_xt_component.html#manual-creation", + "href": "ref/defining_xt_component.html#manual-creation", + "title": "Custom Components", + "section": "Manual Creation", + "text": "Manual Creation\nThe automatic creation isn’t magic. It’s just calling a python function __getattr__ and you can call it yourself to get the same result.\n\nimport fasthtml\n\nauto_called = fasthtml.components.Some_never_before_used_tag()\nmanual_called = fasthtml.components.__getattr__('Some_never_before_used_tag')()\n\n# Proving they generate the same xml\nassert to_xml(auto_called) == to_xml(manual_called)\n\nKnowing that, we know that it’s possible to create a different function that has different behavior than FastHTMLs default behavior by modifying how the ___getattr__ function creates the components! It’s only a few lines of code and reading that what it does is a great way to understand components more deeply.\n\n\n\n\n\n\nTip\n\n\n\nDunder methods and functions are special functions that have double underscores at the beginning and end of their name. They are called at specific times in python so you can use them to cause customized behavior that makes sense for your specific use case. They can appear magical if you don’t know how python works, but they are extremely commonly used to modify python’s default behavior (__init__ is probably the most common one).\nIn a module __getattr__ is called to get an attribute. In fasthtml.components, this is defined to create components automatically for you.\n\n\nFor example if you want a component that creates <path></path> that doesn’t conflict names with pathlib.Path you can do that. FastHTML automatically creates new components with a 1:1 mapping and a consistent name, which is almost always what you want. But in some cases you may want to customize that and you can use the ft_hx function to do that differently than the default.\n\nfrom fasthtml.common import ft_hx\n\ndef ft_path(*c, target_id=None, **kwargs): \n return ft_hx('path', *c, target_id=target_id, **kwargs)\n\nft_path()\n\n<path></path>\n\n\nWe can add any behavior in that function that we need to, so let’s go through some progressively complex examples that you may need in some of your projects.\n\nUnderscores in tags\nNow that we understand how FastHTML generates components, we can create our own in all kinds of ways. For example, maybe we need a weird HTML tag that uses underscores. FastHTML replaces _ with - in tags because underscores in tags are highly unusual and rarely what you want, though it does come up rarely.\n\ndef tag_with_underscores(*c, target_id=None, **kwargs): \n return ft_hx('tag_with_underscores', *c, target_id=target_id, **kwargs)\n\ntag_with_underscores()\n\n<tag_with_underscores></tag_with_underscores>\n\n\n\n\nSymbols (ie @) in tags\nSometimes you may need to use a tag that uses characters that are not allowed in function names in python (again, very unusual).\n\ndef tag_with_AtSymbol(*c, target_id=None, **kwargs): \n return ft_hx('tag-with-@symbol', *c, target_id=target_id, **kwargs)\n\ntag_with_AtSymbol()\n\n<tag-with-@symbol></tag-with-@symbol>\n\n\n\n\nSymbols (ie @) in tag attributes\nIt also may be that an argument in an HTML tag uses characters that can’t be used in python arguments. To handle these you can define those args using a dictionary.\n\nDiv(normal_arg='normal stuff',**{'notNormal:arg:with_varing@symbols!':'123'})\n\n<div normal-arg=\"normal stuff\" notnormal:arg:with_varing@symbols!=\"123\"></div>", + "crumbs": [ + "Home", + "Reference", + "Custom Components" + ] + }, + { + "objectID": "explains/minidataapi.html", + "href": "explains/minidataapi.html", + "title": "MiniDataAPI Spec", + "section": "", + "text": "The MiniDataAPI is a persistence API specification that designed to be small and relatively easy to implement across a wide range of datastores. While early implementations have been SQL-based, the specification can be quickly implemented in key/value stores, document databases, and more.", + "crumbs": [ + "Home", + "Explanations", + "MiniDataAPI Spec" + ] + }, + { + "objectID": "explains/minidataapi.html#why", + "href": "explains/minidataapi.html#why", + "title": "MiniDataAPI Spec", + "section": "Why?", + "text": "Why?\nThe MiniDataAPI specification allows us to use the same API for many different database engines. Any application using the MiniDataAPI spec for interacting with its database requires no modification beyond import and configuration changes to switch database engines. For example, to convert an application from Fastlite running SQLite to FastSQL running PostgreSQL, should require only changing these two lines:\n\n\nFastLite version\nfrom fastlite import *\ndb = database('test.db')\n\nFastSQL version\nfrom fastsql import *\ndb = Database('postgres:...')\n\n\nAs both libraries adhere to the MiniDataAPI specification, the rest of the code in the application should remain the same. The advantage of the MiniDataAPI spec is that it allows people to use whatever datastores they have access to or prefer.\n\n\n\n\n\n\nNote\n\n\n\nSwitching databases won’t migrate any existing data between databases.\n\n\n\nEasy to learn, quick to implement\nThe MiniDataAPI specification is designed to be easy-to-learn and quick to implement. It focuses on straightforward Create, Read, Update, and Delete (CRUD) operations.\nMiniDataAPI databases aren’t limited to just row-based systems. In fact, the specification is closer in design to a key/value store than a set of records. What’s exciting about this is we can write implementations for tools like Python dict stored as JSON, Redis, and even the venerable ZODB.\n\n\nLimitations of the MiniDataAPI Specification\n\n“Mini refers to the lightweightness of specification, not the data.”\n– Jeremy Howard\n\nThe advantages of the MiniDataAPI come at a cost. The MiniDataAPI specification focuses a very small set of features compared to what can be found in full-fledged ORMs and query languages. It intentionally avoids nuances or sophisticated features.\nThis means the specification does not include joins or formal foreign keys. Complex data stored over multiple tables that require joins isn’t handled well. For this kind of scenario it’s probably for the best to use more sophisticated ORMs or even direct database queries.\n\n\nSummary of the MiniDataAPI Design\n\nEasy-to-learn\nRelative quick to implement for new database engines\nAn API for CRUD operations\nFor many different types of databases including row- and key/value-based designs\nIntentionally small in terms of features: no joins, no foreign keys, no database specific features\nBest for simpler designs, complex architectures will need more sophisticated tools.", + "crumbs": [ + "Home", + "Explanations", + "MiniDataAPI Spec" + ] + }, + { + "objectID": "explains/minidataapi.html#connectconstruct-the-database", + "href": "explains/minidataapi.html#connectconstruct-the-database", + "title": "MiniDataAPI Spec", + "section": "Connect/construct the database", + "text": "Connect/construct the database\nWe connect or construct the database by passing in a string connecting to the database endpoint or a filepath representing the database’s location. While this example is for SQLite running in memory, other databases such as PostgreSQL, Redis, MongoDB, might instead use a URI pointing at the database’s filepath or endpoint. The method of connecting to a DB is not part of this API, but part of the underlying library. For instance, for fastlite:\n\ndb = database(':memory:')\n\nHere’s a complete list of the available methods in the API, all documented below (assuming db is a database and t is a table):\n\ndb.create\nt.insert\nt.delete\nt.update\nt[key]\nt(...)\nt.xtra", + "crumbs": [ + "Home", + "Explanations", + "MiniDataAPI Spec" + ] + }, + { + "objectID": "explains/minidataapi.html#tables", + "href": "explains/minidataapi.html#tables", + "title": "MiniDataAPI Spec", + "section": "Tables", + "text": "Tables\nFor the sake of expediency, this document uses a SQL example. However, tables can represent anything, not just the fundamental construct of a SQL databases. They might represent keys within a key/value structure or files on a hard-drive.\n\nCreating tables\nWe use a create() method attached to Database object (db in our example) to create the tables.\n\nclass User: name:str; email: str; year_started:int\nusers = db.create(User, pk='name')\nusers\n\n<Table user (name, email, year_started)>\n\n\n\nclass User: name:str; email: str; year_started:int\nusers = db.create(User, pk='name')\nusers\n\n<Table user (name, email, year_started)>\n\n\nIf no pk is provided, id is assumed to be the primary key. Regardless of whether you mark a class as a dataclass or not, it will be turned into one – specifically into a flexiclass.\n\n@dataclass\nclass Todo: id: int; title: str; detail: str; status: str; name: str\ntodos = db.create(Todo) \ntodos\n\n<Table todo (id, title, detail, status, name)>\n\n\n\n\nCompound primary keys\nThe MiniData API spec supports compound primary keys, where more than one column is used to identify records. We’ll also use this example to demonstrate creating a table using a dict of keyword arguments.\n\nclass Publication: authors: str; year: int; title: str\npublications = db.create(Publication, pk=('authors', 'year'))\n\n\n\nTransforming tables\nDepending on the database type, this method can include transforms - the ability to modify the tables. Let’s go ahead and add a password field for our table called pwd.\n\nclass User: name:str; email: str; year_started:int; pwd:str\nusers = db.create(User, pk='name', transform=True)\nusers\n\n<Table user (name, email, year_started, pwd)>", + "crumbs": [ + "Home", + "Explanations", + "MiniDataAPI Spec" + ] + }, + { + "objectID": "explains/minidataapi.html#manipulating-data", + "href": "explains/minidataapi.html#manipulating-data", + "title": "MiniDataAPI Spec", + "section": "Manipulating data", + "text": "Manipulating data\nThe specification is designed to provide as straightforward CRUD API (Create, Read, Update, and Delete) as possible. Additional features like joins are out of scope.\n\n.insert()\nAdd a new record to the database. We want to support as many types as possible, for now we have tests for Python classes, dataclasses, and dicts. Returns an instance of the new record.\nHere’s how to add a record using a Python class:\n\nusers.insert(User(name='Braden', email='b@example.com', year_started=2018))\n\nUser(name='Braden', email='b@example.com', year_started=2018, pwd=None)\n\n\nWe can also use keyword arguments directly:\n\nusers.insert(name='Alma', email='a@example.com', year_started=2019)\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\nAnd now Charlie gets added via a Python dict.\n\nusers.insert({'name': 'Charlie', 'email': 'c@example.com', 'year_started': 2018})\n\nUser(name='Charlie', email='c@example.com', year_started=2018, pwd=None)\n\n\nAnd now TODOs. Note that the inserted row is returned:\n\ntodos.insert(Todo(title='Write MiniDataAPI spec', status='open', name='Braden'))\ntodos.insert(title='Implement SSE in FastHTML', status='open', name='Alma')\ntodo = todos.insert(dict(title='Finish development of FastHTML', status='closed', name='Charlie'))\ntodo\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nLet’s do the same with the Publications table.\n\npublications.insert(Publication(authors='Alma', year=2019, title='FastHTML'))\npublications.insert(authors='Alma', year=2030, title='FastHTML and beyond')\npublication= publications.insert((dict(authors='Alma', year=2035, title='FastHTML, the early years')))\npublication\n\nPublication(authors='Alma', year=2035, title='FastHTML, the early years')\n\n\n\n\nSquare bracket search []\nGet a single record by entering a primary key into a table object within square brackets. Let’s see if we can find Alma.\n\nuser = users['Alma']\nuser\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\nIf no record is found, a NotFoundError error is raised. Here we look for David, who hasn’t yet been added to our users table.\n\ntry: users['David']\nexcept NotFoundError: print(f'User not found')\n\nUser not found\n\n\nHere’s a demonstration of a ticket search, demonstrating how this works with non-string primary keys.\n\ntodos[1]\n\nTodo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden')\n\n\nCompound primary keys can be supplied in lists or tuples, in the order they were defined. In this case it is the authors and year columns.\nHere’s a query by compound primary key done with a list:\n\npublications[['Alma', 2019]]\n\nPublication(authors='Alma', year=2019, title='FastHTML')\n\n\nHere’s the same query done directly with index args.\n\npublications['Alma', 2030]\n\nPublication(authors='Alma', year=2030, title='FastHTML and beyond')\n\n\n\n\nParentheses search ()\nGet zero to many records by entering values with parentheses searches. If nothing is in the parentheses, then everything is returned.\n\nusers()\n\n[User(name='Braden', email='b@example.com', year_started=2018, pwd=None),\n User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\nWe can order the results.\n\nusers(order_by='name')\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Braden', email='b@example.com', year_started=2018, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\nWe can filter on the results:\n\nusers(where=\"name='Alma'\")\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None)]\n\n\nGenerally you probably want to use placeholders, to avoid SQL injection attacks:\n\nusers(\"name=?\", ('Alma',))\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None)]\n\n\nWe can limit results with the limit keyword:\n\nusers(limit=1)\n\n[User(name='Braden', email='b@example.com', year_started=2018, pwd=None)]\n\n\nIf we’re using the limit keyword, we can also use the offset keyword to start the query later.\n\nusers(limit=5, offset=1)\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\n\n\n.update()\nUpdate an existing record of the database. Must accept Python dict, dataclasses, and standard classes. Uses the primary key for identifying the record to be changed. Returns an instance of the updated record.\nHere’s with a normal Python class:\n\nuser\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\n\nuser.year_started = 2099\nusers.update(user)\n\nUser(name='Alma', email='a@example.com', year_started=2099, pwd=None)\n\n\nOr use a dict:\n\nusers.update(dict(name='Alma', year_started=2199, email='a@example.com'))\n\nUser(name='Alma', email='a@example.com', year_started=2199, pwd=None)\n\n\nOr use kwargs:\n\nusers.update(name='Alma', year_started=2149)\n\nUser(name='Alma', email='a@example.com', year_started=2149, pwd=None)\n\n\nIf the primary key doesn’t match a record, raise a NotFoundError.\nJohn hasn’t started with us yet so doesn’t get the chance yet to travel in time.\n\ntry: users.update(User(name='John', year_started=2024, email='j@example.com'))\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\n\n\n.delete()\nDelete a record of the database. Uses the primary key for identifying the record to be removed. Returns a table object.\nCharlie decides to not travel in time. He exits our little group.\n\nusers.delete('Charlie')\n\n<Table user (name, email, year_started, pwd)>\n\n\nIf the primary key value can’t be found, raises a NotFoundError.\n\ntry: users.delete('Charlies')\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\nIn John’s case, he isn’t time travelling with us yet so can’t be removed.\n\ntry: users.delete('John')\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\nDeleting records with compound primary keys requires providing the entire key.\n\npublications.delete(['Alma' , 2035])\n\n<Table publication (authors, year, title)>\n\n\n\n\nin keyword\nAre Alma and John contained in the Users table? Or, to be technically precise, is the item with the specified primary key value in this table?\n\n'Alma' in users, 'John' in users\n\n(True, False)\n\n\nAlso works with compound primary keys, as shown below. You’ll note that the operation can be done with either a list or tuple.\n\n['Alma', 2019] in publications\n\nTrue\n\n\nAnd now for a False result, where John has no publications.\n\n('John', 1967) in publications\n\nFalse\n\n\n\n\n.xtra()\nIf we set fields within the .xtra function to a particular value, then indexing is also filtered by those. This applies to every database method except for record creation. This makes it easier to limit users (or other objects) access to only things for which they have permission. This is a one-way operation, once set it can’t be undone for a particular table object.\nFor example, if we query all our records below without setting values via the .xtra function, we can see todos for everyone. Pay special attention to the id values of all three records, as we are about to filter most of them away.\n\ntodos()\n\n[Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden'),\n Todo(id=2, title='Implement SSE in FastHTML', detail=None, status='open', name='Alma'),\n Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]\n\n\nLet’s use .xtra to constrain results just to Charlie. We set the name field in Todos, but it could be any field defined for this table.\n\ntodos.xtra(name='Charlie')\n\nWe’ve now set a field to a value with .xtra, if we loop over all the records again, only those assigned to records with a name of Charlie will be displayed.\n\ntodos()\n\n[Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]\n\n\nThe in keyword is also affected. Only records with a name of Charlie will evaluate to be True. Let’s demonstrate by testing it with a Charlie record:\n\nct = todos[3]\nct\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nCharlie’s record has an ID of 3. Here we demonstrate that Charlie’s TODO can be found in the list of todos:\n\nct.id in todos\n\nTrue\n\n\nIf we try in with the other IDs the query fails because the filtering is now set to just records with a name of Charlie.\n\n1 in todos, 2 in todos\n\n(False, False)\n\n\n\ntry: todos[2]\nexcept NotFoundError: print('Record not found')\n\nRecord not found\n\n\nWe are also constrained by what records we can update. In the following example we try to update a TODO not named ‘Charlie’. Because the name is wrong, the .update function will raise a NotFoundError.\n\ntry: todos.update(Todo(id=1, title='Finish MiniDataAPI Spec', status='closed', name='Braden'))\nexcept NotFoundError as e: print('Record not updated')\n\nRecord not updated\n\n\nUnlike poor Braden, Charlie isn’t filtered out. Let’s update his TODO.\n\ntodos.update(Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie'))\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nFinally, once constrained by .xtra, only records with Charlie as the name can be deleted.\n\ntry: todos.delete(1)\nexcept NotFoundError as e: print('Record not updated')\n\nRecord not updated\n\n\nCharlie’s TODO was to finish development of FastHTML. While the framework will stabilize, like any good project it will see new features added and the odd bug corrected for many years to come. Therefore, Charlie’s TODO is nonsensical. Let’s delete it.\n\ntodos.delete(ct.id)\n\n<Table todo (id, title, detail, status, name)>\n\n\nWhen a TODO is inserted, the xtra fields are automatically set. This ensures that we don’t accidentally, for instance, insert items for others users. Note that here we don’t set the name field, but it’s still included in the resultant row:\n\nct = todos.insert(Todo(title='Rewrite personal site in FastHTML', status='open'))\nct\n\nTodo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')\n\n\nIf we try to change the username to someone else, the change is ignored, due to xtra:\n\nct.name = 'Braden'\ntodos.update(ct)\n\nTodo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')", + "crumbs": [ + "Home", + "Explanations", + "MiniDataAPI Spec" + ] + }, + { + "objectID": "explains/minidataapi.html#sql-first-design", + "href": "explains/minidataapi.html#sql-first-design", + "title": "MiniDataAPI Spec", + "section": "SQL-first design", + "text": "SQL-first design\n\nusers = None\nUser = None\n\n\nusers = db.t.user\nusers\n\n<Table user (name, email, year_started, pwd)>\n\n\n(This section needs to be documented properly.)\nFrom the table objects we can extract a Dataclass version of our tables. Usually this is given an singular uppercase version of our table name, which in this case is User.\n\nUser = users.dataclass()\n\n\nUser(name='Braden', email='b@example.com', year_started=2018)\n\nUser(name='Braden', email='b@example.com', year_started=2018, pwd=UNSET)", + "crumbs": [ + "Home", + "Explanations", + "MiniDataAPI Spec" + ] + }, + { + "objectID": "explains/minidataapi.html#implementations", + "href": "explains/minidataapi.html#implementations", + "title": "MiniDataAPI Spec", + "section": "Implementations", + "text": "Implementations\n\nImplementing MiniDataAPI for a new datastore\nFor creating new implementations, the code examples in this specification are the test case for the API. New implementations should pass the tests in order to be compliant with the specification.\n\n\nImplementations\n\nfastlite - The original implementation, only for Sqlite\nfastsql - An SQL database agnostic implementation based on the excellent SQLAlchemy library.", + "crumbs": [ + "Home", + "Explanations", + "MiniDataAPI Spec" + ] + }, + { + "objectID": "explains/routes.html", + "href": "explains/routes.html", + "title": "Routes", + "section": "", + "text": "Behaviour in FastHTML apps is defined by routes. The syntax is largely the same as the wonderful FastAPI (which is what you should be using instead of this if you’re creating a JSON service. FastHTML is mainly for making HTML web apps, not APIs).\nNote that you need to include the types of your parameters, so that FastHTML knows what to pass to your function. Here, we’re just expecting a string:\nfrom fasthtml.common import *\napp = FastHTML()\n\n@app.get('/user/{nm}')\ndef get_nm(nm:str): return f\"Good day to you, {nm}!\"\nNormally you’d save this into a file such as main.py, and then run it in uvicorn using:\nHowever, for testing, we can use Starlette’s TestClient to try it out:\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\nr = client.get('/user/Jeremy')\nr\n\n<Response [200 OK]>\nTestClient uses httpx behind the scenes, so it returns a httpx.Response, which has a text attribute with our response body:\nr.text\n\n'Good day to you, Jeremy!'\nIn the previous example, the function name (get_nm) didn’t actually matter – we could have just called it _, for instance, since we never actually call it directly. It’s just called through HTTP. In fact, we often do call our functions _ when using this style of route, since that’s one less thing we have to worry about, naming.\nAn alternative approach to creating a route is to use app.route instead, in which case, you make the function name the HTTP method you want. Since this is such a common pattern, you might like to give a shorter name to app.route – we normally use rt:\nrt = app.route\n\n@rt('/')\ndef post(): return \"Going postal!\"\n\nclient.post('/').text\n\n'Going postal!'", + "crumbs": [ + "Home", + "Explanations", + "Routes" + ] + }, + { + "objectID": "explains/routes.html#combining-routes", + "href": "explains/routes.html#combining-routes", + "title": "Routes", + "section": "Combining Routes", + "text": "Combining Routes\nSometimes a FastHTML project can grow so weildy that putting all the routes into main.py becomes unweildy. Or, we install a FastHTML- or Starlette-based package that requires us to add routes.\nFirst let’s create a books.py module, that represents all the user-related views:\n\n# books.py\nbooks_app, rt = fast_app()\n\nbooks = ['A Guide to FastHTML', 'FastHTML Cookbook', 'FastHTML in 24 Hours']\n\n@rt(\"/\", name=\"list\")\ndef get():\n return Titled(\"Books\", *[P(book) for book in books])\n\nLet’s mount it in our main module:\nfrom books import books_app\n\n1app, rt = fast_app(routes=[Mount(\"/books\", books_app, name=\"books\")])\n\n@rt(\"/\")\ndef get():\n return Titled(\"Dashboard\",\n2 P(A(href=\"/books\")(\"Books\")),\n Hr(),\n3 P(A(link=uri(\"books:list\"))(\"Books\")),\n )\n\nserve()\n\n1\n\nWe use starlette.Mount to add the route to our routes list. We provide the name of books to make discovery and management of the links easier. More on that in items 2 and 3 of this annotations list\n\n2\n\nThis example link to the books list view is hand-crafted. Obvious in purpose, it makes changing link patterns in the future harder\n\n3\n\nThis example link uses the named URL route for the books. The advantage of this approach is it makes management of large numbers of link items easier.", + "crumbs": [ + "Home", + "Explanations", + "Routes" + ] + }, + { + "objectID": "explains/background_tasks.html", + "href": "explains/background_tasks.html", + "title": "Background Tasks", + "section": "", + "text": "Useful for operations where the user gets a response quickly but doesn’t need to wait for the operation to finish. Typical scenarios include:", + "crumbs": [ + "Home", + "Explanations", + "Background Tasks" + ] + }, + { + "objectID": "explains/background_tasks.html#a-simple-background-task-example", + "href": "explains/background_tasks.html#a-simple-background-task-example", + "title": "Background Tasks", + "section": "A simple background task example", + "text": "A simple background task example\nIn this example we are attaching a task to FtResponse by assigning it via the background argument. When the page is visited, it will display ‘Simple Background Task Example’ almost instantly, while in the terminal it will slowly count upward from 0.\n\n\nmain.py\n\nfrom fasthtml.common import *\nfrom starlette.background import BackgroundTask\nfrom time import sleep\n\napp, rt = fast_app()\n\n1def counter(loops:int):\n \"Slowly print integers to the terminal\"\n for i in range(loops):\n print(i)\n sleep(i)\n\n@rt\ndef index():\n2 task = BackgroundTask(counter, loops=5)\n3 return Titled('Simple Background Task Example'), task\n\nserve()\n\n\n1\n\ncounter is our task function. There is nothing special about it, although it is a good practice for its arguments to be serializable as JSON\n\n2\n\nWe use starlette.background.BackgroundTask to turn counter() into a background task\n\n3\n\nTo add a background task to a handler, we add it to the return values at the top level of the response.", + "crumbs": [ + "Home", + "Explanations", + "Background Tasks" + ] + }, + { + "objectID": "explains/background_tasks.html#a-more-realistic-example", + "href": "explains/background_tasks.html#a-more-realistic-example", + "title": "Background Tasks", + "section": "A more realistic example", + "text": "A more realistic example\nLet’s imagine that we are accessing a slow-to-process critical service. We don’t want our users to have to wait. While we could set up SSE to notify on completion, instead we decide to periodically check to see if the status of their record has changed.\n\nSimulated Slow API Service\nFirst, create a very simple slow timestamp API. All it does is stall requests for a few seconds before returning JSON containing timestamps.\n# slow_api.py\nfrom fasthtml.common import *\nfrom time import sleep, time\n\napp, rt = fast_app()\n\n@rt('/slow')\ndef slow(ts: int):\n1 sleep(3)\n2 return dict(request_time=ts, response_time=int(time()))\n\nserve(port=8123)\n\n1\n\nThis represents slow processing.\n\n2\n\nReturns both the task’s original timestamp and the time after completion\n\n\n\n\nMain FastHTML app\nNow let’s create a user-facing app that uses this API to fetch the timestamp from the glacially slow service.\n# main.py\nfrom fasthtml.common import *\nfrom starlette.background import BackgroundTask\nimport time\nimport httpx\n\napp, rt = fast_app()\n\ndb = database(':memory:')\n\n1class TStamp: request_time: int; response_time: int\n\ntstamps = db.create(TStamp, pk='request_time')\n\n2def task_submit(request_time: int):\n client = httpx.Client()\n3 response = client.post(f'http://127.0.0.1:8123/slow?ts={request_time}')\n4 tstamps.insert(**response.json())\n\n@rt\ndef submit():\n \"Route that initiates a background task and returns immediately.\"\n request_time = int(time.time())\n5 task = BackgroundTask(task_submit, request_time=request_time)\n6 return P(f'Request submitted at: {request_time}'), task\n\n@rt\n7def show_tstamps(): return Ul(map(Li, tstamps()))\n\n@rt\ndef index():\n return Titled('Background Task Dashboard',\n8 P(Button('Press to call slow service',\n hx_post=submit, hx_target='#res')),\n H2('Responses from Tasks'),\n P('', id='res'),\n Div(Ul(map(Li, tstamps())),\n9 hx_get=show_tstamps, hx_trigger='every 5s'),\n )\n\nserve()\n\n1\n\nTracks when requests are sent and responses received\n\n2\n\nTask function calling slow service to be run in the background of a route handler. It is common but not necessary to prefix task functions with ‘task_’\n\n3\n\nCall the slow API service (simulating a time-consuming operation)\n\n4\n\nStore both timestamps in our database\n\n5\n\nCreate a background task by passing in the function to a BackgroundTask object, followed by any arguments.\n\n6\n\nIn FtResponse, use the background keyword argument to set the task to be run after the HTTP response is generated.\n\n7\n\nEndpoint that displays all recorded timestamp pairs.\n\n8\n\nWhen this button is pressed, the ‘submit’ handler will respond instantly. The task_submit function will insert the slow API response into the db later.\n\n9\n\nEvery 5 seconds get the tstamps stored in the DB.\n\n\n\n\n\n\n\n\nTip\n\n\n\nIn the example above we use a synchronous background task function set in the FtResponse of a synchronous handler. However, we can also use asynchronous functions and handlers.", + "crumbs": [ + "Home", + "Explanations", + "Background Tasks" + ] + }, + { + "objectID": "explains/background_tasks.html#multiple-background-tasks-in-a-handler", + "href": "explains/background_tasks.html#multiple-background-tasks-in-a-handler", + "title": "Background Tasks", + "section": "Multiple background tasks in a handler", + "text": "Multiple background tasks in a handler\nIt is possible to add multiple background tasks to an FtResponse.\n\n\n\n\n\n\nWarning\n\n\n\nMultiple background tasks on a background task are executed in order. In the case a task raises an exception, following tasks will not get the opportunity to be executed.\n\n\nfrom starlette.background import BackgroundTasks\n\n@rt\nasync def signup(email, username):\n tasks = BackgroundTasks()\n tasks.add_task(send_welcome_email, to_address=email)\n tasks.add_task(send_admin_notification, username=username)\n return Titled('Signup successful!'), tasks\n\nasync def send_welcome_email(to_address):\n ...\n\nasync def send_admin_notification(username):\n ...", + "crumbs": [ + "Home", + "Explanations", + "Background Tasks" + ] + }, + { + "objectID": "explains/background_tasks.html#background-tasks-at-scale", + "href": "explains/background_tasks.html#background-tasks-at-scale", + "title": "Background Tasks", + "section": "Background tasks at scale", + "text": "Background tasks at scale\nBackground tasks enhance application performance both for users and apps by handling blocking processes asynchronously, even when defined as synchronous functions.\nWhen FastHTML’s background tasks aren’t enough and your app runs slow on a server, manually offloading processes to the multiprocessing library is an option. By doing so you can leverage multiple cores and bypass the GIL, significantly improving speed and performance at the cost of added complexity.\nSometimes a server reaches its processing limits, and this is where distributed task queue systems like Celery and Dramatiq come into play. They are designed to distribute tasks across multiple servers, offering improved observability, retry mechanisms, and persistence, at the cost of substantially increased complexity.\nHowever most applications work well with built-in background tasks like those in FastHTML, which we recommend trying first. Writing these functions with JSON-serializable arguments ensures straightforward conversion to other concurrency methods if needed.", + "crumbs": [ + "Home", + "Explanations", + "Background Tasks" + ] + }, + { + "objectID": "explains/oauth.html", + "href": "explains/oauth.html", + "title": "OAuth", + "section": "", + "text": "OAuth is an open standard for ‘access delegation’, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. It is the mechanism that enables “Log in with Google” on many sites, saving you from having to remember and manage yet another password. Like many auth-related topics, there’s a lot of depth and complexity to the OAuth standard, but once you understand the basic usage it can be a very convenient alternative to managing your own user accounts.\nOn this page you’ll see how to use OAuth with FastHTML to implement some common pieces of functionality.", + "crumbs": [ + "Home", + "Explanations", + "OAuth" + ] + }, + { + "objectID": "explains/oauth.html#creating-an-client", + "href": "explains/oauth.html#creating-an-client", + "title": "OAuth", + "section": "Creating an Client", + "text": "Creating an Client\nFastHTML has Client classes for managing settings and state for different OAuth providers. Currently implemented are: GoogleAppClient, GitHubAppClient, HuggingFaceClient and DiscordAppClient - see the source if you need to add other providers. You’ll need a client_id and client_secret from the provider (see the from-scratch example later in this page for an example of registering with GitHub) to create the client. We recommend storing these in environment variables, rather than hardcoding them in your code.\n\nimport os\nfrom fasthtml.oauth import GoogleAppClient\nclient = GoogleAppClient(os.getenv(\"AUTH_CLIENT_ID\"),\n os.getenv(\"AUTH_CLIENT_SECRET\"))\n\nThe client is used to obtain a login link and to manage communications between your app and the OAuth provider (client.login_link(redirect_uri=\"/redirect\")).", + "crumbs": [ + "Home", + "Explanations", + "OAuth" + ] + }, + { + "objectID": "explains/oauth.html#using-the-oauth-class", + "href": "explains/oauth.html#using-the-oauth-class", + "title": "OAuth", + "section": "Using the OAuth class", + "text": "Using the OAuth class\nOnce you’ve set up a client, adding OAuth to a FastHTML app can be as simple as:\n\nfrom fasthtml.oauth import OAuth\nfrom fasthtml.common import FastHTML, RedirectResponse\n\nclass Auth(OAuth):\n def get_auth(self, info, ident, session, state):\n email = info.email or ''\n if info.email_verified and email.split('@')[-1]=='answer.ai':\n return RedirectResponse('/', status_code=303)\n\napp = FastHTML()\noauth = Auth(app, client)\n\n@app.get('/')\ndef home(auth): return P('Logged in!'), A('Log out', href='/logout')\n\n@app.get('/login')\ndef login(req): return Div(P(\"Not logged in\"), A('Log in', href=oauth.login_link(req)))\n\nThere’s a fair bit going on here, so let’s unpack what’s happening in that code:\n\nOAuth (and by extension our custom Auth class) has a number of default arguments, including some key URLs: redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login'. It will create and handle the redirect and logout paths, and it’s up to you to handle /login (where unsuccessful login attempts will be redirected) and /error (for oauth errors).\nWhen we run oauth = Auth(app, client) it adds the redirect and logout paths to the app and also adds some beforeware. This beforeware runs on any requests (apart from any specified with the skip parameter).\n\nThe added beforeware specifies some app behaviour:\n\nIf someone who isn’t logged in attempts to visit our homepage (/) here, they will be redirected to /login.\nIf they are logged in, it calls a check_invalid method. This defaults to False, which let’s the user continue to the page they requested. The behaviour can be modified by defining your own check_invalid method in the Auth class - for example, you could have this forcibly log out users who have recently been banned.\n\nSo how does someone log in? If they visit (or are redirected to) the login page at /login, we show them a login link. This sends them to the OAuth provider, where they’ll go through the steps of selecting their account, giving permissions etc. Once done they will be redirected back to /redirect. Behind the scenes a code that comes as part of their request gets turned into user info, which is then passed to the key function get_auth(self, info, ident, session, state). Here is where you’d handle looking up or adding a user in a database, checking for some condition (for example, this code checks if the email is an answer.ai email address) or choosing the destination based on state. The arguments are:\n\nself: the Auth object, which you can use to access the client (self.cli)\ninfo: the information provided by the OAuth provider, typically including a unique user id, email address, username and other metadata.\nident: a unique identifier for this user. What this looks like varies between providers. This is useful for managing a database of users, for example.\nsession: the current session, that you can store information in securely\nstate: you can optionally pass in some state when creating the login link. This persists and is returned after the user goes through the Oath steps, which is useful for returning them to the same page they left. It can also be used as added security against CSRF attacks.\n\nIn our example, we check the email in info (we use a GoogleAppClient, not all providers will include an email). If we aren’t happy, and get_auth returns False or nothing (as in the case here for non-answerai people) then the user is redirected back to the login page. But if everything looks good we return a redirect to the homepage, and an auth key is added to the session and the scope containing the users identity ident. So, for example, in the homepage route we could use auth to look up this particular user’s profile info and customize the page accordingly. This auth will persist in their session until they clear the browser cache, so by default they’ll stay logged in. To log them out, remove it ( session.pop('auth', None)) or send them to /logout which will do that for you.", + "crumbs": [ + "Home", + "Explanations", + "OAuth" + ] + }, + { + "objectID": "explains/oauth.html#explaining-oauth-with-a-from-scratch-implementation", + "href": "explains/oauth.html#explaining-oauth-with-a-from-scratch-implementation", + "title": "OAuth", + "section": "Explaining OAuth with a from-scratch implementation", + "text": "Explaining OAuth with a from-scratch implementation\nHopefully the example above is enough to get you started. You can also check out the (fairly minimal) source code where this is implemented, and the examples here.\nIf you’re wanting to learn more about how this works, and to see where you might add additional functionality, the rest of this page will walk through some examples without the OAuth convenience class, to illustrate the concepts. This was written before said OAuth class was available, and is kept here for educational purposes - we recommend you stick with the new approach shown above in most cases.", + "crumbs": [ + "Home", + "Explanations", + "OAuth" + ] + }, + { + "objectID": "explains/oauth.html#a-minimal-login-flow-github", + "href": "explains/oauth.html#a-minimal-login-flow-github", + "title": "OAuth", + "section": "A Minimal Login Flow (GitHub)", + "text": "A Minimal Login Flow (GitHub)\nLet’s begin by building a minimal ‘Sign in with GitHub’ flow. This will demonstrate the basic steps of OAuth.\nOAuth requires a “provider” (in this case, GitHub) to authenticate the user. So the first step when setting up our app is to register with GitHub to set things up.\nGo to https://github.com/settings/developers and click “New OAuth App”. Fill in the form with the following values, then click ‘Register application’.\n\nApplication name: Your app name\nHomepage URL: http://localhost:8000 (or whatever URL you’re using - you can change this later)\nAuthorization callback URL: http://localhost:8000/auth_redirect (you can modify this later too)\n\n\n\n\nAfter you register, you’ll see a screen where you can view the client ID and generate a client secret. Store these values in a safe place. You’ll use them to create a GitHubAppClient object in FastHTML.\nThis client object is responsible for handling the parts of the OAuth flow which depend on direct communication between your app and GitHub, as opposed to interactions which go through the user’s browser via redirects.\nHere is how to setup the client object:\nclient = GitHubAppClient(\n client_id=\"your_client_id\",\n client_secret=\"your_client_secret\"\n)\nYou should also save the path component of the authorization callback URL which you provided on registration.\nThis route is where GitHub will redirect the user’s browser in order to send an authorization code to your app. You should save only the URL’s path component rather than the entire URL because you want your code to work automatically in deployment, when the host and port part of the URL change from localhost:8000 to your real DNS name.\nSave the special authorization callback path under an obvious name:\nauth_callback_path = \"/auth_redirect\"\n\n\n\n\n\n\nNote\n\n\n\nIt’s recommended to store the client ID, and secret, in environment variables, rather than hardcoding them in your code.\n\n\nWhen the user visit a normal page of your app, if they are not already logged in, then you’ll want to redirect them to your app’s login page, which will live at the /login path. We accomplish that by using this piece of “beforeware”, which defines logic which runs before other work for all routes except ones we specify to be skipped:\ndef before(req, session):\n auth = req.scope['auth'] = session.get('user_id', None)\n if not auth: return RedirectResponse('/login', status_code=303)\n counts.xtra(name=auth)\nbware = Beforeware(before, skip=['/login', auth_callback_path])\nWe configure the beforeware to skip /login because that’s where the user goes to login, and we also skip the special authorization callback path because that is used by OAuth itself to receive information from GitHub.\nIt’s only at your login page that we start the OAuth flow. To start the OAuth flow, you need to give the user a link to GitHub’s login for your app. You’ll need the client object to generate that link, and the client object will in turn need the full authorization callback URL, which we need to build from the authorization callback path, so it is a multi-step process to produce this GitHub login link.\nHere is an implementation of your own /login route handler. It generates the GitHub login link and presents it to the user:\n@app.get('/login')\ndef login(request)\n redir = redir_url(request,auth_callback_path)\n login_link = client.login_link(redir)\n return P(A('Login with GitHub', href=login_link)) \nOnce the user follows that link, GitHub will ask them to grant permission to your app to access their GitHub account. If they agree, GitHub will redirect them back to your app’s authorization callback URL, carrying an authorization code which your app can use to generate an access token. To receive this code, you need to set up a route in FastHTML that listens for requests at the authorization callback path. For example:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str):\n return P(f\"code: {code}\")\nThis authorization code is temporary, and is used by your app to directly ask the provider for user information like an access token.\nTo recap, you can think of the exchange so far as:\n\nUser to us: “I want to log in with you, app.”\nUs to User: “Okay but first, here’s a special link to log in with GitHub”\nUser to GitHub: “I want to log in with you, GitHub, to use this app.”\nGitHub to User: “OK, redirecting you back to the app’s URL (with an auth code)”\nUser to Us: “Hi again, app. Here’s the GitHub auth code you need to ask GitHub for info about me” (delivered via /auth_redirect?code=...)\n\nThe final steps we need to implement are as follows:\n\nUs to GitHUb: “A user just gave me this auth code. May I have the user info (e.g., an access token)?”\nGitHub to us: “Since you have an auth code, here’s the user info”\n\nIt’s critical for us to derive the user info from the auth code immediately in the authorization callback, because the auth code may be used only once. So we use it that once in order to get information like an access token, which will remain valid for longer.\nTo go from the auth code to user info, you use info = client.retr_info(code,redirect_uri). From the user info, you can extract the user_id, which is a unique identifier for the user:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = info[client.id_key]\n return P(f\"User id: {user_id}\")\nBut we want the user ID not to print it but to remember the user.\nSo let us store it in the session object, to remember who is logged in:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request, session):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = user_info[client.id_key] # get their ID\n session['user_id'] = user_id # save ID in the session\n return RedirectResponse('/', status_code=303)\nThe session object is derived from values visible to the user’s browser, but it is cryptographically signed so the user can’t read it themselves. This makes it safe to store even information we don’t want to expose to the user.\nFor larger quantities of data, we’d want to save that information in a database and use the session to hold keys to lookup information from that database.\nHere’s a minimal app that puts all these pieces together. It uses the user info to get the user_id. It stores that in the session object. It then uses the user_id as a key into a database, which tracks how frequently every user has hit an increment button.\nimport os\nfrom fasthtml.common import *\nfrom fasthtml.oauth import GitHubAppClient, redir_url\n\ndb = database('data/counts.db')\ncounts = db.t.counts\nif counts not in db.t: counts.create(dict(name=str, count=int), pk='name')\nCount = counts.dataclass()\n\n# Auth client setup for GitHub\nclient = GitHubAppClient(os.getenv(\"AUTH_CLIENT_ID\"), \n os.getenv(\"AUTH_CLIENT_SECRET\"))\nauth_callback_path = \"/auth_redirect\"\n\ndef before(req, session):\n # if not logged in, we send them to our login page\n # logged in means:\n # - 'user_id' in the session object, \n # - 'auth' in the request object\n auth = req.scope['auth'] = session.get('user_id', None)\n if not auth: return RedirectResponse('/login', status_code=303)\n counts.xtra(name=auth)\nbware = Beforeware(before, skip=['/login', auth_callback_path])\n\napp = FastHTML(before=bware)\n\n# User asks us to Login\n@app.get('/login')\ndef login(request):\n redir = redir_url(request,auth_callback_path)\n login_link = client.login_link(redir)\n # we tell user to login at github\n return P(A('Login with GitHub', href=login_link)) \n\n# User comes back to us with an auth code from Github\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request, session):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = user_info[client.id_key] # get their ID\n session['user_id'] = user_id # save ID in the session\n # create a db entry for the user\n if user_id not in counts: counts.insert(name=user_id, count=0)\n return RedirectResponse('/', status_code=303)\n\n@app.get('/')\ndef home(auth):\n return Div(\n P(\"Count demo\"),\n P(f\"Count: \", Span(counts[auth].count, id='count')),\n Button('Increment', hx_get='/increment', hx_target='#count'),\n P(A('Logout', href='/logout'))\n )\n\n@app.get('/increment')\ndef increment(auth):\n c = counts[auth]\n c.count += 1\n return counts.upsert(c).count\n\n@app.get('/logout')\ndef logout(session):\n session.pop('user_id', None)\n return RedirectResponse('/login', status_code=303)\n\nserve()\nSome things to note:\n\nThe before function is used to check if the user is authenticated. If not, they are redirected to the login page.\nTo log the user out, we remove the user ID from the session.\nCalling counts.xtra(name=auth) ensures that only the row corresponding to the current user is accessible when responding to a request. This is often nicer than trying to remember to filter the data in every route, and lowers the risk of accidentally leaking data.\nIn the auth_redirect route, we store the user ID in the session and create a new row in the user_counts table if it doesn’t already exist.\n\nYou can find more heavily-commented version of this code in the oauth directory in fasthtml-example, along with an even more minimal example. More examples may be added in the future.\n\nRevoking Tokens (Google)\nWhen the user in the example above logs out, we remove their user ID from the session. However, the user is still logged in to GitHub. If they click ‘Login with GitHub’ again, they’ll be redirected back to our site without having to log in again. This is because GitHub remembers that they’ve already granted our app permission to access their account. Most of the time this is convenient, but for testing or security purposes you may want a way to revoke this permission.\nAs a user, you can usually revoke access to an app from the provider’s website (for example, https://github.com/settings/applications). But as a developer, you can also revoke access programmatically - at least with some providers. This requires keeping track of the access token (stored in client.token[\"access_token\"] after you call retr_info), and sending a request to the provider’s revoke URL:\nauth_revoke_url = \"https://accounts.google.com/o/oauth2/revoke\"\ndef revoke_token(token):\n response = requests.post(auth_revoke_url, params={\"token\": token})\n return response.status_code == 200 # True if successful\nNot all providers support token revocation, and it is not built into FastHTML clients at the moment.\n\n\nUsing State (Hugging Face)\nImagine a user (not logged in) comes to your AI image editing site, starts testing things out, and then realizes they need to sign in before they can click “Run (Pro)” on the edit they’re working on. They click “Sign in with Hugging Face”, log in, and are redirected back to your site. But now they’ve lost their in-progress edit and are left just looking at the homepage! This is an example of a case where you might want to keep track of some additional state. Another strong use case for being able to pass some uniqie state through the OAuth flow is to prevent something called a CSRF attack. To add a state string to the OAuth flow, include a state argument when creating the login link:\n# in login page:\nlink = A('Login with GitHub', href=client.login_link(state='current_prompt: add a unicorn'))\n\n# in auth_redirect:\n@app.get('/auth_redirect')\ndef auth_redirect(code:str, session, state:str=None):\n print(f\"state: {state}\") # Use as needed\n ...\nThe state string is passed through the OAuth flow and back to your site.\n\n\nA Work in Progress\nThis page (and OAuth support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!", + "crumbs": [ + "Home", + "Explanations", + "OAuth" + ] + }, + { + "objectID": "api/core.html", + "href": "api/core.html", + "title": "Core", + "section": "", + "text": "This is the source code to fasthtml. You won’t need to read this unless you want to understand how things are built behind the scenes, or need full details of a particular API. The notebook is converted to the Python module fasthtml/core.py using nbdev.", + "crumbs": [ + "Home", + "Source", + "Core" + ] + }, + { + "objectID": "api/core.html#imports-and-utils", + "href": "api/core.html#imports-and-utils", + "title": "Core", + "section": "Imports and utils", + "text": "Imports and utils\n\nimport time\n\nfrom IPython import display\nfrom enum import Enum\nfrom pprint import pprint\n\nfrom fastcore.test import *\nfrom starlette.testclient import TestClient\nfrom starlette.requests import Headers\nfrom starlette.datastructures import UploadFile\n\nWe write source code first, and then tests come after. The tests serve as both a means to confirm that the code works and also serves as working examples. The first exported function, parsed_date, is an example of this pattern.\n\nsource\n\nparsed_date\n\n parsed_date (s:str)\n\nConvert s to a datetime\n\nparsed_date('2pm')\n\ndatetime.datetime(2025, 5, 29, 14, 0)\n\n\n\nisinstance(date.fromtimestamp(0), date)\n\nTrue\n\n\n\nsource\n\n\nsnake2hyphens\n\n snake2hyphens (s:str)\n\nConvert s from snake case to hyphenated and capitalised\n\nsnake2hyphens(\"snake_case\")\n\n'Snake-Case'\n\n\n\nsource\n\n\nHtmxHeaders\n\n HtmxHeaders (boosted:str|None=None, current_url:str|None=None,\n history_restore_request:str|None=None, prompt:str|None=None,\n request:str|None=None, target:str|None=None,\n trigger_name:str|None=None, trigger:str|None=None)\n\n\ndef test_request(url: str='/', headers: dict={}, method: str='get') -> Request:\n scope = {\n 'type': 'http',\n 'method': method,\n 'path': url,\n 'headers': Headers(headers).raw,\n 'query_string': b'',\n 'scheme': 'http',\n 'client': ('127.0.0.1', 8000),\n 'server': ('127.0.0.1', 8000),\n }\n receive = lambda: {\"body\": b\"\", \"more_body\": False}\n return Request(scope, receive)\n\n\nh = test_request(headers=Headers({'HX-Request':'1'}))\n_get_htmx(h.headers)\n\nHtmxHeaders(boosted=None, current_url=None, history_restore_request=None, prompt=None, request='1', target=None, trigger_name=None, trigger=None)", + "crumbs": [ + "Home", + "Source", + "Core" + ] + }, + { + "objectID": "api/core.html#request-and-response", + "href": "api/core.html#request-and-response", + "title": "Core", + "section": "Request and response", + "text": "Request and response\n\ntest_eq(_fix_anno(Union[str,None], 'a'), 'a')\ntest_eq(_fix_anno(float, 0.9), 0.9)\ntest_eq(_fix_anno(int, '1'), 1)\ntest_eq(_fix_anno(int, ['1','2']), 2)\ntest_eq(_fix_anno(list[int], ['1','2']), [1,2])\ntest_eq(_fix_anno(list[int], '1'), [1])\n\n\nd = dict(k=int, l=List[int])\ntest_eq(_form_arg('k', \"1\", d), 1)\ntest_eq(_form_arg('l', \"1\", d), [1])\ntest_eq(_form_arg('l', [\"1\",\"2\"], d), [1,2])\n\n\nsource\n\nHttpHeader\n\n HttpHeader (k:str, v:str)\n\n\n_to_htmx_header('trigger_after_settle')\n\n'HX-Trigger-After-Settle'\n\n\n\nsource\n\n\nHtmxResponseHeaders\n\n HtmxResponseHeaders (location=None, push_url=None, redirect=None,\n refresh=None, replace_url=None, reswap=None,\n retarget=None, reselect=None, trigger=None,\n trigger_after_settle=None, trigger_after_swap=None)\n\nHTMX response headers\n\nHtmxResponseHeaders(trigger_after_settle='hi')\n\nHttpHeader(k='HX-Trigger-After-Settle', v='hi')\n\n\n\nsource\n\n\nform2dict\n\n form2dict (form:starlette.datastructures.FormData)\n\nConvert starlette form data to a dict\n\nd = [('a',1),('a',2),('b',0)]\nfd = FormData(d)\nres = form2dict(fd)\ntest_eq(res['a'], [1,2])\ntest_eq(res['b'], 0)\n\n\nsource\n\n\nparse_form\n\n parse_form (req:starlette.requests.Request)\n\nStarlette errors on empty multipart forms, so this checks for that situation\n\nsource\n\n\nJSONResponse\n\n JSONResponse (content:Any, status_code:int=200,\n headers:Optional[Mapping[str,str]]=None,\n media_type:str|None=None,\n background:starlette.background.BackgroundTask|None=None)\n\nSame as starlette’s version, but auto-stringifies non serializable types\n\nasync def f(req):\n def _f(p:HttpHeader): ...\n p = first(_params(_f).values())\n result = await _from_body(req, p)\n return JSONResponse(result.__dict__)\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\n\nd = dict(k='value1',v=['value2','value3'])\nresponse = client.post('/', data=d)\nprint(response.json())\n\n{'k': 'value1', 'v': 'value3'}\n\n\n\nasync def f(req): return Response(str(req.query_params.getlist('x')))\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['GET'])]))\nclient.get('/?x=1&x=2').text\n\n\"['1', '2']\"\n\n\n\ndef g(req, this:Starlette, a:str, b:HttpHeader): ...\n\nasync def f(req):\n a = await _wrap_req(req, _params(g))\n return Response(str(a))\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\nresponse = client.post('/?a=1', data=d)\nprint(response.text)\n\n[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]\n\n\n\ndef g(req, this:Starlette, a:str, b:HttpHeader): ...\n\nasync def f(req):\n a = await _wrap_req(req, _params(g))\n return Response(str(a))\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\nresponse = client.post('/?a=1', data=d)\nprint(response.text)\n\n[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]\n\n\nMissing Request Params\nIf a request param has a default value (e.g. a:str=''), the request is valid even if the user doesn’t include the param in their request.\n\ndef g(req, this:Starlette, a:str=''): ...\n\nasync def f(req):\n a = await _wrap_req(req, _params(g))\n return Response(str(a))\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\nresponse = client.post('/', json={}) # no param in request\nprint(response.text)\n\n[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '']\n\n\nIf we remove the default value and re-run the request, we should get the following error Missing required field: a.\n\ndef g(req, this:Starlette, a:str): ...\n\nasync def f(req):\n a = await _wrap_req(req, _params(g))\n return Response(str(a))\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\nresponse = client.post('/', json={}) # no param in request\nprint(response.text)\n\nMissing required field: a\n\n\n\nsource\n\n\nflat_xt\n\n flat_xt (lst)\n\nFlatten lists\n\nx = ft('a',1)\ntest_eq(flat_xt([x, x, [x,x]]), (x,)*4)\ntest_eq(flat_xt(x), (x,))\n\n\nsource\n\n\nBeforeware\n\n Beforeware (f, skip=None)\n\nInitialize self. See help(type(self)) for accurate signature.", + "crumbs": [ + "Home", + "Source", + "Core" + ] + }, + { + "objectID": "api/core.html#websockets-sse", + "href": "api/core.html#websockets-sse", + "title": "Core", + "section": "Websockets / SSE", + "text": "Websockets / SSE\n\ndef on_receive(self, msg:str): return f\"Message text was: {msg}\"\nc = _ws_endp(on_receive)\ncli = TestClient(Starlette(routes=[WebSocketRoute('/', _ws_endp(on_receive))]))\nwith cli.websocket_connect('/') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\n assert data == 'Message text was: Hi!'\n\n\nsource\n\nEventStream\n\n EventStream (s)\n\nCreate a text/event-stream response from s\n\nsource\n\n\nsignal_shutdown\n\n signal_shutdown ()", + "crumbs": [ + "Home", + "Source", + "Core" + ] + }, + { + "objectID": "api/core.html#routing-and-application", + "href": "api/core.html#routing-and-application", + "title": "Core", + "section": "Routing and application", + "text": "Routing and application\n\nsource\n\nuri\n\n uri (_arg, **kwargs)\n\n\nsource\n\n\ndecode_uri\n\n decode_uri (s)\n\n\nsource\n\n\nStringConvertor.to_string\n\n StringConvertor.to_string (value:str)\n\n\nsource\n\n\nHTTPConnection.url_path_for\n\n HTTPConnection.url_path_for (name:str, **path_params)\n\n\nsource\n\n\nflat_tuple\n\n flat_tuple (o)\n\nFlatten lists\n\nsource\n\n\nnoop_body\n\n noop_body (c, req)\n\nDefault Body wrap function which just returns the content\n\nsource\n\n\nrespond\n\n respond (req, heads, bdy)\n\nDefault FT response creation function\nRender fragment if HX-Request header is present and HX-History-Restore-Request header is absent.\n\nsource\n\n\nis_full_page\n\n is_full_page (req, resp)\n\n\nsource\n\n\nRedirect\n\n Redirect (loc)\n\nUse HTMX or Starlette RedirectResponse as required to redirect to loc\nThe FastHTML exts param supports the following:\n\nprint(' '.join(htmx_exts))\n\nmorph head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer\n\n\n\nsource\n\n\nget_key\n\n get_key (key=None, fname='.sesskey')\n\n\nsource\n\n\nqp\n\n qp (p:str, **kw)\n\nAdd parameters kw to path p\nqp adds query parameters to route path strings\n\nvals = {'a':5, 'b':False, 'c':[1,2], 'd':'bar', 'e':None, 'ab':42}\n\n\nres = qp('/foo', **vals)\ntest_eq(res, '/foo?a=5&b=&c=1&c=2&d=bar&e=&ab=42')\n\nqp checks to see if each param should be sent as a query parameter or as part of the route, and encodes that properly.\n\npath = '/foo/{a}/{d}/{ab:int}'\nres = qp(path, **vals)\ntest_eq(res, '/foo/5/bar/42?b=&c=1&c=2&e=')\n\n\nsource\n\n\ndef_hdrs\n\n def_hdrs (htmx=True, surreal=True)\n\nDefault headers for a FastHTML app\n\nsource\n\n\nFastHTML\n\n FastHTML (debug=False, routes=None, middleware=None, title:str='FastHTML\n page', exception_handlers=None, on_startup=None,\n on_shutdown=None, lifespan=None, hdrs=None, ftrs=None,\n exts=None, before=None, after=None, surreal=True, htmx=True,\n default_hdrs=True, sess_cls=<class\n 'starlette.middleware.sessions.SessionMiddleware'>,\n secret_key=None, session_cookie='session_', max_age=31536000,\n sess_path='/', same_site='lax', sess_https_only=False,\n sess_domain=None, key_fname='.sesskey', body_wrap=<function\n noop_body>, htmlkw=None, nb_hdrs=False, canonical=True,\n **bodykw)\n\nCreates an Starlette application.\n\nsource\n\n\nFastHTML.ws\n\n FastHTML.ws (path:str, conn=None, disconn=None, name=None,\n middleware=None)\n\nAdd a websocket route at path\n\nsource\n\n\nnested_name\n\n nested_name (f)\n\n*Get name of function f using ’_’ to join nested function names*\n\ndef f():\n def g(): ...\n return g\n\n\nfunc = f()\nnested_name(func)\n\n'f_g'\n\n\n\nsource\n\n\nFastHTML.route\n\n FastHTML.route (path:str=None, methods=None, name=None,\n include_in_schema=True, body_wrap=None)\n\nAdd a route at path\n\napp = FastHTML()\n@app.get\ndef foo(a:str, b:list[int]): ...\n\nfoo.to(a='bar', b=[1,2])\n\n'/foo?a=bar&b=1&b=2'\n\n\n\n@app.get('/foo/{a}')\ndef foo(a:str, b:list[int]): ...\n\nfoo.to(a='bar', b=[1,2])\n\n'/foo/bar?b=1&b=2'\n\n\n\nsource\n\n\nserve\n\n serve (appname=None, app='app', host='0.0.0.0', port=None, reload=True,\n reload_includes:list[str]|str|None=None,\n reload_excludes:list[str]|str|None=None)\n\nRun the app in an async server, with live reload set as the default.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nappname\nNoneType\nNone\nName of the module\n\n\napp\nstr\napp\nApp instance to be served\n\n\nhost\nstr\n0.0.0.0\nIf host is 0.0.0.0 will convert to localhost\n\n\nport\nNoneType\nNone\nIf port is None it will default to 5001 or the PORT environment variable\n\n\nreload\nbool\nTrue\nDefault is to reload the app upon code changes\n\n\nreload_includes\nlist[str] | str | None\nNone\nAdditional files to watch for changes\n\n\nreload_excludes\nlist[str] | str | None\nNone\nFiles to ignore for changes\n\n\n\n\nsource\n\n\nClient\n\n Client (app, url='http://testserver')\n\nA simple httpx ASGI client that doesn’t require async\n\napp = FastHTML(routes=[Route('/', lambda _: Response('test'))])\ncli = Client(app)\n\ncli.get('/').text\n\n'test'\n\n\nNote that you can also use Starlette’s TestClient instead of FastHTML’s Client. They should be largely interchangable.", + "crumbs": [ + "Home", + "Source", + "Core" + ] + }, + { + "objectID": "api/core.html#fasthtml-tests", + "href": "api/core.html#fasthtml-tests", + "title": "Core", + "section": "FastHTML Tests", + "text": "FastHTML Tests\n\ndef get_cli(app): return app,TestClient(app),app.route\n\n\napp,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))\n\n\napp,cli,rt = get_cli(FastHTML(title=\"My Custom Title\"))\n@app.get\ndef foo(): return Div(\"Hello World\")\n\nprint(app.routes)\n\nresponse = cli.get('/foo')\nassert '<title>My Custom Title</title>' in response.text\n\nfoo.to(param='value')\n\n[Route(path='/foo', name='foo', methods=['GET', 'HEAD'])]\n\n\n'/foo?param=value'\n\n\n\napp,cli,rt = get_cli(FastHTML())\n\n@rt('/xt2')\ndef get(): return H1('bar')\n\ntxt = cli.get('/xt2').text\nassert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\n\n@rt(\"/hi\")\ndef get(): return 'Hi there'\n\nr = cli.get('/hi')\nr.text\n\n'Hi there'\n\n\n\n@rt(\"/hi\")\ndef post(): return 'Postal'\n\ncli.post('/hi').text\n\n'Postal'\n\n\n\n@app.get(\"/hostie\")\ndef show_host(req): return req.headers['host']\n\ncli.get('/hostie').text\n\n'testserver'\n\n\n\n@app.get(\"/setsess\")\ndef set_sess(session):\n session['foo'] = 'bar'\n return 'ok'\n\n@app.ws(\"/ws\")\ndef ws(self, msg:str, ws:WebSocket, session): return f\"Message text was: {msg} with session {session.get('foo')}, from client: {ws.client}\"\n\ncli.get('/setsess')\nwith cli.websocket_connect('/ws') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\nassert 'Message text was: Hi! with session bar' in data\nprint(data)\n\nMessage text was: Hi! with session bar, from client: Address(host='testclient', port=50000)\n\n\n\n@rt\ndef yoyo(): return 'a yoyo'\n\ncli.post('/yoyo').text\n\n'a yoyo'\n\n\n\n@app.get\ndef autopost(): return Html(Div('Text.', hx_post=yoyo()))\nprint(cli.get('/autopost').text)\n\n <!doctype html>\n <html>\n <div hx-post=\"a yoyo\">Text.</div>\n </html>\n\n\n\n\n@app.get\ndef autopost2(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b'))))\nprint(cli.get('/autopost2').text)\n\n <!doctype html>\n <html>\n <body>\n <div class=\"px-2\" hx-post=\"/hostie?a=b\">Text.</div>\n </body>\n </html>\n\n\n\n\n@app.get\ndef autoget2(): return Html(Div('Text.', hx_get=show_host))\nprint(cli.get('/autoget2').text)\n\n <!doctype html>\n <html>\n <div hx-get=\"/hostie\">Text.</div>\n </html>\n\n\n\n\n@rt('/user/{nm}', name='gday')\ndef get(nm:str=''): return f\"Good day to you, {nm}!\"\ncli.get('/user/Alexis').text\n\n'Good day to you, Alexis!'\n\n\n\n@app.get\ndef autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))\nprint(cli.get('/autolink').text)\n\n <!doctype html>\n <html>\n <div href=\"/user/Alexis\">Text.</div>\n </html>\n\n\n\n\n@rt('/link')\ndef get(req): return f\"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}\"\n\ncli.get('/link').text\n\n'http://testserver/user/Alexis; http://testserver/hostie'\n\n\n\n@app.get(\"/background\")\nasync def background_task(request):\n async def long_running_task():\n await asyncio.sleep(0.1)\n print(\"Background task completed!\")\n return P(\"Task started\"), BackgroundTask(long_running_task)\n\nresponse = cli.get(\"/background\")\n\nBackground task completed!\n\n\n\ntest_eq(app.router.url_path_for('gday', nm='Jeremy'), '/user/Jeremy')\n\n\nhxhdr = {'headers':{'hx-request':\"1\"}}\n\n@rt('/ft')\ndef get(): return Title('Foo'),H1('bar')\n\ntxt = cli.get('/ft').text\nassert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\n@rt('/xt2')\ndef get(): return H1('bar')\n\ntxt = cli.get('/xt2').text\nassert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\nassert cli.get('/xt2', **hxhdr).text.strip() == '<h1>bar</h1>'\n\n@rt('/xt3')\ndef get(): return Html(Head(Title('hi')), Body(P('there')))\n\ntxt = cli.get('/xt3').text\nassert '<title>FastHTML page</title>' not in txt and '<title>hi</title>' in txt and '<p>there</p>' in txt\n\n\n@rt('/oops')\ndef get(nope): return nope\ntest_warns(lambda: cli.get('/oops?nope=1'))\n\n\ndef test_r(cli, path, exp, meth='get', hx=False, **kwargs):\n if hx: kwargs['headers'] = {'hx-request':\"1\"}\n test_eq(getattr(cli, meth)(path, **kwargs).text, exp)\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n\n@rt('/html/{idx}')\nasync def get(idx:int): return Body(H4(f'Next is {idx+1}.'))\n\n\n@rt(\"/models/{nm}\")\ndef get(nm:ModelName): return nm\n\n@rt(\"/files/{path}\")\nasync def get(path: Path): return path.with_suffix('.txt')\n\n@rt(\"/items/\")\ndef get(idx:int|None = 0): return fake_db[idx]\n\n@rt(\"/idxl/\")\ndef get(idx:list[int]): return str(idx)\n\n\nr = cli.get('/html/1', headers={'hx-request':\"1\"})\nassert '<h4>Next is 2.</h4>' in r.text\ntest_r(cli, '/models/alexnet', 'alexnet')\ntest_r(cli, '/files/foo', 'foo.txt')\ntest_r(cli, '/items/?idx=1', '{\"name\":\"Bar\"}')\ntest_r(cli, '/items/', '{\"name\":\"Foo\"}')\nassert cli.get('/items/?idx=g').text=='404 Not Found'\nassert cli.get('/items/?idx=g').status_code == 404\ntest_r(cli, '/idxl/?idx=1&idx=2', '[1, 2]')\nassert cli.get('/idxl/?idx=1&idx=g').status_code == 404\n\n\napp = FastHTML()\nrt = app.route\ncli = TestClient(app)\n@app.route(r'/static/{path:path}.jpg')\ndef index(path:str): return f'got {path}'\ncli.get('/static/sub/a.b.jpg').text\n\n'got sub/a.b'\n\n\n\napp.chk = 'foo'\n\n\n@app.get(\"/booly/\")\ndef _(coming:bool=True): return 'Coming' if coming else 'Not coming'\n\n@app.get(\"/datie/\")\ndef _(d:parsed_date): return d\n\n@app.get(\"/ua\")\nasync def _(user_agent:str): return user_agent\n\n@app.get(\"/hxtest\")\ndef _(htmx): return htmx.request\n\n@app.get(\"/hxtest2\")\ndef _(foo:HtmxHeaders, req): return foo.request\n\n@app.get(\"/app\")\ndef _(app): return app.chk\n\n@app.get(\"/app2\")\ndef _(foo:FastHTML): return foo.chk,HttpHeader(\"mykey\", \"myval\")\n\n@app.get(\"/app3\")\ndef _(foo:FastHTML): return HtmxResponseHeaders(location=\"http://example.org\")\n\n@app.get(\"/app4\")\ndef _(foo:FastHTML): return Redirect(\"http://example.org\")\n\n\ntest_r(cli, '/booly/?coming=true', 'Coming')\ntest_r(cli, '/booly/?coming=no', 'Not coming')\ndate_str = \"17th of May, 2024, 2p\"\ntest_r(cli, f'/datie/?d={date_str}', '2024-05-17 14:00:00')\ntest_r(cli, '/ua', 'FastHTML', headers={'User-Agent':'FastHTML'})\ntest_r(cli, '/hxtest' , '1', headers={'HX-Request':'1'})\ntest_r(cli, '/hxtest2', '1', headers={'HX-Request':'1'})\ntest_r(cli, '/app' , 'foo')\n\n\nr = cli.get('/app2', **hxhdr)\ntest_eq(r.text, 'foo')\ntest_eq(r.headers['mykey'], 'myval')\n\n\nr = cli.get('/app3')\ntest_eq(r.headers['HX-Location'], 'http://example.org')\n\n\nr = cli.get('/app4', follow_redirects=False)\ntest_eq(r.status_code, 303)\n\n\nr = cli.get('/app4', headers={'HX-Request':'1'})\ntest_eq(r.headers['HX-Redirect'], 'http://example.org')\n\n\n@rt\ndef meta():\n return ((Title('hi'),H1('hi')),\n (Meta(property='image'), Meta(property='site_name'))\n )\n\nt = cli.post('/meta').text\nassert re.search(r'<body>\\s*<h1>hi</h1>\\s*</body>', t)\nassert '<meta' in t\n\n\n@app.post('/profile/me')\ndef profile_update(username: str): return username\n\ntest_r(cli, '/profile/me', 'Alexis', 'post', data={'username' : 'Alexis'})\ntest_r(cli, '/profile/me', 'Missing required field: username', 'post', data={})\n\n\n# Example post request with parameter that has a default value\n@app.post('/pet/dog')\ndef pet_dog(dogname: str = None): return dogname\n\n# Working post request with optional parameter\ntest_r(cli, '/pet/dog', '', 'post', data={})\n\n\n@dataclass\nclass Bodie: a:int;b:str\n\n@rt(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\n@app.post(\"/bodied/\")\ndef bodied(data:dict): return data\n\nnt = namedtuple('Bodient', ['a','b'])\n\n@app.post(\"/bodient/\")\ndef bodient(data:nt): return asdict(data)\n\nclass BodieTD(TypedDict): a:int;b:str='foo'\n\n@app.post(\"/bodietd/\")\ndef bodient(data:BodieTD): return data\n\nclass Bodie2:\n a:int|None; b:str\n def __init__(self, a, b='foo'): store_attr()\n\n@rt(\"/bodie2/\", methods=['get','post'])\ndef bodie(d:Bodie2): return f\"a: {d.a}; b: {d.b}\"\n\n\nfrom fasthtml.xtend import Titled\n\n\nd = dict(a=1, b='foo')\n\ntest_r(cli, '/bodie/me', '{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}', 'post', data=dict(a=1, b='foo', nm='me'))\ntest_r(cli, '/bodied/', '{\"a\":\"1\",\"b\":\"foo\"}', 'post', data=d)\ntest_r(cli, '/bodie2/', 'a: 1; b: foo', 'post', data={'a':1})\ntest_r(cli, '/bodie2/?a=1&b=foo&nm=me', 'a: 1; b: foo')\ntest_r(cli, '/bodient/', '{\"a\":\"1\",\"b\":\"foo\"}', 'post', data=d)\ntest_r(cli, '/bodietd/', '{\"a\":1,\"b\":\"foo\"}', 'post', data=d)\n\n\n# Testing POST with Content-Type: application/json\n@app.post(\"/\")\ndef index(it: Bodie): return Titled(\"It worked!\", P(f\"{it.a}, {it.b}\"))\n\ns = json.dumps({\"b\": \"Lorem\", \"a\": 15})\nresponse = cli.post('/', headers={\"Content-Type\": \"application/json\"}, data=s).text\nassert \"<title>It worked!</title>\" in response and \"<p>15, Lorem</p>\" in response\n\n\n# Testing POST with Content-Type: application/json\n@app.post(\"/bodytext\")\ndef index(body): return body\n\nresponse = cli.post('/bodytext', headers={\"Content-Type\": \"application/json\"}, data=s).text\ntest_eq(response, '{\"b\": \"Lorem\", \"a\": 15}')\n\n\nfiles = [ ('files', ('file1.txt', b'content1')),\n ('files', ('file2.txt', b'content2')) ]\n\n\n@rt(\"/uploads\")\nasync def post(files:list[UploadFile]):\n return ','.join([(await file.read()).decode() for file in files])\n\nres = cli.post('/uploads', files=files)\nprint(res.status_code)\nprint(res.text)\n\n200\ncontent1,content2\n\n\n\nres = cli.post('/uploads', files=[files[0]])\nprint(res.status_code)\nprint(res.text)\n\n200\ncontent1\n\n\n\n@rt(\"/setsess\")\ndef get(sess, foo:str=''):\n now = datetime.now()\n sess['auth'] = str(now)\n return f'Set to {now}'\n\n@rt(\"/getsess\")\ndef get(sess): return f'Session time: {sess[\"auth\"]}'\n\nprint(cli.get('/setsess').text)\ntime.sleep(0.01)\n\ncli.get('/getsess').text\n\nSet to 2025-05-29 08:31:48.235262\n\n\n'Session time: 2025-05-29 08:31:48.235262'\n\n\n\n@rt(\"/sess-first\")\ndef post(sess, name: str):\n sess[\"name\"] = name\n return str(sess)\n\ncli.post('/sess-first', data={'name': 2})\n\n@rt(\"/getsess-all\")\ndef get(sess): return sess['name']\n\ntest_eq(cli.get('/getsess-all').text, '2')\n\n\n@rt(\"/upload\")\nasync def post(uf:UploadFile): return (await uf.read()).decode()\n\nwith open('../../CHANGELOG.md', 'rb') as f:\n print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])\n\n# Release notes\n\n\n\n@rt(\"/form-submit/{list_id}\")\ndef options(list_id: str):\n headers = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'POST',\n 'Access-Control-Allow-Headers': '*',\n }\n return Response(status_code=200, headers=headers)\n\n\nh = cli.options('/form-submit/2').headers\ntest_eq(h['Access-Control-Allow-Methods'], 'POST')\n\n\nfrom fasthtml.authmw import user_pwd_auth\n\n\ndef _not_found(req, exc): return Div('nope')\n\napp,cli,rt = get_cli(FastHTML(exception_handlers={404:_not_found}))\n\ntxt = cli.get('/').text\nassert '<div>nope</div>' in txt\nassert '<!doctype html>' in txt\n\n\napp,cli,rt = get_cli(FastHTML())\n\n@rt(\"/{name}/{age}\")\ndef get(name: str, age: int):\n return Titled(f\"Hello {name.title()}, age {age}\")\n\nassert '<title>Hello Uma, age 5</title>' in cli.get('/uma/5').text\nassert '404 Not Found' in cli.get('/uma/five').text\n\n\nauth = user_pwd_auth(testuser='spycraft')\napp,cli,rt = get_cli(FastHTML(middleware=[auth]))\n\n@rt(\"/locked\")\ndef get(auth): return 'Hello, ' + auth\n\ntest_eq(cli.get('/locked').text, 'not authenticated')\ntest_eq(cli.get('/locked', auth=(\"testuser\",\"spycraft\")).text, 'Hello, testuser')\n\n\nauth = user_pwd_auth(testuser='spycraft')\napp,cli,rt = get_cli(FastHTML(middleware=[auth]))\n\n@rt(\"/locked\")\ndef get(auth): return 'Hello, ' + auth\n\ntest_eq(cli.get('/locked').text, 'not authenticated')\ntest_eq(cli.get('/locked', auth=(\"testuser\",\"spycraft\")).text, 'Hello, testuser')", + "crumbs": [ + "Home", + "Source", + "Core" + ] + }, + { + "objectID": "api/core.html#apirouter", + "href": "api/core.html#apirouter", + "title": "Core", + "section": "APIRouter", + "text": "APIRouter\n\nsource\n\nRouteFuncs\n\n RouteFuncs ()\n\nInitialize self. See help(type(self)) for accurate signature.\n\nsource\n\n\nAPIRouter\n\n APIRouter (prefix:str|None=None, body_wrap=<function noop_body>)\n\nAdd routes to an app\n\nar = APIRouter()\n\n\n@ar(\"/hi\")\ndef get(): return 'Hi there'\n@ar(\"/hi\")\ndef post(): return 'Postal'\n@ar\ndef ho(): return 'Ho ho'\n@ar(\"/hostie\")\ndef show_host(req): return req.headers['host']\n@ar\ndef yoyo(): return 'a yoyo'\n@ar\ndef index(): return \"home page\"\n\n@ar.ws(\"/ws\")\ndef ws(self, msg:str): return f\"Message text was: {msg}\"\n\n\napp,cli,_ = get_cli(FastHTML())\nar.to_app(app)\n\n\nassert str(yoyo) == '/yoyo'\n# ensure route functions are properly discoverable on `APIRouter` and `APIRouter.rt_funcs`\nassert ar.prefix == ''\nassert str(ar.rt_funcs.index) == '/'\nassert str(ar.index) == '/'\nwith ExceptionExpected(): ar.blah()\nwith ExceptionExpected(): ar.rt_funcs.blah()\n# ensure any route functions named using an HTTPMethod are not discoverable via `rt_funcs`\nassert \"get\" not in ar.rt_funcs._funcs.keys()\n\n\ntest_eq(cli.get('/hi').text, 'Hi there')\ntest_eq(cli.post('/hi').text, 'Postal')\ntest_eq(cli.get('/hostie').text, 'testserver')\ntest_eq(cli.post('/yoyo').text, 'a yoyo')\n\ntest_eq(cli.get('/ho').text, 'Ho ho')\ntest_eq(cli.post('/ho').text, 'Ho ho')\n\n\nwith cli.websocket_connect('/ws') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\n assert data == 'Message text was: Hi!'\n\n\nar2 = APIRouter(\"/products\")\n\n\n@ar2(\"/hi\")\ndef get(): return 'Hi there'\n@ar2(\"/hi\")\ndef post(): return 'Postal'\n@ar2\ndef ho(): return 'Ho ho'\n@ar2(\"/hostie\")\ndef show_host(req): return req.headers['host']\n@ar2\ndef yoyo(): return 'a yoyo'\n@ar2\ndef index(): return \"home page\"\n\n@ar2.ws(\"/ws\")\ndef ws(self, msg:str): return f\"Message text was: {msg}\"\n\n\napp,cli,_ = get_cli(FastHTML())\nar2.to_app(app)\n\n\nassert str(yoyo) == '/products/yoyo'\nassert ar2.prefix == '/products'\nassert str(ar2.rt_funcs.index) == '/products/'\nassert str(ar2.index) == '/products/'\nassert str(ar.index) == '/'\nwith ExceptionExpected(): ar2.blah()\nwith ExceptionExpected(): ar2.rt_funcs.blah()\nassert \"get\" not in ar2.rt_funcs._funcs.keys()\n\n\ntest_eq(cli.get('/products/hi').text, 'Hi there')\ntest_eq(cli.post('/products/hi').text, 'Postal')\ntest_eq(cli.get('/products/hostie').text, 'testserver')\ntest_eq(cli.post('/products/yoyo').text, 'a yoyo')\n\ntest_eq(cli.get('/products/ho').text, 'Ho ho')\ntest_eq(cli.post('/products/ho').text, 'Ho ho')\n\n\nwith cli.websocket_connect('/products/ws') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\n assert data == 'Message text was: Hi!'\n\n\n@ar.get\ndef hi2(): return 'Hi there'\n@ar.get(\"/hi3\")\ndef _(): return 'Hi there'\n@ar.post(\"/post2\")\ndef _(): return 'Postal'\n\n@ar2.get\ndef hi2(): return 'Hi there'\n@ar2.get(\"/hi3\")\ndef _(): return 'Hi there'\n@ar2.post(\"/post2\")\ndef _(): return 'Postal'", + "crumbs": [ + "Home", + "Source", + "Core" + ] + }, + { + "objectID": "api/core.html#extras", + "href": "api/core.html#extras", + "title": "Core", + "section": "Extras", + "text": "Extras\n\napp,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))\n\n\nsource\n\ncookie\n\n cookie (key:str, value='', max_age=None, expires=None, path='/',\n domain=None, secure=False, httponly=False, samesite='lax')\n\nCreate a ‘set-cookie’ HttpHeader\n\n@rt(\"/setcookie\")\ndef get(req): return cookie('now', datetime.now())\n\n@rt(\"/getcookie\")\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nprint(cli.get('/setcookie').text)\ntime.sleep(0.01)\ncli.get('/getcookie').text\n\n <!doctype html>\n <html>\n <head>\n <title>FastHTML page</title>\n <link rel=\"canonical\" href=\"http://testserver/setcookie\">\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<script src=\"https://cdn.jsdelivr.net/npm/htmx.org@2.0.4/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script>\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };</script> </head>\n <body></body>\n </html>\n\n\n\n'Cookie was set at time 08:31:49.013668'\n\n\n\nsource\n\n\nreg_re_param\n\n reg_re_param (m, s)\n\n\nsource\n\n\nFastHTML.static_route_exts\n\n FastHTML.static_route_exts (prefix='/', static_path='.', exts='static')\n\nAdd a static route at URL path prefix with files from static_path and exts defined by reg_re_param()\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm|pdf\")\n\n@rt(r'/static/{path:path}{fn}.{ext:imgext}')\ndef get(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\ntest_r(cli, '/static/foo/jph.me.ico', 'Getting jph.me.ico from /foo/')\n\n\napp.static_route_exts()\nassert 'These are the source notebooks for FastHTML' in cli.get('/README.txt').text\n\n\nsource\n\n\nFastHTML.static_route\n\n FastHTML.static_route (ext='', prefix='/', static_path='.')\n\nAdd a static route at URL path prefix with files from static_path and single ext (including the ‘.’)\n\napp.static_route('.md', static_path='../..')\nassert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text\n\n\nsource\n\n\nMiddlewareBase\n\n MiddlewareBase ()\n\nInitialize self. See help(type(self)) for accurate signature.\n\nsource\n\n\nFtResponse\n\n FtResponse (content, status_code:int=200, headers=None, cls=<class\n 'starlette.responses.HTMLResponse'>,\n media_type:str|None=None,\n background:starlette.background.BackgroundTask|None=None)\n\nWrap an FT response with any Starlette Response\n\n@rt('/ftr')\ndef get():\n cts = Title('Foo'),H1('bar')\n return FtResponse(cts, status_code=201, headers={'Location':'/foo/1'})\n\nr = cli.get('/ftr')\n\ntest_eq(r.status_code, 201)\ntest_eq(r.headers['location'], '/foo/1')\ntxt = r.text\nassert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\nTest on a single background task:\n\ndef my_slow_task():\n print('Starting slow task') \n time.sleep(0.001)\n print('Finished slow task') \n\n@rt('/background')\ndef get():\n return P('BG Task'), BackgroundTask(my_slow_task)\n\nr = cli.get('/background')\n\ntest_eq(r.status_code, 200)\n\nStarting slow task\nFinished slow task\n\n\nTest multiple background tasks:\n\ndef increment(amount):\n amount = amount/1000\n print(f'Sleeping for {amount}s') \n time.sleep(amount)\n print(f'Slept for {amount}s')\n\n\n@rt\ndef backgrounds():\n tasks = BackgroundTasks()\n for i in range(3): tasks.add_task(increment, i)\n return P('BG Tasks'), tasks\n\nr = cli.get('/backgrounds')\ntest_eq(r.status_code, 200)\n\nSleeping for 0.0s\nSlept for 0.0s\nSleeping for 0.001s\nSlept for 0.001s\nSleeping for 0.002s\nSlept for 0.002s\n\n\n\n@rt\ndef backgrounds2():\n tasks = [BackgroundTask(increment,i) for i in range(3)]\n return P('BG Tasks'), *tasks\n\nr = cli.get('/backgrounds2')\ntest_eq(r.status_code, 200)\n\nSleeping for 0.0s\nSlept for 0.0s\nSleeping for 0.001s\nSlept for 0.001s\nSleeping for 0.002s\nSlept for 0.002s\n\n\n\n@rt\ndef backgrounds3():\n tasks = [BackgroundTask(increment,i) for i in range(3)]\n return {'status':'done'}, *tasks\n\nr = cli.get('/backgrounds3')\ntest_eq(r.status_code, 200)\nr.json()\n\nSleeping for 0.0s\nSlept for 0.0s\nSleeping for 0.001s\nSlept for 0.001s\nSleeping for 0.002s\nSlept for 0.002s\n\n\n{'status': 'done'}\n\n\n\nsource\n\n\nunqid\n\n unqid (seeded=False)\n\n\nsource\n\n\nsetup_ws\n\n setup_ws (app, f=<function noop>)\n\n\nsource\n\n\nFastHTML.devtools_json\n\n FastHTML.devtools_json (path=None, uuid=None)", + "crumbs": [ + "Home", + "Source", + "Core" + ] + }, + { + "objectID": "api/pico.html", + "href": "api/pico.html", + "title": "Pico.css components", + "section": "", + "text": "picocondlink is the class-conditional css link tag, and picolink is the regular tag.\n\nshow(picocondlink)\n\n\n\n\n\n\nsource\n\nset_pico_cls\n\n set_pico_cls ()\n\nRun this to make jupyter outputs styled with pico:\n\nset_pico_cls()\n\n\n\n\n\nsource\n\n\nCard\n\n Card (*c, header=None, footer=None, target_id=None, hx_vals=None,\n hx_target=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Card, implemented as an Article with optional Header and Footer\n\nshow(Card('body', header=P('head'), footer=P('foot')))\n\n\n head\n\nbody\n foot\n\n\n\n\n\nsource\n\n\nGroup\n\n Group (*c, target_id=None, hx_vals=None, hx_target=None, id=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None, enterkeyhint=None,\n hidden=None, inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Group, implemented as a Fieldset with role ‘group’\n\nshow(Group(Input(), Button(\"Save\")))\n\n\n \n Save\n\n\n\n\nsource\n\n\nSearch\n\n Search (*c, target_id=None, hx_vals=None, hx_target=None, id=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Search, implemented as a Form with role ‘search’\n\nshow(Search(Input(type=\"search\"), Button(\"Search\")))\n\n\n \n Search\n\n\n\n\nsource\n\n\nGrid\n\n Grid (*c, cls='grid', target_id=None, hx_vals=None, hx_target=None,\n id=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None, enterkeyhint=None,\n hidden=None, inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Grid, implemented as child Divs in a Div with class ‘grid’\n\ncolors = [Input(type=\"color\", value=o) for o in ('#e66465', '#53d2c5', '#f6b73c')]\nshow(Grid(*colors))\n\n\n \n\n \n\n \n\n\n\n\n\nsource\n\n\nDialogX\n\n DialogX (*c, open=None, header=None, footer=None, id=None,\n target_id=None, hx_vals=None, hx_target=None, cls=None,\n title=None, style=None, accesskey=None, contenteditable=None,\n dir=None, draggable=None, enterkeyhint=None, hidden=None,\n inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None,\n hx_history_elt=None, hx_inherit=None, hx_params=None,\n hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None,\n hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None,\n hx_on_mouseout=None, hx_on_mouseover=None, hx_on_mouseup=None,\n hx_on_wheel=None, hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Dialog, with children inside a Card\n\nhdr = Div(Button(aria_label=\"Close\", rel=\"prev\"), P('confirm'))\nftr = Div(Button('Cancel', cls=\"secondary\"), Button('Confirm'))\nd = DialogX('thank you!', header=hdr, footer=ftr, open=None, id='dlgtest')\n# use js or htmx to display modal\n\n\nsource\n\n\nContainer\n\n Container (*args, target_id=None, hx_vals=None, hx_target=None, id=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None,\n hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,\n hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,\n hx_on_reset=None, hx_on_select=None, hx_on_submit=None,\n hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,\n hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Container, implemented as a Main with class ‘container’\n\nsource\n\n\nPicoBusy\n\n PicoBusy ()", + "crumbs": [ + "Home", + "Source", + "Pico.css components" + ] + }, + { + "objectID": "api/oauth.html", + "href": "api/oauth.html", + "title": "OAuth", + "section": "", + "text": "See the docs page for an explanation of how to use this.\n\nfrom IPython.display import Markdown\n\n\nsource\n\nGoogleAppClient\n\n GoogleAppClient (client_id, client_secret, code=None, scope=None,\n project_id=None, **kwargs)\n\nA WebApplicationClient for Google oauth2\n\nsource\n\n\nGitHubAppClient\n\n GitHubAppClient (client_id, client_secret, code=None, scope=None,\n **kwargs)\n\nA WebApplicationClient for GitHub oauth2\n\nsource\n\n\nHuggingFaceClient\n\n HuggingFaceClient (client_id, client_secret, code=None, scope=None,\n state=None, **kwargs)\n\nA WebApplicationClient for HuggingFace oauth2\n\nsource\n\n\nDiscordAppClient\n\n DiscordAppClient (client_id, client_secret, is_user=False, perms=0,\n scope=None, **kwargs)\n\nA WebApplicationClient for Discord oauth2\n\nsource\n\n\nAuth0AppClient\n\n Auth0AppClient (domain, client_id, client_secret, code=None, scope=None,\n redirect_uri='', **kwargs)\n\nA WebApplicationClient for Auth0 OAuth2\n\n# cli = GoogleAppClient.from_file('client_secret.json')\n\n\nsource\n\n\nWebApplicationClient.login_link\n\n WebApplicationClient.login_link (redirect_uri, scope=None, state=None,\n **kwargs)\n\nGet a login link for this client\nGenerating a login link that sends the user to the OAuth provider is done with client.login_link().\nIt can sometimes be useful to pass state to the OAuth provider, so that when the user returns you can pick up where they left off. This can be done by passing the state parameter.\n\nfrom fasthtml.jupyter import *\n\n\nredir_path = '/redirect'\nport = 8000\n\n\napp,rt = fast_app()\nserver = JupyUvi(app, port=port)\n\n\n\n\n\n\nsource\n\n\nredir_url\n\n redir_url (request, redir_path, scheme=None)\n\nGet the redir url for the host in request\n\n@rt\ndef index(request):\n redir = redir_url(request, redir_path)\n return A('login', href=cli.login_link(redir), target='_blank')\n\n\nsource\n\n\n_AppClient.parse_response\n\n _AppClient.parse_response (code, redirect_uri)\n\nGet the token from the oauth2 server response\n\nsource\n\n\n_AppClient.get_info\n\n _AppClient.get_info (token=None)\n\nGet the info for authenticated user\n\nsource\n\n\n_AppClient.retr_info\n\n _AppClient.retr_info (code, redirect_uri)\n\nCombines parse_response and get_info\n\n@rt(redir_path)\ndef get(request, code:str):\n redir = redir_url(request, redir_path)\n info = cli.retr_info(code, redir)\n return P(f'Login successful for {info[\"name\"]}!')\n\n\n# HTMX()\n\n\nserver.stop()\n\n\nsource\n\n\n_AppClient.retr_id\n\n _AppClient.retr_id (code, redirect_uri)\n\nCall retr_info and then return id/subscriber value\nAfter logging in via the provider, the user will be redirected back to the supplied redirect URL. The request to this URL will contain a code parameter, which is used to get an access token and fetch the user’s profile information. See the explanation here for a worked example. You can either:\n\nUse client.retr_info(code) to get all the profile information, or\nUse client.retr_id(code) to get just the user’s ID.\n\nAfter either of these calls, you can also access the access token (used to revoke access, for example) with client.token[\"access_token\"].\n\nsource\n\n\nurl_match\n\n url_match (url, patterns=('^(localhost|127\\\\.0\\\\.0\\\\.1)(:\\\\d+)?$',))\n\n\nsource\n\n\nOAuth\n\n OAuth (app, cli, skip=None, redir_path='/redirect', error_path='/error',\n logout_path='/logout', login_path='/login', https=True,\n http_patterns=('^(localhost|127\\\\.0\\\\.0\\\\.1)(:\\\\d+)?$',))\n\nInitialize self. See help(type(self)) for accurate signature.\n\n\nGoogle helpers\n\nsource\n\n\nGoogleAppClient.consent_url\n\n GoogleAppClient.consent_url (proj=None)\n\nGet Google OAuth consent screen URL\n\nsource\n\n\nGoogleAppClient.consent_url\n\n GoogleAppClient.consent_url (proj=None)\n\nGet Google OAuth consent screen URL\n\nsource\n\n\nCredentials.update\n\n Credentials.update ()\n\nRefresh the credentials if they are expired, and return them\n\nsource\n\n\nCredentials.update\n\n Credentials.update ()\n\nRefresh the credentials if they are expired, and return them\n\nsource\n\n\nCredentials.save\n\n Credentials.save (fname)\n\nSave credentials to fname\n\nsource\n\n\nCredentials.save\n\n Credentials.save (fname)\n\nSave credentials to fname\n\nsource\n\n\nload_creds\n\n load_creds (fname)\n\nLoad credentials from fname\n\nsource\n\n\nGoogleAppClient.creds\n\n GoogleAppClient.creds ()\n\nCreate Credentials from the client, refreshing if needed\n\nsource\n\n\nGoogleAppClient.creds\n\n GoogleAppClient.creds ()\n\nCreate Credentials from the client, refreshing if needed", + "crumbs": [ + "Home", + "Source", + "OAuth" + ] + }, + { + "objectID": "api/svg.html", + "href": "api/svg.html", + "title": "SVG", + "section": "", + "text": "from nbdev.showdoc import show_doc\nYou can create SVGs directly from strings, for instance (as always, use NotStr or Safe to tell FastHTML to not escape the text):\nsvg = '<svg width=\"50\" height=\"50\"><circle cx=\"20\" cy=\"20\" r=\"15\" fill=\"red\"></circle></svg>'\nshow(NotStr(svg))\nYou can also use libraries such as fa6-icons.\nTo create and modify SVGs using a Python API, use the FT elements in fasthtml.svg, discussed below.\nNote: fasthtml.common does NOT automatically export SVG elements. To get access to them, you need to import fasthtml.svg like so\nsource", + "crumbs": [ + "Home", + "Source", + "SVG" + ] + }, + { + "objectID": "api/svg.html#basic-shapes", + "href": "api/svg.html#basic-shapes", + "title": "SVG", + "section": "Basic shapes", + "text": "Basic shapes\nWe’ll define a simple function to display SVG shapes in this notebook:\n\ndef demo(el, h=50, w=50): return show(Svg(h=h,w=w)(el))\n\n\nsource\n\nRect\n\n Rect (width, height, x=0, y=0, fill=None, stroke=None, stroke_width=None,\n rx=None, ry=None, transform=None, opacity=None, clip=None,\n mask=None, filter=None, vector_effect=None, pointer_events=None,\n target_id=None, hx_vals=None, hx_target=None, id=None, cls=None,\n title=None, style=None, accesskey=None, contenteditable=None,\n dir=None, draggable=None, enterkeyhint=None, hidden=None,\n inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nA standard SVG rect element\nAll our shapes just create regular FT elements. The only extra functionality provided by most of them is to add additional defined kwargs to improve auto-complete in IDEs and notebooks, and re-order parameters so that positional args can also be used to save a bit of typing, e.g:\n\ndemo(Rect(30, 30, fill='blue', rx=8, ry=8))\n\n\n\n\n\nsource\n\n\nCircle\n\n Circle (r, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nA standard SVG circle element\n\ndemo(Circle(20, 25, 25, stroke='red', stroke_width=3))\n\n\n\n\n\nsource\n\n\nEllipse\n\n Ellipse (rx, ry, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None,\n hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,\n hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,\n hx_on_reset=None, hx_on_select=None, hx_on_submit=None,\n hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,\n hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)\n\nA standard SVG ellipse element\n\ndemo(Ellipse(20, 10, 25, 25))\n\n\n\n\n\nsource\n\n\ntransformd\n\n transformd (translate=None, scale=None, rotate=None, skewX=None,\n skewY=None, matrix=None)\n\nCreate an SVG transform kwarg dict\n\nrot = transformd(rotate=(45, 25, 25))\nrot\n\n{'transform': 'rotate(45,25,25)'}\n\n\n\ndemo(Ellipse(20, 10, 25, 25, **rot))\n\n\n\n\n\nsource\n\n\nLine\n\n Line (x1, y1, x2=0, y2=0, stroke='black', w=None, stroke_width=1,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nA standard SVG line element\n\ndemo(Line(20, 30, w=3))\n\n\n\n\n\nsource\n\n\nPolyline\n\n Polyline (*args, points=None, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None,\n filter=None, vector_effect=None, pointer_events=None,\n target_id=None, hx_vals=None, hx_target=None, id=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None,\n hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,\n hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,\n hx_on_reset=None, hx_on_select=None, hx_on_submit=None,\n hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,\n hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)\n\nA standard SVG polyline element\n\ndemo(Polyline((0,0), (10,10), (20,0), (30,10), (40,0),\n fill='yellow', stroke='blue', stroke_width=2))\n\n\n\n\n\ndemo(Polyline(points='0,0 10,10 20,0 30,10 40,0', fill='purple', stroke_width=2))\n\n\n\n\n\nsource\n\n\nPolygon\n\n Polygon (*args, points=None, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None,\n hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,\n hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,\n hx_on_reset=None, hx_on_select=None, hx_on_submit=None,\n hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,\n hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)\n\nA standard SVG polygon element\n\ndemo(Polygon((25,5), (43.3,15), (43.3,35), (25,45), (6.7,35), (6.7,15), \n fill='lightblue', stroke='navy', stroke_width=2))\n\n\n\n\n\ndemo(Polygon(points='25,5 43.3,15 43.3,35 25,45 6.7,35 6.7,15',\n fill='lightgreen', stroke='darkgreen', stroke_width=2))\n\n\n\n\n\nsource\n\n\nText\n\n Text (*args, x=0, y=0, font_family=None, font_size=None, fill=None,\n text_anchor=None, dominant_baseline=None, font_weight=None,\n font_style=None, text_decoration=None, transform=None,\n opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nA standard SVG text element\n\ndemo(Text(\"Hello!\", x=10, y=30))\n\nHello!", + "crumbs": [ + "Home", + "Source", + "SVG" + ] + }, + { + "objectID": "api/svg.html#paths", + "href": "api/svg.html#paths", + "title": "SVG", + "section": "Paths", + "text": "Paths\nPaths in SVGs are more complex, so we add a small (optional) fluent interface for constructing them:\n\nsource\n\nPathFT\n\n PathFT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs)\n\nA ‘Fast Tag’ structure, containing tag,children,and attrs\n\nsource\n\n\nPath\n\n Path (d='', fill=None, stroke=None, stroke_width=None, transform=None,\n opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nCreate a standard path SVG element. This is a special object\nLet’s create a square shape, but using Path instead of Rect:\n\nM(10, 10): Move to starting point (10, 10)\nL(40, 10): Line to (40, 10) - top edge\nL(40, 40): Line to (40, 40) - right edge\nL(10, 40): Line to (10, 40) - bottom edge\nZ(): Close path - connects back to start\n\nM = Move to, L = Line to, Z = Close path\n\ndemo(Path(fill='none', stroke='purple', stroke_width=2\n ).M(10, 10).L(40, 10).L(40, 40).L(10, 40).Z())\n\n\n\n\nUsing curves we can create a spiral:\n\np = (Path(fill='none', stroke='purple', stroke_width=2)\n .M(25, 25)\n .C(25, 25, 20, 20, 30, 20)\n .C(40, 20, 40, 30, 30, 30)\n .C(20, 30, 20, 15, 35, 15)\n .C(50, 15, 50, 35, 25, 35)\n .C(0, 35, 0, 10, 40, 10)\n .C(80, 10, 80, 40, 25, 40))\ndemo(p, 50, 100)\n\n\n\n\nUsing arcs and curves we can create a map marker icon:\n\np = (Path(fill='red')\n .M(25,45)\n .C(25,45,10,35,10,25)\n .A(15,15,0,1,1,40,25)\n .C(40,35,25,45,25,45)\n .Z())\ndemo(p)\n\n\n\n\nBehind the scenes it’s just creating regular SVG path d attr – you can pass d in directly if you prefer.\n\nprint(p.d)\n\n M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z\n\n\n\ndemo(Path(d='M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z'))\n\n\n\n\n\nsource\n\n\nPathFT.M\n\n PathFT.M (x, y)\n\nMove to.\n\nsource\n\n\nPathFT.L\n\n PathFT.L (x, y)\n\nLine to.\n\nsource\n\n\nPathFT.H\n\n PathFT.H (x)\n\nHorizontal line to.\n\nsource\n\n\nPathFT.V\n\n PathFT.V (y)\n\nVertical line to.\n\nsource\n\n\nPathFT.Z\n\n PathFT.Z ()\n\nClose path.\n\nsource\n\n\nPathFT.C\n\n PathFT.C (x1, y1, x2, y2, x, y)\n\nCubic Bézier curve.\n\nsource\n\n\nPathFT.S\n\n PathFT.S (x2, y2, x, y)\n\nSmooth cubic Bézier curve.\n\nsource\n\n\nPathFT.Q\n\n PathFT.Q (x1, y1, x, y)\n\nQuadratic Bézier curve.\n\nsource\n\n\nPathFT.T\n\n PathFT.T (x, y)\n\nSmooth quadratic Bézier curve.\n\nsource\n\n\nPathFT.A\n\n PathFT.A (rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y)\n\nElliptical Arc.", + "crumbs": [ + "Home", + "Source", + "SVG" + ] + }, + { + "objectID": "api/svg.html#htmx-helpers", + "href": "api/svg.html#htmx-helpers", + "title": "SVG", + "section": "HTMX helpers", + "text": "HTMX helpers\n\nsource\n\nSvgOob\n\n SvgOob (*args, **kwargs)\n\nWraps an SVG shape as required for an HTMX OOB swap\nWhen returning an SVG shape out-of-band (OOB) in HTMX, you need to wrap it with SvgOob to have it appear correctly. (SvgOob is just a shortcut for Template(Svg(...)), which is the trick that makes SVG OOB swaps work.)\n\nsource\n\n\nSvgInb\n\n SvgInb (*args, **kwargs)\n\nWraps an SVG shape as required for an HTMX inband swap\nWhen returning an SVG shape in-band in HTMX, either have the calling element include hx_select='svg>*', or **svg_inb (which are two ways of saying the same thing), or wrap the response with SvgInb to have it appear correctly. (SvgInb is just a shortcut for the tuple (Svg(...), HtmxResponseHeaders(hx_reselect='svg>*')), which is the trick that makes SVG in-band swaps work.)", + "crumbs": [ + "Home", + "Source", + "SVG" + ] + }, + { + "objectID": "api/jupyter.html", + "href": "api/jupyter.html", + "title": "Jupyter compatibility", + "section": "", + "text": "from httpx import get, AsyncClient", + "crumbs": [ + "Home", + "Source", + "Jupyter compatibility" + ] + }, + { + "objectID": "api/jupyter.html#helper-functions", + "href": "api/jupyter.html#helper-functions", + "title": "Jupyter compatibility", + "section": "Helper functions", + "text": "Helper functions\n\nsource\n\nnb_serve\n\n nb_serve (app, log_level='error', port=8000, host='0.0.0.0', **kwargs)\n\nStart a Jupyter compatible uvicorn server with ASGI app on port with log_level\n\nsource\n\n\nnb_serve_async\n\n nb_serve_async (app, log_level='error', port=8000, host='0.0.0.0',\n **kwargs)\n\nAsync version of nb_serve\n\nsource\n\n\nis_port_free\n\n is_port_free (port, host='localhost')\n\nCheck if port is free on host\n\nsource\n\n\nwait_port_free\n\n wait_port_free (port, host='localhost', max_wait=3)\n\nWait for port to be free on host", + "crumbs": [ + "Home", + "Source", + "Jupyter compatibility" + ] + }, + { + "objectID": "api/jupyter.html#using-fasthtml-in-jupyter", + "href": "api/jupyter.html#using-fasthtml-in-jupyter", + "title": "Jupyter compatibility", + "section": "Using FastHTML in Jupyter", + "text": "Using FastHTML in Jupyter\n\nsource\n\nshow\n\n show (*s, iframe=False, height='auto', style=None)\n\nSame as fasthtml.components.show, but also adds htmx.process()\n\nsource\n\n\nrender_ft\n\n render_ft ()\n\n\nsource\n\n\nhtmx_config_port\n\n htmx_config_port (port=8000)\n\n\nsource\n\n\nJupyUvi\n\n JupyUvi (app, log_level='error', host='0.0.0.0', port=8000, start=True,\n **kwargs)\n\nStart and stop a Jupyter compatible uvicorn server with ASGI app on port with log_level\nCreating an object of this class also starts the Uvicorn server. It runs in a separate thread, so you can use normal HTTP client functions in a notebook.\n\napp = FastHTML()\nrt = app.route\n\n@app.route\ndef index(): return 'hi'\n\nport = 8000\nserver = JupyUvi(app, port=port)\n\n\n\n\n\n\nget(f'http://localhost:{port}').text\n\n'hi'\n\n\nYou can stop the server, modify routes, and start the server again without restarting the notebook or recreating the server or application.\n\nserver.stop()\n\n\napp = FastHTML()\nrt = app.route\n\n@app.route\nasync def index(): return 'hi'\n\nserver = JupyUvi(app, port=port, start=False)\nawait server.start_async()\n\n\n\n\n\n\nprint((await AsyncClient().get(f'http://localhost:{port}')).text)\n\nhi\n\n\n\nsource\n\n\nJupyUviAsync\n\n JupyUviAsync (app, log_level='error', host='0.0.0.0', port=8000,\n **kwargs)\n\nStart and stop an async Jupyter compatible uvicorn server with ASGI app on port with log_level\n\nserver = JupyUviAsync(app, port=port)\nawait server.start()\n\n\n\n\n\n\nasync with AsyncClient() as client:\n r = await client.get(f'http://localhost:{port}')\nprint(r.text)\n\nhi\n\n\n\nserver.stop()\n\n\n\nUsing a notebook as a web app\nYou can also run an HTMX web app directly in a notebook. To make this work, you have to add the default FastHTML headers to the DOM of the notebook with show(*def_hdrs()). Additionally, you might find it convenient to use auto_id mode, in which the ID of an FT object is automatically generated if not provided.\n\nfh_cfg['auto_id' ]=True\n\nAfter importing fasthtml.jupyter and calling render_ft(), FT components render directly in the notebook.\n\nshow(*def_hdrs())\nrender_ft()\n\n\n\n\n\n\n\n(c := Div('Cogito ergo sum'))\n\n\n\nCogito ergo sum\n\n\n\n\n\nHandlers are written just like a regular web app:\n\nserver = JupyUvi(app, port=port)\n\n\n\n\n\n\n@rt\ndef hoho(): return P('loaded!'), Div('hee hee', id=c, hx_swap_oob='true')\n\nAll the usual hx_* attributes can be used:\n\nP('not loaded', hx_get=hoho, hx_trigger='load')\n\n\n\nnot loaded\n\n\n\n\n\nFT components can be used directly both as id values and as hx_target values.\n\n(c := Div(''))\n\n\n\n\n\n\n\n\n\n\n@rt\ndef foo(): return Div('foo bar')\nP('hi', hx_get=foo, hx_trigger='load', hx_target=c)\n\n\n\nhi\n\n\n\n\n\n\nserver.stop()\n\n\n\nRunning apps in an IFrame\nUsing an IFrame can be a good idea to get complete isolation of the styles and scripts in an app. The HTMX function creates an auto-sizing IFrame for a web app.\n\nsource\n\n\nHTMX\n\n HTMX (path='', app=None, host='localhost', port=8000, height='auto',\n link=False, iframe=True)\n\nAn iframe which displays the HTMX application in a notebook.\n\n@rt\ndef index():\n return Div(\n P(A('Click me', hx_get=update, hx_target='#result')),\n P(A('No me!', hx_get=update, hx_target='#result')),\n Div(id='result'))\n\n@rt\ndef update(): return Div(P('Hi!'),P('There!'))\n\n\nserver.start()\n\n\n# Run the notebook locally to see the HTMX iframe in action\nHTMX()\n\n \n\n\n\nserver.stop()\n\n\nsource\n\n\nws_client\n\n ws_client (app, nm='', host='localhost', port=8000, ws_connect='/ws',\n frame=True, link=True, **kwargs)", + "crumbs": [ + "Home", + "Source", + "Jupyter compatibility" + ] + }, + { + "objectID": "tutorials/e2e.html", + "href": "tutorials/e2e.html", + "title": "JS App Walkthrough", + "section": "", + "text": "You’ll need the following software to complete the tutorial, read on for specific installation instructions:\n\nPython\nA Python package manager such as pip (which normally comes with Python) or uv\nFastHTML\nWeb browser\nRailway.app account\n\nIf you haven’t worked with Python before, we recommend getting started with Miniconda.\nNote that you will only need to follow the steps in the installation section once per environment. If you create a new repo, you won’t need to redo these.\n\n\nFor Mac, Windows and Linux, enter:\npip install python-fasthtml", + "crumbs": [ + "Home", + "Tutorials", + "JS App Walkthrough" + ] + }, + { + "objectID": "tutorials/e2e.html#installation", + "href": "tutorials/e2e.html#installation", + "title": "JS App Walkthrough", + "section": "", + "text": "You’ll need the following software to complete the tutorial, read on for specific installation instructions:\n\nPython\nA Python package manager such as pip (which normally comes with Python) or uv\nFastHTML\nWeb browser\nRailway.app account\n\nIf you haven’t worked with Python before, we recommend getting started with Miniconda.\nNote that you will only need to follow the steps in the installation section once per environment. If you create a new repo, you won’t need to redo these.\n\n\nFor Mac, Windows and Linux, enter:\npip install python-fasthtml", + "crumbs": [ + "Home", + "Tutorials", + "JS App Walkthrough" + ] + }, + { + "objectID": "tutorials/e2e.html#first-steps", + "href": "tutorials/e2e.html#first-steps", + "title": "JS App Walkthrough", + "section": "First steps", + "text": "First steps\nBy the end of this section you’ll have your own FastHTML website with tests deployed to railway.app.\n\nCreate a hello world\nCreate a new folder to organize all the files for your project. Inside this folder, create a file called main.py and add the following code to it:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\napp = FastHTML()\nrt = app.route\n\n@rt('/')\ndef get():\n return 'Hello, world!'\n\nserve()\n\nFinally, run python main.py in your terminal and open your browser to the ‘Link’ that appears.\n\n\nQuickDraw: A FastHTML Adventure 🎨✨\nThe end result of this tutorial will be QuickDraw, a real-time collaborative drawing app using FastHTML. Here is what the final site will look like:\n\n\n\nQuickDraw\n\n\n\nDrawing Rooms\nDrawing rooms are the core concept of our application. Each room represents a separate drawing space where a user can let their inner Picasso shine. Here’s a detailed breakdown:\n\nRoom Creation and Storage\n\n\n\nmain.py\n\ndb = database('data/drawapp.db')\nrooms = db.t.rooms\nif rooms not in db.t:\n rooms.create(id=int, name=str, created_at=str, pk='id')\nRoom = rooms.dataclass()\n\n@patch\ndef __ft__(self:Room):\n return Li(A(self.name, href=f\"/rooms/{self.id}\"))\n\nOr you can use our fast_app function to create a FastHTML app with a SQLite database and dataclass in one line:\n\n\nmain.py\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')\n\nWe are specifying a render function to convert our dataclass into HTML, which is the same as extending the __ft__ method from the patch decorator we used before. We will use this method for the rest of the tutorial since it is a lot cleaner and easier to read.\n\nWe’re using a SQLite database (via FastLite) to store our rooms.\nEach room has an id (integer), a name (string), and a created_at timestamp (string).\nThe Room dataclass is automatically generated based on this structure.\n\n\nCreating a room\n\n\n\nmain.py\n\n@rt(\"/\")\ndef get():\n # The 'Input' id defaults to the same as the name, so you can omit it if you wish\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"DrawCollab\", \n H1(\"DrawCollab\"),\n create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n\nWhen a user submits the “Create Room” form, this route is called.\nIt creates a new Room object, sets the creation time, and inserts it into the database.\nIt returns an HTML list item with a link to the new room, which is dynamically added to the room list on the homepage thanks to HTMX.\n\n\nLet’s give our rooms shape\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n return Titled(f\"Room: {room.name}\", H1(f\"Welcome to {room.name}\"), A(Button(\"Leave Room\"), href=\"/\"))\n\n\nThis route renders the interface for a specific room.\nIt fetches the room from the database and renders a title, heading, and paragraph.\n\nHere is the full code so far:\n\n\nmain.py\n\nfrom fasthtml.common import *\nfrom datetime import datetime\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')\n\n@rt(\"/\")\ndef get():\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"DrawCollab\", create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n return Titled(f\"Room: {room.name}\", H1(f\"Welcome to {room.name}\"), A(Button(\"Leave Room\"), href=\"/\"))\n\nserve()\n\nNow run python main.py in your terminal and open your browser to the ‘Link’ that appears. You should see a page with a form to create a new room and a list of existing rooms.\n\n\nThe Canvas - Let’s Get Drawing! 🖌️\nTime to add the actual drawing functionality. We’ll use Fabric.js for this:\n\n\nmain.py\n\n# ... (keep the previous imports and database setup)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#3CDD8C\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n \n js = \"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#3CDD8C';\n canvas.freeDrawingBrush.width = 10;\n \n document.getElementById('color-picker').onchange = function() {\n canvas.freeDrawingBrush.color = this.value;\n };\n \n document.getElementById('brush-size').oninput = function() {\n canvas.freeDrawingBrush.width = parseInt(this.value, 10);\n };\n \"\"\"\n \n return Titled(f\"Room: {room.name}\",\n A(Button(\"Leave Room\"), href=\"/\"),\n canvas,\n Div(color_picker, brush_size),\n Script(src=\"https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js\"),\n Script(js))\n\n# ... (keep the serve() part)\n\nNow we’ve got a drawing canvas! FastHTML makes it easy to include external libraries and add custom JavaScript.\n\n\nSaving and Loading Canvases 💾\nNow that we have a working drawing canvas, let’s add the ability to save and load drawings. We’ll modify our database schema to include a canvas_data field, and add new routes for saving and loading canvas data. Here’s how we’ll update our code:\n\nModify the database schema:\n\n\n\nmain.py\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')\n\n\nAdd a save button that grabs the canvas’ state and sends it to the server:\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#3CDD8C\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n save_button = Button(\"Save Canvas\", id=\"save-canvas\", hx_post=f\"/rooms/{id}/save\", hx_vals=\"js:{canvas_data: JSON.stringify(canvas.toJSON())}\")\n # ... (rest of the function remains the same)\n\n\nAdd routes for saving and loading canvas data:\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}/save\")\nasync def post(id:int, canvas_data:str):\n rooms.update({'canvas_data': canvas_data}, id)\n return \"Canvas saved successfully\"\n\n@rt(\"/rooms/{id}/load\")\nasync def get(id:int):\n room = rooms[id]\n return room.canvas_data if room.canvas_data else \"{}\"\n\n\nUpdate the JavaScript to load existing canvas data:\n\n\n\nmain.py\n\njs = f\"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#3CDD8C';\n canvas.freeDrawingBrush.width = 10;\n // Load existing canvas data\n fetch(`/rooms/{id}/load`)\n .then(response => response.json())\n .then(data => {{\n if (data && Object.keys(data).length > 0) {{\n canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));\n }}\n }});\n \n // ... (rest of the JavaScript remains the same)\n\"\"\"\n\nWith these changes, users can now save their drawings and load them when they return to the room. The canvas data is stored as a JSON string in the database, allowing for easy serialization and deserialization. Try it out! Create a new room, make a drawing, save it, and then reload the page. You should see your drawing reappear, ready for further editing.\nHere is the completed code:\n\n\nmain.py\n\nfrom fasthtml.common import *\nfrom datetime import datetime\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')\n\n@rt(\"/\")\ndef get():\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"QuickDraw\", \n create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#000000\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n save_button = Button(\"Save Canvas\", id=\"save-canvas\", hx_post=f\"/rooms/{id}/save\", hx_vals=\"js:{canvas_data: JSON.stringify(canvas.toJSON())}\")\n\n js = f\"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#000000';\n canvas.freeDrawingBrush.width = 10;\n\n // Load existing canvas data\n fetch(`/rooms/{id}/load`)\n .then(response => response.json())\n .then(data => {{\n if (data && Object.keys(data).length > 0) {{\n canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));\n }}\n }});\n \n document.getElementById('color-picker').onchange = function() {{\n canvas.freeDrawingBrush.color = this.value;\n }};\n \n document.getElementById('brush-size').oninput = function() {{\n canvas.freeDrawingBrush.width = parseInt(this.value, 10);\n }};\n \"\"\"\n \n return Titled(f\"Room: {room.name}\",\n A(Button(\"Leave Room\"), href=\"/\"),\n canvas,\n Div(color_picker, brush_size, save_button),\n Script(src=\"https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js\"),\n Script(js))\n\n@rt(\"/rooms/{id}/save\")\nasync def post(id:int, canvas_data:str):\n rooms.update({'canvas_data': canvas_data}, id)\n return \"Canvas saved successfully\"\n\n@rt(\"/rooms/{id}/load\")\nasync def get(id:int):\n room = rooms[id]\n return room.canvas_data if room.canvas_data else \"{}\"\n\nserve()\n\n\n\n\nDeploying to Railway\nYou can deploy your website to a number of hosting providers, for this tutorial we’ll be using Railway. To get started, make sure you create an account and install the Railway CLI. Once installed, make sure to run railway login to log in to your account.\nTo make deploying your website as easy as possible, FastHTMl comes with a built in CLI tool that will handle most of the deployment process for you. To deploy your website, run the following command in your terminal in the root directory of your project:\nfh_railway_deploy quickdraw\n\n\n\n\n\n\nNote\n\n\n\nYour app must be located in a main.py file for this to work.\n\n\n\n\nConclusion: You’re a FastHTML Artist Now! 🎨🚀\nCongratulations! You’ve just built a sleek, interactive web application using FastHTML. Let’s recap what we’ve learned:\n\nFastHTML allows you to create dynamic web apps with minimal code.\nWe used FastHTML’s routing system to handle different pages and actions.\nWe integrated with a SQLite database to store room information and canvas data.\nWe utilized Fabric.js to create an interactive drawing canvas.\nWe implemented features like color picking, brush size adjustment, and canvas saving.\nWe used HTMX for seamless, partial page updates without full reloads.\nWe learned how to deploy our FastHTML application to Railway for easy hosting.\n\nYou’ve taken your first steps into the world of FastHTML development. From here, the possibilities are endless! You could enhance the drawing app further by adding features like:\n\nImplementing different drawing tools (e.g., shapes, text)\nAdding user authentication\nCreating a gallery of saved drawings\nImplementing real-time collaborative drawing using WebSockets\n\nWhatever you choose to build next, FastHTML has got your back. Now go forth and create something awesome! Happy coding! 🖼️🚀", + "crumbs": [ + "Home", + "Tutorials", + "JS App Walkthrough" + ] + }, + { + "objectID": "tutorials/best_practice.html", + "href": "tutorials/best_practice.html", + "title": "FastHTML Best Practices", + "section": "", + "text": "FastHTML applications are different to applications using FastAPI/react, Django, etc. Don’t assume that FastHTML best practices are the same as those for other frameworks. Best practices embody the fast.ai philosophy: remove ceremony, leverage smart defaults, and write code that’s both concise and clear. The following are some particular opportunities that both humans and language models sometimes miss:", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#database-table-creation---use-dataclasses-and-idempotent-patterns", + "href": "tutorials/best_practice.html#database-table-creation---use-dataclasses-and-idempotent-patterns", + "title": "FastHTML Best Practices", + "section": "Database Table Creation - Use dataclasses and idempotent patterns", + "text": "Database Table Creation - Use dataclasses and idempotent patterns\nBefore:\ntodos = db.t.todos\nif not todos.exists():\ntodos.create(id=int, task=str, completed=bool, created=str, pk='id')\nAfter:\nclass Todo: id:int; task:str; completed:bool; created:str\ntodos = db.create(Todo)\nFastLite’s create() is idempotent - it creates the table if needed and returns the table object either way. Using a dataclass-style definition is cleaner and more Pythonic. The id field is automatically the primary key.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#route-naming-conventions---let-function-names-define-routes", + "href": "tutorials/best_practice.html#route-naming-conventions---let-function-names-define-routes", + "title": "FastHTML Best Practices", + "section": "Route Naming Conventions - Let function names define routes", + "text": "Route Naming Conventions - Let function names define routes\nBefore:\n@rt(\"/\")\ndef get(): return Titled(\"Todo List\", ...)\n\n@rt(\"/add\")\ndef post(task: str): ...\nAfter:\n@rt\ndef index(): return Titled(\"Todo List\", ...) # Special name for \"/\"\n@rt\ndef add(task: str): ... # Function name becomes route\nUse @rt without arguments and let the function name define the route. The special name index maps to /.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#query-parameters-over-path-parameters---cleaner-url-patterns", + "href": "tutorials/best_practice.html#query-parameters-over-path-parameters---cleaner-url-patterns", + "title": "FastHTML Best Practices", + "section": "Query Parameters over Path Parameters - Cleaner URL patterns", + "text": "Query Parameters over Path Parameters - Cleaner URL patterns\nBefore:\n@rt(\"/toggle/{todo_id}\")\ndef post(todo_id: int): ...\n# URL: /toggle/123\nAfter:\n@rt\ndef toggle(id: int): ...\n# URL: /toggle?id=123\nQuery parameters are more idiomatic in FastHTML and avoid duplicating param names in the path.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#leverage-return-values---chain-operations-in-one-line", + "href": "tutorials/best_practice.html#leverage-return-values---chain-operations-in-one-line", + "title": "FastHTML Best Practices", + "section": "Leverage Return Values - Chain operations in one line", + "text": "Leverage Return Values - Chain operations in one line\n\nBefore:\n@rt\ndef add(task: str):\n new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat())\n return todo_item(todos[new_todo])\n\n@rt\ndef toggle(id: int):\n todo = todos[id]\n todos.update(completed=not todo.completed, id=id)\n return todo_item(todos[id])\nAfter:\n@rt\ndef add(task: str):\n return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat()))\n\n@rt\ndef toggle(id: int):\n return todo_item(todos.update(completed=not todos[id].completed, id=id))\nBoth insert() and update() return the affected object, enabling functional chaining.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#use-.to-for-url-generation---type-safe-route-references", + "href": "tutorials/best_practice.html#use-.to-for-url-generation---type-safe-route-references", + "title": "FastHTML Best Practices", + "section": "Use .to() for URL Generation - Type-safe route references", + "text": "Use .to() for URL Generation - Type-safe route references\nBefore:\nhx_post=f\"/toggle?id={todo.id}\"\nAfter:\nhx_post=toggle.to(id=todo.id)\nThe .to() method generates URLs with type safety and is refactoring-friendly.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#built-in-css-frameworks---picocss-comes-free-with-fast_app", + "href": "tutorials/best_practice.html#built-in-css-frameworks---picocss-comes-free-with-fast_app", + "title": "FastHTML Best Practices", + "section": "Built-in CSS Frameworks - PicoCSS comes free with fast_app()", + "text": "Built-in CSS Frameworks - PicoCSS comes free with fast_app()\nBefore:\nstyle = Style(\"\"\"\n.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; }\n/* ... many more lines ... */\n\"\"\")\nAfter:\n# Just use semantic HTML - Pico styles it automatically\nContainer(...), Article(...), Card(...), Group(...)\nfast_app() includes PicoCSS by default. Use semantic HTML elements that Pico styles automatically. Use MonsterUI (like shadcn, but for FastHTML) for more complex UI needs.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#smart-defaults---titled-creates-container-serve-handles-main", + "href": "tutorials/best_practice.html#smart-defaults---titled-creates-container-serve-handles-main", + "title": "FastHTML Best Practices", + "section": "Smart Defaults - Titled creates Container, serve() handles main", + "text": "Smart Defaults - Titled creates Container, serve() handles main\nBefore:\nreturn Titled(\"Todo List\", Container(...))\n\nif __name__ == \"__main__\":\n serve()\nAfter:\nreturn Titled(\"Todo List\", ...) # Container is automatic\n\nserve() # No need for if __name__ guard\nTitled already wraps content in a Container, and serve() handles the main check internally.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#fasthtml-handles-iterables---no-unpacking-needed-for-generators", + "href": "tutorials/best_practice.html#fasthtml-handles-iterables---no-unpacking-needed-for-generators", + "title": "FastHTML Best Practices", + "section": "FastHTML Handles Iterables - No unpacking needed for generators", + "text": "FastHTML Handles Iterables - No unpacking needed for generators\nBefore:\nSection(*[todo_item(todo) for todo in all_todos], id=\"todo-list\")\nAfter:\nSection(map(todo_item, all_todos), id=\"todo-list\")\nFastHTML components accept iterables directly - no need to unpack with *.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#functional-patterns---use-map-over-list-comprehensions", + "href": "tutorials/best_practice.html#functional-patterns---use-map-over-list-comprehensions", + "title": "FastHTML Best Practices", + "section": "Functional Patterns - Use map() over list comprehensions", + "text": "Functional Patterns - Use map() over list comprehensions\nList comprehensions are great, but map() is often cleaner for simple transformations, especially when combined with FastHTML’s iterable handling.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#minimal-code---remove-comments-and-unnecessary-returns", + "href": "tutorials/best_practice.html#minimal-code---remove-comments-and-unnecessary-returns", + "title": "FastHTML Best Practices", + "section": "Minimal Code - Remove comments and unnecessary returns", + "text": "Minimal Code - Remove comments and unnecessary returns\nBefore:\n@rt\ndef delete(id: int):\n # Delete from database\n todos.delete(id)\n # Return empty response\n return \"\"\nAfter:\n@rt\ndef delete(id: int): todos.delete(id)\n\nSkip comments when code is self-documenting\nDon’t return empty strings - None is returned by default\nUse a single line for a single idea.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#use-post-for-all-mutations", + "href": "tutorials/best_practice.html#use-post-for-all-mutations", + "title": "FastHTML Best Practices", + "section": "Use POST for All Mutations", + "text": "Use POST for All Mutations\nBefore:\nhx_delete=f\"/delete?id={todo.id}\"\nAfter:\nhx_post=delete.to(id=todo.id)\nFastHTML routes handle only GET and POST by default. Using only these two verbs is more idiomatic and simpler.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/best_practice.html#modern-htmx-event-syntax", + "href": "tutorials/best_practice.html#modern-htmx-event-syntax", + "title": "FastHTML Best Practices", + "section": "Modern HTMX Event Syntax", + "text": "Modern HTMX Event Syntax\nBefore:\nhx_on=\"htmx:afterRequest: this.reset()\"\nAfter:\nhx_on__after_request=\"this.reset()\"\nThis works because:\n\nhx-on=\"event: code\" is deprecated; hx-on-event=\"code\" is preferred\nFastHTML converts _ to - (so hx_on__after_request becomes hx-on--after-request)\n:: in HTMX can be used as a shortcut for :htmx:.\nHTMX natively accepts - instead of : (so -htmx- works like :htmx:)\nHTMX accepts e.g after-request as an alternative to camelCase afterRequest", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML Best Practices" + ] + }, + { + "objectID": "tutorials/by_example.html", + "href": "tutorials/by_example.html", + "title": "FastHTML By Example", + "section": "", + "text": "This tutorial provides an alternate introduction to FastHTML by building out example applications. We also illustrate how to use FastHTML foundations to create custom web apps. Finally, this document serves as minimal context for a LLM to turn it into a FastHTML assistant.\nLet’s get started.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#fasthtml-basics", + "href": "tutorials/by_example.html#fasthtml-basics", + "title": "FastHTML By Example", + "section": "FastHTML Basics", + "text": "FastHTML Basics\nFastHTML is just Python. You can install it with pip install python-fasthtml. Extensions/components built for it can likewise be distributed via PyPI or as simple Python files.\nThe core usage of FastHTML is to define routes, and then to define what to do at each route. This is similar to the FastAPI web framework (in fact we implemented much of the functionality to match the FastAPI usage examples), but where FastAPI focuses on returning JSON data to build APIs, FastHTML focuses on returning HTML data.\nHere’s a simple FastHTML app that returns a “Hello, World” message:\n\nfrom fasthtml.common import FastHTML, serve\n\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n return \"<h1>Hello, World</h1>\"\n\nserve()\n\nTo run this app, place it in a file, say app.py, and then run it with python app.py.\nINFO: Will watch for changes in these directories: ['/home/jonathan/fasthtml-example']\nINFO: Uvicorn running on http://127.0.0.1:5001 (Press CTRL+C to quit)\nINFO: Started reloader process [871942] using WatchFiles\nINFO: Started server process [871945]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nIf you navigate to http://127.0.0.1:5001 in a browser, you’ll see your “Hello, World”. If you edit the app.py file and save it, the server will reload and you’ll see the updated message when you refresh the page in your browser.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#constructing-html", + "href": "tutorials/by_example.html#constructing-html", + "title": "FastHTML By Example", + "section": "Constructing HTML", + "text": "Constructing HTML\nNotice we wrote some HTML in the previous example. We don’t want to do that! Some web frameworks require that you learn HTML, CSS, JavaScript AND some templating language AND python. We want to do as much as possible with just one language. Fortunately, the Python module fastcore.xml has all we need for constructing HTML from Python, and FastHTML includes all the tags you need to get started. For example:\n\nfrom fasthtml.common import *\npage = Html(\n Head(Title('Some page')),\n Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src=\"https://placehold.co/200\"), cls='myclass')))\nprint(to_xml(page))\n\n<!doctype html></!doctype>\n\n<html>\n <head>\n <title>Some page</title>\n </head>\n <body>\n <div class=\"myclass\">\nSome text, \n <a href=\"https://example.com\">A link</a>\n <img src=\"https://placehold.co/200\">\n </div>\n </body>\n</html>\n\n\n\n\nshow(page)\n\n\n\n\n \n Some page\n \n \n \nSome text, \n A link\n \n \n \n\n\n\nIf that import * worries you, you can always import only the tags you need.\nFastHTML is smart enough to know about fastcore.xml, and so you don’t need to use the to_xml function to convert your FT objects to HTML. You can just return them as you would any other Python object. For example, if we modify our previous example to use fastcore.xml, we can return an FT object directly:\n\nfrom fasthtml.common import *\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n page = Html(\n Head(Title('Some page')),\n Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src=\"https://placehold.co/200\"), cls='myclass')))\n return page\n\nserve()\n\nThis will render the HTML in the browser.\nFor debugging, you can right-click on the rendered HTML in the browser and select “Inspect” to see the underlying HTML that was generated. There you’ll also find the ‘network’ tab, which shows you the requests that were made to render the page. Refresh and look for the request to 127.0.0.1 - and you’ll see it’s just a GET request to /, and the response body is the HTML you just returned.\n\n\n\n\n\n\nLive Reloading\n\n\n\nYou can also enable live reloading so you don’t have to manually refresh your browser to view updates.\n\n\nYou can also use Starlette’s TestClient to try it out in a notebook:\n\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\nr = client.get(\"/\")\nprint(r.text)\n\n<html>\n <head><title>Some page</title>\n</head>\n <body><div class=\"myclass\">\nSome text, \n <a href=\"https://example.com\">A link</a>\n <img src=\"https://placehold.co/200\">\n</div>\n</body>\n</html>\n\n\n\nFastHTML wraps things in an Html tag if you don’t do it yourself (unless the request comes from htmx, in which case you get the element directly). See FT objects and HTML for more on creating custom components or adding HTML rendering to existing Python objects. To give the page a non-default title, return a Title before your main content:\n\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n return Title(\"Page Demo\"), Div(H1('Hello, World'), P('Some text'), P('Some more text'))\n\nclient = TestClient(app)\nprint(client.get(\"/\").text)\n\n<!doctype html></!doctype>\n\n<html>\n <head>\n <title>Page Demo</title>\n <meta charset=\"utf-8\"></meta>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\"></meta>\n <script src=\"https://unpkg.com/htmx.org@next/dist/htmx.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@1.3.0/surreal.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script>\n </head>\n <body>\n<div>\n <h1>Hello, World</h1>\n <p>Some text</p>\n <p>Some more text</p>\n</div>\n </body>\n</html>\n\n\n\nWe’ll use this pattern often in the examples to follow.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#defining-routes", + "href": "tutorials/by_example.html#defining-routes", + "title": "FastHTML By Example", + "section": "Defining Routes", + "text": "Defining Routes\nThe HTTP protocol defines a number of methods (‘verbs’) to send requests to a server. The most common are GET, POST, PUT, DELETE, and HEAD. We saw ‘GET’ in action before - when you navigate to a URL, you’re making a GET request to that URL. We can do different things on a route for different HTTP methods. For example:\n@app.route(\"/\", methods='get')\ndef home():\n return H1('Hello, World')\n\n@app.route(\"/\", methods=['post', 'put'])\ndef post_or_put():\n return \"got a POST or PUT request\"\nThis says that when someone navigates to the root URL “/” (i.e. sends a GET request), they will see the big “Hello, World” heading. When someone submits a POST or PUT request to the same URL, the server should return the string “got a post or put request”.\n\n\n\n\n\n\nTest the POST request\n\n\n\nYou can test the POST request with curl -X POST http://127.0.0.1:8000 -d \"some data\". This sends some data to the server, you should see the response “got a post or put request” printed in the terminal.\n\n\nThere are a few other ways you can specify the route+method - FastHTML has .get, .post, etc. as shorthand for route(..., methods=['get']), etc.\n\n@app.get(\"/\")\ndef my_function():\n return \"Hello World from a GET request\"\n\nOr you can use the @rt decorator without a method but specify the method with the name of the function. For example:\n\nrt = app.route\n\n@rt(\"/\")\ndef post():\n return \"Hello World from a POST request\"\n\n\nclient.post(\"/\").text\n\n'Hello World from a POST request'\n\n\nYou’re welcome to pick whichever style you prefer. Using routes lets you show different content on different pages - ‘/home’, ‘/about’ and so on. You can also respond differently to different kinds of requests to the same route, as shown above. You can also pass data via the route:\n\n@app.get@rt\n\n\n\n@app.get(\"/greet/{nm}\")\ndef greet(nm:str):\n return f\"Good day to you, {nm}!\"\n\nclient.get(\"/greet/Dave\").text\n\n'Good day to you, Dave!'\n\n\n\n\n\n@rt(\"/greet/{nm}\")\ndef get(nm:str):\n return f\"Good day to you, {nm}!\"\n\nclient.get(\"/greet/Dave\").text\n\n'Good day to you, Dave!'\n\n\n\n\n\nMore on this in the More on Routing and Request Parameters section, which goes deeper into the different ways to get information from a request.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#styling-basics", + "href": "tutorials/by_example.html#styling-basics", + "title": "FastHTML By Example", + "section": "Styling Basics", + "text": "Styling Basics\nPlain HTML probably isn’t quite what you imagine when you visualize your beautiful web app. CSS is the go-to language for styling HTML. But again, we don’t want to learn extra languages unless we absolutely have to! Fortunately, there are ways to get much more visually appealing sites by relying on the hard work of others, using existing CSS libraries. One of our favourites is PicoCSS. A common way to add CSS files to web pages is to use a <link> tag inside your HTML header, like this:\n<header>\n ...\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css\">\n</header>\nFor convenience, FastHTML already defines a Pico component for you with picolink:\n\nprint(to_xml(picolink))\n\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css\">\n\n<style>:root { --pico-font-size: 100%; }</style>\n\n\n\n\n\n\n\n\n\nNote\n\n\n\npicolink also includes a <style> tag, as we found that setting the font-size to 100% to be a good default. We show you how to override this below.\n\n\nSince we typically want CSS styling on all pages of our app, FastHTML lets you define a shared HTML header with the hdrs argument as shown below:\n\nfrom fasthtml.common import *\n1css = Style(':root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}')\n2app = FastHTML(hdrs=(picolink, css))\n\n@app.route(\"/\")\ndef get():\n return (Title(\"Hello World\"), \n3 Main(H1('Hello, World'), cls=\"container\"))\n\n\n1\n\nCustom styling to override the pico defaults\n\n2\n\nDefine shared headers for all pages\n\n3\n\nAs per the pico docs, we put all of our content inside a <main> tag with a class of container:\n\n\n\n\n\n\n\n\n\n\nReturning Tuples\n\n\n\nWe’re returning a tuple here (a title and the main page). Returning a tuple, list, FT object, or an object with a __ft__ method tells FastHTML to turn the main body into a full HTML page that includes the headers (including the pico link and our custom css) which we passed in. This only occurs if the request isn’t from HTMX (for HTMX requests we need only return the rendered components).\n\n\nYou can check out the Pico examples page to see how different elements will look. If everything is working, the page should now render nice text with our custom font, and it should respect the user’s light/dark mode preferences too.\nIf you want to override the default styles or add more custom CSS, you can do so by adding a <style> tag to the headers as shown above. So you are allowed to write CSS to your heart’s content - we just want to make sure you don’t necessarily have to! Later on we’ll see examples using other component libraries and tailwind css to do more fancy styling things, along with tips to get an LLM to write all those fiddly bits so you don’t have to.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#web-page---web-app", + "href": "tutorials/by_example.html#web-page---web-app", + "title": "FastHTML By Example", + "section": "Web Page -> Web App", + "text": "Web Page -> Web App\nShowing content is all well and good, but we typically expect a bit more interactivity from something calling itself a web app! So, let’s add a few different pages, and use a form to let users add messages to a list:\n\napp = FastHTML()\nmessages = [\"This is a message, which will get rendered as a paragraph\"]\n\n@app.get(\"/\")\ndef home():\n return Main(H1('Messages'), \n *[P(msg) for msg in messages],\n A(\"Link to Page 2 (to add messages)\", href=\"/page2\"))\n\n@app.get(\"/page2\")\ndef page2():\n return Main(P(\"Add a message with the form below:\"),\n Form(Input(type=\"text\", name=\"data\"),\n Button(\"Submit\"),\n action=\"/\", method=\"post\"))\n\n@app.post(\"/\")\ndef add_message(data:str):\n messages.append(data)\n return home()\n\nWe re-render the entire homepage to show the newly added message. This is fine, but modern web apps often don’t re-render the entire page, they just update a part of the page. In fact even very complicated applications are often implemented as ‘Single Page Apps’ (SPAs). This is where HTMX comes in.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#htmx", + "href": "tutorials/by_example.html#htmx", + "title": "FastHTML By Example", + "section": "HTMX", + "text": "HTMX\nHTMX addresses some key limitations of HTML. In vanilla HTML, links can trigger a GET request to show a new page, and forms can send requests containing data to the server. A lot of ‘Web 1.0’ design revolved around ways to use these to do everything we wanted. But why should only some elements be allowed to trigger requests? And why should we refresh the entire page with the result each time one does? HTMX extends HTML to allow us to trigger requests from any element on all kinds of events, and to update a part of the page without refreshing the entire page. It’s a powerful tool for building modern web apps.\nIt does this by adding attributes to HTML tags to make them do things. For example, here’s a page with a counter and a button that increments it:\n\napp = FastHTML()\n\ncount = 0\n\n@app.get(\"/\")\ndef home():\n return Title(\"Count Demo\"), Main(\n H1(\"Count Demo\"),\n P(f\"Count is set to {count}\", id=\"count\"),\n Button(\"Increment\", hx_post=\"/increment\", hx_target=\"#count\", hx_swap=\"innerHTML\")\n )\n\n@app.post(\"/increment\")\ndef increment():\n print(\"incrementing\")\n global count\n count += 1\n return f\"Count is set to {count}\"\n\nThe button triggers a POST request to /increment (since we set hx_post=\"/increment\"), which increments the count and returns the new count. The hx_target attribute tells HTMX where to put the result. If no target is specified it replaces the element that triggered the request. The hx_swap attribute specifies how it adds the result to the page. Useful options are:\n\ninnerHTML: Replace the target element’s content with the result.\nouterHTML: Replace the target element with the result.\nbeforebegin: Insert the result before the target element.\nbeforeend: Insert the result inside the target element, after its last child.\nafterbegin: Insert the result inside the target element, before its first child.\nafterend: Insert the result after the target element.\n\nYou can also use an hx_swap of delete to delete the target element regardless of response, or of none to do nothing.\nBy default, requests are triggered by the “natural” event of an element - click in the case of a button (and most other elements). You can also specify different triggers, along with various modifiers - see the HTMX docs for more.\nThis pattern of having elements trigger requests that modify or replace other elements is a key part of the HTMX philosophy. It takes a little getting used to, but once mastered it is extremely powerful.\n\nReplacing Elements Besides the Target\nSometimes having a single target is not enough, and we’d like to specify some additional elements to update or remove. In these cases, returning elements with an id that matches the element to be replaced and hx_swap_oob='true' will replace those elements too. We’ll use this in the next example to clear an input field when we submit a form.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#full-example-1---todo-app", + "href": "tutorials/by_example.html#full-example-1---todo-app", + "title": "FastHTML By Example", + "section": "Full Example #1 - ToDo App", + "text": "Full Example #1 - ToDo App\nThe canonical demo web app! A TODO list. Rather than create yet another variant for this tutorial, we recommend starting with this video tutorial from Jeremy:\n\n\n\n\nimage.png\n\n\nWe’ve made a number of variants of this app - so in addition to the version shown in the video you can browse this series of examples with increasing complexity, the heavily-commented “idiomatic” version here, and the example linked from the FastHTML homepage.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#full-example-2---image-generation-app", + "href": "tutorials/by_example.html#full-example-2---image-generation-app", + "title": "FastHTML By Example", + "section": "Full Example #2 - Image Generation App", + "text": "Full Example #2 - Image Generation App\nLet’s create an image generation app. We’d like to wrap a text-to-image model in a nice UI, where the user can type in a prompt and see a generated image appear. We’ll use a model hosted by Replicate to actually generate the images. Let’s start with the homepage, with a form to submit prompts and a div to hold the generated images:\n# Main page\n@app.get(\"/\")\ndef get():\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_list = Div(id='gen-list')\n return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')\nSubmitting the form will trigger a POST request to /, so next we need to generate an image and add it to the list. One problem: generating images is slow! We’ll start the generation in a separate thread, but this now surfaces a different problem: we want to update the UI right away, but our image will only be ready a few seconds later. This is a common pattern - think about how often you see a loading spinner online. We need a way to return a temporary bit of UI which will eventually be replaced by the final image. Here’s how we might do this:\ndef generation_preview(id):\n if os.path.exists(f\"gens/{id}.png\"):\n return Div(Img(src=f\"/gens/{id}.png\"), id=f'gen-{id}')\n else:\n return Div(\"Generating...\", id=f'gen-{id}', \n hx_post=f\"/generations/{id}\",\n hx_trigger='every 1s', hx_swap='outerHTML')\n \n@app.post(\"/generations/{id}\")\ndef get(id:int): return generation_preview(id)\n\n@app.post(\"/\")\ndef post(prompt:str):\n id = len(generations)\n generate_and_save(prompt, id)\n generations.append(prompt)\n clear_input = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\", hx_swap_oob='true')\n return generation_preview(id), clear_input\n\n@threaded\ndef generate_and_save(prompt, id): ... \nThe form sends the prompt to the / route, which starts the generation in a separate thread then returns two things:\n\nA generation preview element that will be added to the top of the gen-list div (since that is the target_id of the form which triggered the request)\nAn input field that will replace the form’s input field (that has the same id), using the hx_swap_oob=‘true’ trick. This clears the prompt field so the user can type another prompt.\n\nThe generation preview first returns a temporary “Generating…” message, which polls the /generations/{id} route every second. This is done by setting hx_post to the route and hx_trigger to ‘every 1s’. The /generations/{id} route returns the preview element every second until the image is ready, at which point it returns the final image. Since the final image replaces the temporary one (hx_swap=‘outerHTML’), the polling stops running and the generation preview is now complete.\nThis works nicely - the user can submit several prompts without having to wait for the first one to generate, and as the images become available they are added to the list. You can see the full code of this version here.\n\nAgain, with Style\nThe app is functional, but can be improved. The next version adds more stylish generation previews, lays out the images in a grid layout that is responsive to different screen sizes, and adds a database to track generations and make them persistent. The database part is very similar to the todo list example, so let’s just quickly look at how we add the nice grid layout. This is what the result looks like:\n\n\n\nimage.png\n\n\nStep one was looking around for existing components. The Pico CSS library we’ve been using has a rudimentary grid but recommends using an alternative layout system. One of the options listed was Flexbox.\nTo use Flexbox you create a “row” with one or more elements. You can specify how wide things should be with a specific syntax in the class name. For example, col-xs-12 means a box that will take up 12 columns (out of 12 total) of the row on extra small screens, col-sm-6 means a column that will take up 6 columns of the row on small screens, and so on. So if you want four columns on large screens you would use col-lg-3 for each item (i.e. each item is using 3 columns out of 12).\n<div class=\"row\">\n <div class=\"col-xs-12\">\n <div class=\"box\">This takes up the full width</div>\n </div>\n</div>\nThis was non-intuitive to me. Thankfully ChatGPT et al know web stuff quite well, and we can also experiment in a notebook to test things out:\n\ngrid = Html(\n Link(rel=\"stylesheet\", href=\"https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css\", type=\"text/css\"),\n Div(\n Div(Div(\"This takes up the full width\", cls=\"box\", style=\"background-color: #800000;\"), cls=\"col-xs-12\"),\n Div(Div(\"This takes up half\", cls=\"box\", style=\"background-color: #008000;\"), cls=\"col-xs-6\"),\n Div(Div(\"This takes up half\", cls=\"box\", style=\"background-color: #0000B0;\"), cls=\"col-xs-6\"),\n cls=\"row\", style=\"color: #fff;\"\n )\n)\nshow(grid)\n\n\n\n\n \n \n \n This takes up the full width\n \n \n This takes up half\n \n \n This takes up half\n \n \n\n\n\nAside: when in doubt with CSS stuff, add a background color or a border so you can see what’s happening!\nTranslating this into our app, we have a new homepage with a div (class=\"row\") to store the generated images / previews, and a generation_preview function that returns boxes with the appropriate classes and styles to make them appear in the grid. I chose a layout with different numbers of columns for different screen sizes, but you could also just specify the col-xs class if you wanted the same layout on all devices.\ngridlink = Link(rel=\"stylesheet\", href=\"https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css\", type=\"text/css\")\napp = FastHTML(hdrs=(picolink, gridlink))\n\n# Main page\n@app.get(\"/\")\ndef get():\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_containers = [generation_preview(g) for g in gens(limit=10)] # Start with last 10\n gen_list = Div(*gen_containers[::-1], id='gen-list', cls=\"row\") # flexbox container: class = row\n return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')\n\n# Show the image (if available) and prompt for a generation\ndef generation_preview(g):\n grid_cls = \"box col-xs-12 col-sm-6 col-md-4 col-lg-3\"\n image_path = f\"{g.folder}/{g.id}.png\"\n if os.path.exists(image_path):\n return Div(Card(\n Img(src=image_path, alt=\"Card image\", cls=\"card-img-top\"),\n Div(P(B(\"Prompt: \"), g.prompt, cls=\"card-text\"),cls=\"card-body\"),\n ), id=f'gen-{g.id}', cls=grid_cls)\n return Div(f\"Generating gen {g.id} with prompt {g.prompt}\", \n id=f'gen-{g.id}', hx_get=f\"/gens/{g.id}\", \n hx_trigger=\"every 2s\", hx_swap=\"outerHTML\", cls=grid_cls)\nYou can see the final result in main.py in the image_app_simple example directory, along with info on deploying it (tl;dr don’t!). We’ve also deployed a version that only shows your generations (tied to browser session) and has a credit system to save our bank accounts. You can access that here. Now for the next question: how do we keep track of different users?\n\n\nAgain, with Sessions\nAt the moment everyone sees all images! How do we keep some sort of unique identifier tied to a user? Before going all the way to setting up users, login pages etc., let’s look at a way to at least limit generations to the user’s session. You could do this manually with cookies. For convenience and security, fasthtml (via Starlette) has a special mechanism for storing small amounts of data in the user’s browser via the session argument to your route. This acts like a dictionary and you can set and get values from it. For example, here we look for a session_id key, and if it doesn’t exist we generate a new one:\n@app.get(\"/\")\ndef get(session):\n if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())\n return H1(f\"Session ID: {session['session_id']}\")\nRefresh the page a few times - you’ll notice that the session ID remains the same. If you clear your browsing data, you’ll get a new session ID. And if you load the page in a different browser (but not a different tab), you’ll get a new session ID. This will persist within the current browser, letting us use it as a key for our generations. As a bonus, someone can’t spoof this session id by passing it in another way (for example, sending a query parameter). Behind the scenes, the data is stored in a browser cookie but it is signed with a secret key that stops the user or anyone nefarious from being able to tamper with it. The cookie is decoded back into a dictionary by something called a middleware function, which we won’t cover here. All you need to know is that we can use this to store bits of state in the user’s browser.\nIn the image app example, we can add a session_id column to our database, and modify our homepage like so:\n@app.get(\"/\")\ndef get(session):\n if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_containers = [generation_preview(g) for g in gens(limit=10, where=f\"session_id == '{session['session_id']}'\")]\n ...\nSo we check if the session id exists in the session, add one if not, and then limit the generations shown to only those tied to this session id. We filter the database with a where clause - see [TODO link Jeremy’s example for a more reliable way to do this]. The only other change we need to make is to store the session id in the database when a generation is made. You can check out this version here. You could instead write this app without relying on a database at all - simply storing the filenames of the generated images in the session, for example. But this more general approach of linking some kind of unique session identifier to users or data in our tables is a useful general pattern for more complex examples.\n\n\nAgain, with Credits!\nGenerating images with replicate costs money. So next let’s add a pool of credits that get used up whenever anyone generates an image. To recover our lost funds, we’ll also set up a payment system so that generous users can buy more credits for everyone. You could modify this to let users buy credits tied to their session ID, but at that point you risk having angry customers losing their money after wiping their browser history, and should consider setting up proper account management :)\nTaking payments with Stripe is intimidating but very doable. Here’s a tutorial that shows the general principle using Flask. As with other popular tasks in the web-dev world, ChatGPT knows a lot about Stripe - but you should exercise extra caution when writing code that handles money!\nFor the finished example we add the bare minimum:\n\nA way to create a Stripe checkout session and redirect the user to the session URL\n‘Success’ and ‘Cancel’ routes to handle the result of the checkout\nA route that listens for a webhook from Stripe to update the number of credits when a payment is made.\n\nIn a typical application you’ll want to keep track of which users make payments, catch other kinds of stripe events and so on. This example is more a ‘this is possible, do your own research’ than ‘this is how you do it’. But hopefully it does illustrate the key idea: there is no magic here. Stripe (and many other technologies) relies on sending users to different routes and shuttling data back and forth in requests. And we know how to do that!", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#more-on-routing-and-request-parameters", + "href": "tutorials/by_example.html#more-on-routing-and-request-parameters", + "title": "FastHTML By Example", + "section": "More on Routing and Request Parameters", + "text": "More on Routing and Request Parameters\nThere are a number of ways information can be passed to the server. When you specify arguments to a route, FastHTML will search the request for values with the same name, and convert them to the correct type. In order, it searches\n\nThe path parameters\nThe query parameters\nThe cookies\nThe headers\nThe session\nForm data\n\nThere are also a few special arguments\n\nrequest (or any prefix like req): gets the raw Starlette Request object\nsession (or any prefix like sess): gets the session object\nauth\nhtmx\napp\n\nIn this section let’s quickly look at some of these in action.\n\nfrom fasthtml.common import *\nfrom starlette.testclient import TestClient\n\napp = FastHTML()\ncli = TestClient(app)\n\nPart of the route (path parameters):\n\n@app.get('/user/{nm}')\ndef _(nm:str): return f\"Good day to you, {nm}!\"\n\ncli.get('/user/jph').text\n\n'Good day to you, jph!'\n\n\nMatching with a regex:\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm\")\n\n@app.get(r'/static/{path:path}/{fn}.{ext:imgext}')\ndef get_img(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\ncli.get('/static/foo/jph.ico').text\n\n'Getting jph.ico from /foo/'\n\n\nUsing an enum (try using a string that isn’t in the enum):\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\n\n@app.get(\"/models/{nm}\")\ndef model(nm:ModelName): return nm\n\nprint(cli.get('/models/alexnet').text)\n\nalexnet\n\n\nCasting to a Path:\n\n@app.get(\"/files/{path}\")\ndef txt(path: Path): return path.with_suffix('.txt')\n\nprint(cli.get('/files/foo').text)\n\nfoo.txt\n\n\nAn integer with a default value:\n\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n@app.get(\"/items/\")\ndef read_item(idx: int = 0): return fake_db[idx]\n\nprint(cli.get('/items/?idx=1').text)\n\n{\"name\":\"Bar\"}\n\n\n\n# Equivalent to `/items/?idx=0`.\nprint(cli.get('/items/').text)\n\n{\"name\":\"Foo\"}\n\n\nBoolean values (takes anything “truthy” or “falsy”):\n\n@app.get(\"/booly/\")\ndef booly(coming:bool=True): return 'Coming' if coming else 'Not coming'\n\nprint(cli.get('/booly/?coming=true').text)\n\nComing\n\n\n\nprint(cli.get('/booly/?coming=no').text)\n\nNot coming\n\n\nGetting dates:\n\n@app.get(\"/datie/\")\ndef datie(d:parsed_date): return d\n\ndate_str = \"17th of May, 2024, 2p\"\nprint(cli.get(f'/datie/?d={date_str}').text)\n\n2024-05-17 14:00:00\n\n\nMatching a dataclass:\n\nfrom dataclasses import dataclass, asdict\n\n@dataclass\nclass Bodie:\n a:int;b:str\n\n@app.route(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\ncli.post('/bodie/me', data=dict(a=1, b='foo')).text\n\n'{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}'\n\n\n\nCookies\nCookies can be set via a Starlette Response object, and can be read back by specifying the name:\n\nfrom datetime import datetime\n\n@app.get(\"/setcookie\")\ndef setc(req):\n now = datetime.now()\n res = Response(f'Set to {now}')\n res.set_cookie('now', str(now))\n return res\n\ncli.get('/setcookie').text\n\n'Set to 2024-07-20 23:14:54.364793'\n\n\n\n@app.get(\"/getcookie\")\ndef getc(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\ncli.get('/getcookie').text\n\n'Cookie was set at time 23:14:54.364793'\n\n\n\n\nUser Agent and HX-Request\nAn argument of user_agent will match the header User-Agent. This holds for special headers like HX-Request (used by HTMX to signal when a request comes from an HTMX request) - the general pattern is that “-” is replaced with “_” and strings are turned to lowercase.\n\n@app.get(\"/ua\")\nasync def ua(user_agent:str): return user_agent\n\ncli.get('/ua', headers={'User-Agent':'FastHTML'}).text\n\n'FastHTML'\n\n\n\n@app.get(\"/hxtest\")\ndef hxtest(htmx): return htmx.request\n\ncli.get('/hxtest', headers={'HX-Request':'1'}).text\n\n'1'\n\n\n\n\nStarlette Requests\nIf you add an argument called request(or any prefix of that, for example req) it will be populated with the Starlette Request object. This is useful if you want to do your own processing manually. For example, although FastHTML will parse forms for you, you could instead get form data like so:\n@app.get(\"/form\")\nasync def form(request:Request):\n form_data = await request.form()\n a = form_data.get('a')\nSee the Starlette docs for more information on the Request object.\n\n\nStarlette Responses\nYou can return a Starlette Response object from a route to control the response. For example:\n@app.get(\"/redirect\")\ndef redirect():\n return RedirectResponse(url=\"/\")\nWe used this to set cookies in the previous example. See the Starlette docs for more information on the Response object.\n\n\nStatic Files\nWe often want to serve static files like images. This is easily done! For common file types (images, CSS etc) we can create a route that returns a Starlette FileResponse like so:\n# For images, CSS, etc.\n@app.get(\"/{fname:path}.{ext:static}\")\ndef static(fname: str, ext: str):\n return FileResponse(f'{fname}.{ext}')\nYou can customize it to suit your needs (for example, only serving files in a certain directory). You’ll notice some variant of this route in all our complete examples - even for apps with no static files the browser will typically request a /favicon.ico file, for example, and as the astute among you will have noticed this has sparked a bit of competition between Johno and Jeremy regarding which country flag should serve as the default!\n\n\nWebSockets\nFor certain applications such as multiplayer games, websockets can be a powerful feature. Luckily HTMX and FastHTML has you covered! Simply specify that you wish to include the websocket header extension from HTMX:\napp = FastHTML(exts='ws')\nrt = app.route\nWith that, you are now able to specify the different websocket specific HTMX goodies. For example, say we have a website we want to setup a websocket, you can simply:\ndef mk_inp(): return Input(id='msg')\n\n@rt('/')\nasync def get(request):\n cts = Div(\n Div(id='notifications'),\n Form(mk_inp(), id='form', ws_send=True),\n hx_ext='ws', ws_connect='/ws')\n return Titled('Websocket Test', cts)\nAnd this will setup a connection on the route /ws along with a form that will send a message to the websocket whenever the form is submitted. Let’s go ahead and handle this route:\n@app.ws('/ws')\nasync def ws(msg:str, send):\n await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()\nOne thing you might have noticed is a lack of target id for our websocket trigger for swapping HTML content. This is because HTMX always swaps content with websockets with Out of Band Swaps. Therefore, HTMX will look for the id in the returned HTML content from the server for determining what to swap. To send stuff to the client, you can either use the send parameter or simply return the content or both!\nNow, sometimes you might want to perform actions when a client connects or disconnects such as add or remove a user from a player queue. To hook into these events, you can pass your connection or disconnection function to the app.ws decorator:\nasync def on_connect(send):\n print('Connected!')\n await send(Div('Hello, you have connected', id=\"notifications\"))\n\nasync def on_disconnect(ws):\n print('Disconnected!')\n\n@app.ws('/ws', conn=on_connect, disconn=on_disconnect)\nasync def ws(msg:str, send):\n await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#full-example-3---chatbot-example-with-daisyui-components", + "href": "tutorials/by_example.html#full-example-3---chatbot-example-with-daisyui-components", + "title": "FastHTML By Example", + "section": "Full Example #3 - Chatbot Example with DaisyUI Components", + "text": "Full Example #3 - Chatbot Example with DaisyUI Components\nLet’s go back to the topic of adding components or styling beyond the simple PicoCSS examples so far. How might we adopt a component or framework? In this example, let’s build a chatbot UI leveraging the DaisyUI chat bubble. The final result will look like this:\n\n\n\nimage.png\n\n\nAt first glance, DaisyUI’s chat component looks quite intimidating. The examples look like this:\n<div class=\"chat chat-start\">\n <div class=\"chat-image avatar\">\n <div class=\"w-10 rounded-full\">\n <img alt=\"Tailwind CSS chat bubble component\" src=\"https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg\" />\n </div>\n </div>\n <div class=\"chat-header\">\n Obi-Wan Kenobi\n <time class=\"text-xs opacity-50\">12:45</time>\n </div>\n <div class=\"chat-bubble\">You were the Chosen One!</div>\n <div class=\"chat-footer opacity-50\">\n Delivered\n </div>\n</div>\n<div class=\"chat chat-end\">\n <div class=\"chat-image avatar\">\n <div class=\"w-10 rounded-full\">\n <img alt=\"Tailwind CSS chat bubble component\" src=\"https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg\" />\n </div>\n </div>\n <div class=\"chat-header\">\n Anakin\n <time class=\"text-xs opacity-50\">12:46</time>\n </div>\n <div class=\"chat-bubble\">I hate you!</div>\n <div class=\"chat-footer opacity-50\">\n Seen at 12:46\n </div>\n</div>\nWe have several things going for us however.\n\nChatGPT knows DaisyUI and Tailwind (DaisyUI is a Tailwind component library)\nWe can build things up piece by piece with AI standing by to help.\n\nhttps://h2f.answer.ai/ is a tool that can convert HTML to FT (fastcore.xml) and back, which is useful for getting a quick starting point when you have an HTML example to start from.\nWe can strip out some unnecessary bits and try to get the simplest possible example working in a notebook first:\n\n# Loading tailwind and daisyui\nheaders = (Script(src=\"https://cdn.tailwindcss.com\"),\n Link(rel=\"stylesheet\", href=\"https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css\"))\n\n# Displaying a single message\nd = Div(\n Div(\"Chat header here\", cls=\"chat-header\"),\n Div(\"My message goes here\", cls=\"chat-bubble chat-bubble-primary\"),\n cls=\"chat chat-start\"\n)\n# show(Html(*headers, d)) # uncomment to view\n\nNow we can extend this to render multiple messages, with the message being on the left (chat-start) or right (chat-end) depending on the role. While we’re at it, we can also change the color (chat-bubble-primary) of the message and put them all in a chat-box div:\n\nmessages = [\n {\"role\":\"user\", \"content\":\"Hello\"},\n {\"role\":\"assistant\", \"content\":\"Hi, how can I assist you?\"}\n]\n\ndef ChatMessage(msg):\n return Div(\n Div(msg['role'], cls=\"chat-header\"),\n Div(msg['content'], cls=f\"chat-bubble chat-bubble-{'primary' if msg['role'] == 'user' else 'secondary'}\"),\n cls=f\"chat chat-{'end' if msg['role'] == 'user' else 'start'}\")\n\nchatbox = Div(*[ChatMessage(msg) for msg in messages], cls=\"chat-box\", id=\"chatlist\")\n\n# show(Html(*headers, chatbox)) # Uncomment to view\n\nNext, it was back to the ChatGPT to tweak the chat box so it wouldn’t grow as messages were added. I asked:\n\"I have something like this (it's working now) \n[code]\nThe messages are added to this div so it grows over time. \nIs there a way I can set it's height to always be 80% of the total window height with a scroll bar if needed?\"\nBased on this query GPT4o helpfully shared that “This can be achieved using Tailwind CSS utility classes. Specifically, you can use h-[80vh] to set the height to 80% of the viewport height, and overflow-y-auto to add a vertical scroll bar when needed.”\nTo put it another way: none of the CSS classes in the following example were written by a human, and what edits I did make were informed by advice from the AI that made it relatively painless!\nThe actual chat functionality of the app is based on our claudette library. As with the image example, we face a potential hiccup in that getting a response from an LLM is slow. We need a way to have the user message added to the UI immediately, and then have the response added once it’s available. We could do something similar to the image generation example above, or use websockets. Check out the full example for implementations of both, along with further details.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#full-example-4---multiplayer-game-of-life-example-with-websockets", + "href": "tutorials/by_example.html#full-example-4---multiplayer-game-of-life-example-with-websockets", + "title": "FastHTML By Example", + "section": "Full Example #4 - Multiplayer Game of Life Example with Websockets", + "text": "Full Example #4 - Multiplayer Game of Life Example with Websockets\nLet’s see how we can implement a collaborative website using Websockets in FastHTML. To showcase this, we will use the famous Conway’s Game of Life, which is a game that takes place in a grid world. Each cell in the grid can be either alive or dead. The cell’s state is initially given by a user before the game is started and then evolves through the iteration of the grid world once the clock starts. Whether a cell’s state will change from the previous state depends on simple rules based on its neighboring cells’ states. Here is the standard Game of Life logic implemented in Python courtesy of ChatGPT:\ngrid = [[0 for _ in range(20)] for _ in range(20)]\ndef update_grid(grid: list[list[int]]) -> list[list[int]]:\n new_grid = [[0 for _ in range(20)] for _ in range(20)]\n def count_neighbors(x, y):\n directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]\n count = 0\n for dx, dy in directions:\n nx, ny = x + dx, y + dy\n if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]): count += grid[nx][ny]\n return count\n for i in range(len(grid)):\n for j in range(len(grid[0])):\n neighbors = count_neighbors(i, j)\n if grid[i][j] == 1:\n if neighbors < 2 or neighbors > 3: new_grid[i][j] = 0\n else: new_grid[i][j] = 1\n elif neighbors == 3: new_grid[i][j] = 1\n return new_grid\nThis would be a very dull game if we were to run it, since the initial state of everything would remain dead. Therefore, we need a way of letting the user give an initial state before starting the game. FastHTML to the rescue!\ndef Grid():\n cells = []\n for y, row in enumerate(game_state['grid']):\n for x, cell in enumerate(row):\n cell_class = 'alive' if cell else 'dead'\n cell = Div(cls=f'cell {cell_class}', hx_put='/update', hx_vals={'x': x, 'y': y}, hx_swap='none', hx_target='#gol', hx_trigger='click')\n cells.append(cell)\n return Div(*cells, id='grid')\n\n@rt('/update')\nasync def put(x: int, y: int):\n grid[y][x] = 1 if grid[y][x] == 0 else 0\nAbove is a component for representing the game’s state that the user can interact with and update on the server using cool HTMX features such as hx_vals for determining which cell was clicked to make it dead or alive. Now, you probably noticed that the HTTP request in this case is a PUT request, which does not return anything and this means our client’s view of the grid world and the server’s game state will immediately become out of sync :(. We could of course just return a new Grid component with the updated state, but that would only work for a single client, if we had more, they quickly get out of sync with each other and the server. Now Websockets to the rescue!\nWebsockets are a way for the server to keep a persistent connection with clients and send data to the client without explicitly being requested for information, which is not possible with HTTP. Luckily FastHTML and HTMX work well with Websockets. Simply state you wish to use websockets for your app and define a websocket route:\n...\napp = FastHTML(hdrs=(picolink, gridlink, css, htmx_ws), exts='ws')\n\nplayer_queue = []\nasync def update_players():\n for i, player in enumerate(player_queue):\n try: await player(Grid())\n except: player_queue.pop(i)\nasync def on_connect(send): player_queue.append(send)\nasync def on_disconnect(send): await update_players()\n\n@app.ws('/gol', conn=on_connect, disconn=on_disconnect)\nasync def ws(msg:str, send): pass\n\ndef Home(): return Title('Game of Life'), Main(gol, Div(Grid(), id='gol', cls='row center-xs'), hx_ext=\"ws\", ws_connect=\"/gol\")\n\n@rt('/update')\nasync def put(x: int, y: int):\n grid[y][x] = 1 if grid[y][x] == 0 else 0\n await update_players()\n...\nHere we simply keep track of all the players that have connected or disconnected to our site and when an update occurs, we send updates to all the players still connected via websockets. Via HTMX, you are still simply exchanging HTML from the server to the client and will swap in the content based on how you setup your hx_swap attribute. There is only one difference, that being all swaps are OOB. You can find more information on the HTMX websocket extension documentation page here. You can find a full fledge hosted example of this app here.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#ft-objects-and-html", + "href": "tutorials/by_example.html#ft-objects-and-html", + "title": "FastHTML By Example", + "section": "FT objects and HTML", + "text": "FT objects and HTML\nThese FT objects create a ‘FastTag’ structure [tag,children,attrs] for to_xml(). When we call Div(...), the elements we pass in are the children. Attributes are passed in as keywords. class and for are special words in python, so we use cls, klass or _class instead of class and fr or _for instead of for. Note these objects are just 3-element lists - you can create custom ones too as long as they’re also 3-element lists. Alternately, leaf nodes can be strings instead (which is why you can do Div('some text')). If you pass something that isn’t a 3-element list or a string, it will be converted to a string using str()… unless (our final trick) you define a __ft__ method that will run before str(), so you can render things a custom way.\nFor example, here’s one way we could make a custom class that can be rendered into HTML:\n\nclass Person:\n def __init__(self, name, age):\n self.name = name\n self.age = age\n\n def __ft__(self):\n return ['div', [f'{self.name} is {self.age} years old.'], {}]\n\np = Person('Jonathan', 28)\nprint(to_xml(Div(p, \"more text\", cls=\"container\")))\n\n<div class=\"container\">\n <div>Jonathan is 28 years old.</div>\nmore text\n</div>\n\n\n\nIn the examples, you’ll see we often patch in __ft__ methods to existing classes to control how they’re rendered. For example, if Person didn’t have a __ft__ method or we wanted to override it, we could add a new one like this:\n\nfrom fastcore.all import patch\n\n@patch\ndef __ft__(self:Person):\n return Div(\"Person info:\", Ul(Li(\"Name:\",self.name), Li(\"Age:\", self.age)))\n\nshow(p)\n\n\nPerson info:\n \n \nName:\nJonathan\n \n \nAge:\n28\n \n \n\n\n\nSome tags from fastcore.xml are overwritten by fasthtml.core and a few are further extended by fasthtml.xtend using this method. Over time, we hope to see others developing custom components too, giving us a larger and larger ecosystem of reusable components.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#custom-scripts-and-styling", + "href": "tutorials/by_example.html#custom-scripts-and-styling", + "title": "FastHTML By Example", + "section": "Custom Scripts and Styling", + "text": "Custom Scripts and Styling\nThere are many popular JavaScript and CSS libraries that can be used via a simple Script or Style tag. But in some cases you will need to write more custom code. FastHTML’s js.py contains a few examples that may be useful as reference.\nFor example, to use the marked.js library to render markdown in a div, including in components added after the page has loaded via htmx, we do something like this:\nimport { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\nproc_htmx('%s', e => e.innerHTML = marked.parse(e.textContent));\nproc_htmx is a shortcut that we wrote to apply a function to elements matching a selector, including the element that triggered the event. Here’s the code for reference:\nexport function proc_htmx(sel, func) {\n htmx.onLoad(elt => {\n const elements = htmx.findAll(elt, sel);\n if (elt.matches(sel)) elements.unshift(elt)\n elements.forEach(func);\n });\n}\nThe AI Pictionary example uses a larger chunk of custom JavaScript to handle the drawing canvas. It’s a good example of the type of application where running code on the client side makes the most sense, but still shows how you can integrate it with FastHTML on the server side to add functionality (like the AI responses) easily.\nAdding styling with custom CSS and libraries such as tailwind is done the same way we add custom JavaScript. The doodle example uses Doodle.CSS to style the page in a quirky way.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#deploying-your-app", + "href": "tutorials/by_example.html#deploying-your-app", + "title": "FastHTML By Example", + "section": "Deploying Your App", + "text": "Deploying Your App\nWe can deploy FastHTML almost anywhere you can deploy python apps. We’ve tested Railway, Replit, HuggingFace, and PythonAnywhere.\n\nRailway\n\nInstall the Railway CLI and sign up for an account.\nSet up a folder with our app as main.py\nIn the folder, run railway login.\nUse the fh_railway_deploy script to deploy our project:\n\nfh_railway_deploy MY_APP_NAME\nWhat the script does for us:\n\nDo we have an existing railway project?\n\nYes: Link the project folder to our existing Railway project.\nNo: Create a new Railway project.\n\nDeploy the project. We’ll see the logs as the service is built and run!\nFetches and displays the URL of our app.\nBy default, mounts a /app/data folder on the cloud to our app’s root folder. The app is run in /app by default, so from our app anything we store in /data will persist across restarts.\n\nA final note about Railway: We can add secrets like API keys that can be accessed as environment variables from our apps via ‘Variables’. For example, for the image generation app, we can add a REPLICATE_API_KEY variable, and then in main.py we can access it as os.environ['REPLICATE_API_KEY'].\n\n\nReplit\nFork this repl for a minimal example you can edit to your heart’s content. .replit has been edited to add the right run command (run = [\"uvicorn\", \"main:app\", \"--reload\"]) and to set up the ports correctly. FastHTML was installed with poetry add python-fasthtml, you can add additional packages as needed in the same way. Running the app in Replit will show you a webview, but you may need to open in a new tab for all features (such as cookies) to work. When you’re ready, you can deploy your app by clicking the ‘Deploy’ button. You pay for usage - for an app that is mostly idle the cost is usually a few cents per month.\nYou can store secrets like API keys via the ‘Secrets’ tab in the Replit project settings.\n\n\nHuggingFace\nFollow the instructions in this repository to deploy to HuggingFace spaces.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + }, + { + "objectID": "tutorials/by_example.html#where-next", + "href": "tutorials/by_example.html#where-next", + "title": "FastHTML By Example", + "section": "Where Next?", + "text": "Where Next?\nWe’ve covered a lot of ground here! Hopefully this has given you plenty to work with in building your own FastHTML apps. If you have any questions, feel free to ask in the #fasthtml Discord channel (in the fastai Discord community). You can look through the other examples in the fasthtml-example repository for more ideas, and keep an eye on Jeremy’s YouTube channel where we’ll be releasing a number of “dev chats” related to FastHTML in the near future.", + "crumbs": [ + "Home", + "Tutorials", + "FastHTML By Example" + ] + } +] \ No newline at end of file diff --git a/docs/site_libs/bootstrap/bootstrap-e463c5e664eae906a5c2eb38a07ecc3d.min.css b/docs/site_libs/bootstrap/bootstrap-e463c5e664eae906a5c2eb38a07ecc3d.min.css new file mode 100644 index 0000000000000000000000000000000000000000..edac779aa223315d419dd0928f995871fa91eb71 --- /dev/null +++ b/docs/site_libs/bootstrap/bootstrap-e463c5e664eae906a5c2eb38a07ecc3d.min.css @@ -0,0 +1,12 @@ +/*! + * Bootstrap v5.3.1 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */@import"https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap";:root,[data-bs-theme=light]{--bs-blue: #2780e3;--bs-indigo: #6610f2;--bs-purple: #613d7c;--bs-pink: #e83e8c;--bs-red: #ff0039;--bs-orange: #f0ad4e;--bs-yellow: #ff7518;--bs-green: #3fb618;--bs-teal: #20c997;--bs-cyan: #9954bb;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-default: #343a40;--bs-primary: #E8E8FC;--bs-secondary: #343a40;--bs-success: #3fb618;--bs-info: #9954bb;--bs-warning: #ff7518;--bs-danger: #ff0039;--bs-light: #f8f9fa;--bs-dark: #343a40;--bs-default-rgb: 52, 58, 64;--bs-primary-rgb: 232, 232, 252;--bs-secondary-rgb: 52, 58, 64;--bs-success-rgb: 63, 182, 24;--bs-info-rgb: 153, 84, 187;--bs-warning-rgb: 255, 117, 24;--bs-danger-rgb: 255, 0, 57;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 52, 58, 64;--bs-primary-text-emphasis: #5d5d65;--bs-secondary-text-emphasis: #15171a;--bs-success-text-emphasis: #19490a;--bs-info-text-emphasis: #3d224b;--bs-warning-text-emphasis: #662f0a;--bs-danger-text-emphasis: #660017;--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: #fafafe;--bs-secondary-bg-subtle: #d6d8d9;--bs-success-bg-subtle: #d9f0d1;--bs-info-bg-subtle: #ebddf1;--bs-warning-bg-subtle: #ffe3d1;--bs-danger-bg-subtle: #ffccd7;--bs-light-bg-subtle: #fcfcfd;--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: #f6f6fe;--bs-secondary-border-subtle: #aeb0b3;--bs-success-border-subtle: #b2e2a3;--bs-info-border-subtle: #d6bbe4;--bs-warning-border-subtle: #ffc8a3;--bs-danger-border-subtle: #ff99b0;--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-font-sans-serif: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-root-font-size: 18px;--bs-body-font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--bs-body-font-size:1rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #000000;--bs-body-color-rgb: 0, 0, 0;--bs-body-bg: #FFFFFF;--bs-body-bg-rgb: 255, 255, 255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0, 0, 0;--bs-secondary-color: rgba(0, 0, 0, 0.75);--bs-secondary-color-rgb: 0, 0, 0;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233, 236, 239;--bs-tertiary-color: rgba(0, 0, 0, 0.5);--bs-tertiary-color-rgb: 0, 0, 0;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248, 249, 250;--bs-heading-color: inherit;--bs-link-color: #4040BF;--bs-link-color-rgb: 64, 64, 191;--bs-link-decoration: underline;--bs-link-hover-color: #333399;--bs-link-hover-color-rgb: 51, 51, 153;--bs-code-color: #7d12ba;--bs-highlight-bg: #ffe3d1;--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0, 0, 0, 0.175);--bs-border-radius: 0.25rem;--bs-border-radius-sm: 0.2em;--bs-border-radius-lg: 0.5rem;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width: 0.25rem;--bs-focus-ring-opacity: 0.25;--bs-focus-ring-color: rgba(232, 232, 252, 0.25);--bs-form-valid-color: #3fb618;--bs-form-valid-border-color: #3fb618;--bs-form-invalid-color: #ff0039;--bs-form-invalid-border-color: #ff0039}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color: #dee2e6;--bs-body-color-rgb: 222, 226, 230;--bs-body-bg: #212529;--bs-body-bg-rgb: 33, 37, 41;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255, 255, 255;--bs-secondary-color: rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb: 222, 226, 230;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52, 58, 64;--bs-tertiary-color: rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb: 222, 226, 230;--bs-tertiary-bg: #2b3035;--bs-tertiary-bg-rgb: 43, 48, 53;--bs-primary-text-emphasis: #f1f1fd;--bs-secondary-text-emphasis: #85898c;--bs-success-text-emphasis: #8cd374;--bs-info-text-emphasis: #c298d6;--bs-warning-text-emphasis: #ffac74;--bs-danger-text-emphasis: #ff6688;--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: #2e2e32;--bs-secondary-bg-subtle: #0a0c0d;--bs-success-bg-subtle: #0d2405;--bs-info-bg-subtle: #1f1125;--bs-warning-bg-subtle: #331705;--bs-danger-bg-subtle: #33000b;--bs-light-bg-subtle: #343a40;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: #8b8b97;--bs-secondary-border-subtle: #1f2326;--bs-success-border-subtle: #266d0e;--bs-info-border-subtle: #5c3270;--bs-warning-border-subtle: #99460e;--bs-danger-border-subtle: #990022;--bs-light-border-subtle: #495057;--bs-dark-border-subtle: #343a40;--bs-heading-color: inherit;--bs-link-color: #f1f1fd;--bs-link-hover-color: #f4f4fd;--bs-link-color-rgb: 241, 241, 253;--bs-link-hover-color-rgb: 244, 244, 253;--bs-code-color: white;--bs-border-color: #495057;--bs-border-color-translucent: rgba(255, 255, 255, 0.15);--bs-form-valid-color: #8cd374;--bs-form-valid-border-color: #8cd374;--bs-form-invalid-color: #ff6688;--bs-form-invalid-border-color: #ff6688}*,*::before,*::after{box-sizing:border-box}:root{font-size:var(--bs-root-font-size)}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color)}h1,.h1{font-size:calc(1.325rem + 0.9vw)}@media(min-width: 1200px){h1,.h1{font-size:2rem}}h2,.h2{font-size:calc(1.29rem + 0.48vw)}@media(min-width: 1200px){h2,.h2{font-size:1.65rem}}h3,.h3{font-size:calc(1.27rem + 0.24vw)}@media(min-width: 1200px){h3,.h3{font-size:1.45rem}}h4,.h4{font-size:1.25rem}h5,.h5{font-size:1.1rem}h6,.h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{text-decoration:underline dotted;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;-ms-text-decoration:underline dotted;-o-text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem;padding:.625rem 1.25rem;border-left:.25rem solid #e9ecef}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}b,strong{font-weight:bolder}small,.small{font-size:0.875em}mark,.mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:0.75em;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}a{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}a:hover{--bs-link-color-rgb: var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:0.875em;color:#000;background-color:#f8f9fa;line-height:1.5;padding:.5rem;border:1px solid var(--bs-border-color, #dee2e6)}pre code{background-color:rgba(0,0,0,0);font-size:inherit;color:inherit;word-break:normal}code{font-size:0.875em;color:var(--bs-code-color);background-color:#f8f9fa;padding:.125rem .25rem;word-wrap:break-word}a>code{color:inherit}kbd{padding:.4rem .4rem;font-size:0.875em;color:#fff;background-color:#000}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:rgba(0,0,0,.75);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none !important}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + 0.3vw);line-height:inherit}@media(min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none !important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:0.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:0.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:0.875em;color:rgba(0,0,0,.75)}.container,.container-fluid,.container-xxl,.container-xl,.container-lg,.container-md,.container-sm{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x)*.5);padding-left:calc(var(--bs-gutter-x)*.5);margin-right:auto;margin-left:auto}@media(min-width: 576px){.container-sm,.container{max-width:540px}}@media(min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media(min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media(min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}@media(min-width: 1400px){.container-xxl,.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1320px}}:root{--bs-breakpoint-xs: 0;--bs-breakpoint-sm: 576px;--bs-breakpoint-md: 768px;--bs-breakpoint-lg: 992px;--bs-breakpoint-xl: 1200px;--bs-breakpoint-xxl: 1400px}.grid{display:grid;grid-template-rows:repeat(var(--bs-rows, 1), 1fr);grid-template-columns:repeat(var(--bs-columns, 12), 1fr);gap:var(--bs-gap, 1.5rem)}.grid .g-col-1{grid-column:auto/span 1}.grid .g-col-2{grid-column:auto/span 2}.grid .g-col-3{grid-column:auto/span 3}.grid .g-col-4{grid-column:auto/span 4}.grid .g-col-5{grid-column:auto/span 5}.grid .g-col-6{grid-column:auto/span 6}.grid .g-col-7{grid-column:auto/span 7}.grid .g-col-8{grid-column:auto/span 8}.grid .g-col-9{grid-column:auto/span 9}.grid .g-col-10{grid-column:auto/span 10}.grid .g-col-11{grid-column:auto/span 11}.grid .g-col-12{grid-column:auto/span 12}.grid .g-start-1{grid-column-start:1}.grid .g-start-2{grid-column-start:2}.grid .g-start-3{grid-column-start:3}.grid .g-start-4{grid-column-start:4}.grid .g-start-5{grid-column-start:5}.grid .g-start-6{grid-column-start:6}.grid .g-start-7{grid-column-start:7}.grid .g-start-8{grid-column-start:8}.grid .g-start-9{grid-column-start:9}.grid .g-start-10{grid-column-start:10}.grid .g-start-11{grid-column-start:11}@media(min-width: 576px){.grid .g-col-sm-1{grid-column:auto/span 1}.grid .g-col-sm-2{grid-column:auto/span 2}.grid .g-col-sm-3{grid-column:auto/span 3}.grid .g-col-sm-4{grid-column:auto/span 4}.grid .g-col-sm-5{grid-column:auto/span 5}.grid .g-col-sm-6{grid-column:auto/span 6}.grid .g-col-sm-7{grid-column:auto/span 7}.grid .g-col-sm-8{grid-column:auto/span 8}.grid .g-col-sm-9{grid-column:auto/span 9}.grid .g-col-sm-10{grid-column:auto/span 10}.grid .g-col-sm-11{grid-column:auto/span 11}.grid .g-col-sm-12{grid-column:auto/span 12}.grid .g-start-sm-1{grid-column-start:1}.grid .g-start-sm-2{grid-column-start:2}.grid .g-start-sm-3{grid-column-start:3}.grid .g-start-sm-4{grid-column-start:4}.grid .g-start-sm-5{grid-column-start:5}.grid .g-start-sm-6{grid-column-start:6}.grid .g-start-sm-7{grid-column-start:7}.grid .g-start-sm-8{grid-column-start:8}.grid .g-start-sm-9{grid-column-start:9}.grid .g-start-sm-10{grid-column-start:10}.grid .g-start-sm-11{grid-column-start:11}}@media(min-width: 768px){.grid .g-col-md-1{grid-column:auto/span 1}.grid .g-col-md-2{grid-column:auto/span 2}.grid .g-col-md-3{grid-column:auto/span 3}.grid .g-col-md-4{grid-column:auto/span 4}.grid .g-col-md-5{grid-column:auto/span 5}.grid .g-col-md-6{grid-column:auto/span 6}.grid .g-col-md-7{grid-column:auto/span 7}.grid .g-col-md-8{grid-column:auto/span 8}.grid .g-col-md-9{grid-column:auto/span 9}.grid .g-col-md-10{grid-column:auto/span 10}.grid .g-col-md-11{grid-column:auto/span 11}.grid .g-col-md-12{grid-column:auto/span 12}.grid .g-start-md-1{grid-column-start:1}.grid .g-start-md-2{grid-column-start:2}.grid .g-start-md-3{grid-column-start:3}.grid .g-start-md-4{grid-column-start:4}.grid .g-start-md-5{grid-column-start:5}.grid .g-start-md-6{grid-column-start:6}.grid .g-start-md-7{grid-column-start:7}.grid .g-start-md-8{grid-column-start:8}.grid .g-start-md-9{grid-column-start:9}.grid .g-start-md-10{grid-column-start:10}.grid .g-start-md-11{grid-column-start:11}}@media(min-width: 992px){.grid .g-col-lg-1{grid-column:auto/span 1}.grid .g-col-lg-2{grid-column:auto/span 2}.grid .g-col-lg-3{grid-column:auto/span 3}.grid .g-col-lg-4{grid-column:auto/span 4}.grid .g-col-lg-5{grid-column:auto/span 5}.grid .g-col-lg-6{grid-column:auto/span 6}.grid .g-col-lg-7{grid-column:auto/span 7}.grid .g-col-lg-8{grid-column:auto/span 8}.grid .g-col-lg-9{grid-column:auto/span 9}.grid .g-col-lg-10{grid-column:auto/span 10}.grid .g-col-lg-11{grid-column:auto/span 11}.grid .g-col-lg-12{grid-column:auto/span 12}.grid .g-start-lg-1{grid-column-start:1}.grid .g-start-lg-2{grid-column-start:2}.grid .g-start-lg-3{grid-column-start:3}.grid .g-start-lg-4{grid-column-start:4}.grid .g-start-lg-5{grid-column-start:5}.grid .g-start-lg-6{grid-column-start:6}.grid .g-start-lg-7{grid-column-start:7}.grid .g-start-lg-8{grid-column-start:8}.grid .g-start-lg-9{grid-column-start:9}.grid .g-start-lg-10{grid-column-start:10}.grid .g-start-lg-11{grid-column-start:11}}@media(min-width: 1200px){.grid .g-col-xl-1{grid-column:auto/span 1}.grid .g-col-xl-2{grid-column:auto/span 2}.grid .g-col-xl-3{grid-column:auto/span 3}.grid .g-col-xl-4{grid-column:auto/span 4}.grid .g-col-xl-5{grid-column:auto/span 5}.grid .g-col-xl-6{grid-column:auto/span 6}.grid .g-col-xl-7{grid-column:auto/span 7}.grid .g-col-xl-8{grid-column:auto/span 8}.grid .g-col-xl-9{grid-column:auto/span 9}.grid .g-col-xl-10{grid-column:auto/span 10}.grid .g-col-xl-11{grid-column:auto/span 11}.grid .g-col-xl-12{grid-column:auto/span 12}.grid .g-start-xl-1{grid-column-start:1}.grid .g-start-xl-2{grid-column-start:2}.grid .g-start-xl-3{grid-column-start:3}.grid .g-start-xl-4{grid-column-start:4}.grid .g-start-xl-5{grid-column-start:5}.grid .g-start-xl-6{grid-column-start:6}.grid .g-start-xl-7{grid-column-start:7}.grid .g-start-xl-8{grid-column-start:8}.grid .g-start-xl-9{grid-column-start:9}.grid .g-start-xl-10{grid-column-start:10}.grid .g-start-xl-11{grid-column-start:11}}@media(min-width: 1400px){.grid .g-col-xxl-1{grid-column:auto/span 1}.grid .g-col-xxl-2{grid-column:auto/span 2}.grid .g-col-xxl-3{grid-column:auto/span 3}.grid .g-col-xxl-4{grid-column:auto/span 4}.grid .g-col-xxl-5{grid-column:auto/span 5}.grid .g-col-xxl-6{grid-column:auto/span 6}.grid .g-col-xxl-7{grid-column:auto/span 7}.grid .g-col-xxl-8{grid-column:auto/span 8}.grid .g-col-xxl-9{grid-column:auto/span 9}.grid .g-col-xxl-10{grid-column:auto/span 10}.grid .g-col-xxl-11{grid-column:auto/span 11}.grid .g-col-xxl-12{grid-column:auto/span 12}.grid .g-start-xxl-1{grid-column-start:1}.grid .g-start-xxl-2{grid-column-start:2}.grid .g-start-xxl-3{grid-column-start:3}.grid .g-start-xxl-4{grid-column-start:4}.grid .g-start-xxl-5{grid-column-start:5}.grid .g-start-xxl-6{grid-column-start:6}.grid .g-start-xxl-7{grid-column-start:7}.grid .g-start-xxl-8{grid-column-start:8}.grid .g-start-xxl-9{grid-column-start:9}.grid .g-start-xxl-10{grid-column-start:10}.grid .g-start-xxl-11{grid-column-start:11}}.table{--bs-table-color-type: initial;--bs-table-bg-type: initial;--bs-table-color-state: initial;--bs-table-bg-state: initial;--bs-table-color: #000000;--bs-table-bg: #FFFFFF;--bs-table-border-color: #dee2e6;--bs-table-accent-bg: transparent;--bs-table-striped-color: #000000;--bs-table-striped-bg: rgba(0, 0, 0, 0.05);--bs-table-active-color: #000000;--bs-table-active-bg: rgba(0, 0, 0, 0.1);--bs-table-hover-color: #000000;--bs-table-hover-bg: rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(1px*2) solid gray}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-active{--bs-table-color-state: var(--bs-table-active-color);--bs-table-bg-state: var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state: var(--bs-table-hover-color);--bs-table-bg-state: var(--bs-table-hover-bg)}.table-primary{--bs-table-color: #000;--bs-table-bg: #fafafe;--bs-table-border-color: #e1e1e5;--bs-table-striped-bg: #eeeef1;--bs-table-striped-color: #000;--bs-table-active-bg: #e1e1e5;--bs-table-active-color: #000;--bs-table-hover-bg: #e7e7eb;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color: #000;--bs-table-bg: #d6d8d9;--bs-table-border-color: #c1c2c3;--bs-table-striped-bg: #cbcdce;--bs-table-striped-color: #000;--bs-table-active-bg: #c1c2c3;--bs-table-active-color: #000;--bs-table-hover-bg: #c6c8c9;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color: #000;--bs-table-bg: #d9f0d1;--bs-table-border-color: #c3d8bc;--bs-table-striped-bg: #cee4c7;--bs-table-striped-color: #000;--bs-table-active-bg: #c3d8bc;--bs-table-active-color: #000;--bs-table-hover-bg: #c9dec1;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color: #000;--bs-table-bg: #ebddf1;--bs-table-border-color: #d4c7d9;--bs-table-striped-bg: #dfd2e5;--bs-table-striped-color: #000;--bs-table-active-bg: #d4c7d9;--bs-table-active-color: #000;--bs-table-hover-bg: #d9ccdf;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color: #000;--bs-table-bg: #ffe3d1;--bs-table-border-color: #e6ccbc;--bs-table-striped-bg: #f2d8c7;--bs-table-striped-color: #000;--bs-table-active-bg: #e6ccbc;--bs-table-active-color: #000;--bs-table-hover-bg: #ecd2c1;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color: #000;--bs-table-bg: #ffccd7;--bs-table-border-color: #e6b8c2;--bs-table-striped-bg: #f2c2cc;--bs-table-striped-color: #000;--bs-table-active-bg: #e6b8c2;--bs-table-active-color: #000;--bs-table-hover-bg: #ecbdc7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color: #000;--bs-table-bg: #f8f9fa;--bs-table-border-color: #dfe0e1;--bs-table-striped-bg: #ecedee;--bs-table-striped-color: #000;--bs-table-active-bg: #dfe0e1;--bs-table-active-color: #000;--bs-table-hover-bg: #e5e6e7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color: #fff;--bs-table-bg: #343a40;--bs-table-border-color: #484e53;--bs-table-striped-bg: #3e444a;--bs-table-striped-color: #fff;--bs-table-active-bg: #484e53;--bs-table-active-color: #fff;--bs-table-hover-bg: #43494e;--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label,.shiny-input-container .control-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.875rem}.form-text{margin-top:.25rem;font-size:0.875em;color:rgba(0,0,0,.75)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#000;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#fff;background-clip:padding-box;border:1px solid #dee2e6;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#000;background-color:#fff;border-color:#f4f4fe;outline:0;box-shadow:0 0 0 .25rem rgba(232,232,252,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:rgba(0,0,0,.75);opacity:1}.form-control:disabled{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-0.375rem -0.75rem;margin-inline-end:.75rem;color:#000;background-color:#f8f9fa;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#e9ecef}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#000;background-color:rgba(0,0,0,0);border:solid rgba(0,0,0,0);border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + 0.5rem + calc(1px * 2));padding:.25rem .5rem;font-size:0.875rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2));padding:.5rem 1rem;font-size:1.25rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + 0.75rem + calc(1px * 2))}textarea.form-control-sm{min-height:calc(1.5em + 0.5rem + calc(1px * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2))}.form-control-color{width:3rem;height:calc(1.5em + 0.75rem + calc(1px * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0 !important}.form-control-color::-webkit-color-swatch{border:0 !important}.form-control-color.form-control-sm{height:calc(1.5em + 0.5rem + calc(1px * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(1px * 2))}.form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#000;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#fff;background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon, none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #dee2e6;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-select{transition:none}}.form-select:focus{border-color:#f4f4fe;outline:0;box-shadow:0 0 0 .25rem rgba(232,232,252,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:rgba(0,0,0,0);text-shadow:0 0 0 #000}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:0.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check,.shiny-input-container .checkbox,.shiny-input-container .radio{display:block;min-height:1.5rem;padding-left:0;margin-bottom:.125rem}.form-check .form-check-input,.form-check .shiny-input-container .checkbox input,.form-check .shiny-input-container .radio input,.shiny-input-container .checkbox .form-check-input,.shiny-input-container .checkbox .shiny-input-container .checkbox input,.shiny-input-container .checkbox .shiny-input-container .radio input,.shiny-input-container .radio .form-check-input,.shiny-input-container .radio .shiny-input-container .checkbox input,.shiny-input-container .radio .shiny-input-container .radio input{float:left;margin-left:0}.form-check-reverse{padding-right:0;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:0;margin-left:0}.form-check-input,.shiny-input-container .checkbox input,.shiny-input-container .checkbox-inline input,.shiny-input-container .radio input,.shiny-input-container .radio-inline input{--bs-form-check-bg: #FFFFFF;width:1em;height:1em;margin-top:.25em;vertical-align:top;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid #dee2e6;print-color-adjust:exact}.form-check-input[type=radio],.shiny-input-container .checkbox input[type=radio],.shiny-input-container .checkbox-inline input[type=radio],.shiny-input-container .radio input[type=radio],.shiny-input-container .radio-inline input[type=radio]{border-radius:50%}.form-check-input:active,.shiny-input-container .checkbox input:active,.shiny-input-container .checkbox-inline input:active,.shiny-input-container .radio input:active,.shiny-input-container .radio-inline input:active{filter:brightness(90%)}.form-check-input:focus,.shiny-input-container .checkbox input:focus,.shiny-input-container .checkbox-inline input:focus,.shiny-input-container .radio input:focus,.shiny-input-container .radio-inline input:focus{border-color:#f4f4fe;outline:0;box-shadow:0 0 0 .25rem rgba(232,232,252,.25)}.form-check-input:checked,.shiny-input-container .checkbox input:checked,.shiny-input-container .checkbox-inline input:checked,.shiny-input-container .radio input:checked,.shiny-input-container .radio-inline input:checked{background-color:#e8e8fc;border-color:#e8e8fc}.form-check-input:checked[type=checkbox],.shiny-input-container .checkbox input:checked[type=checkbox],.shiny-input-container .checkbox-inline input:checked[type=checkbox],.shiny-input-container .radio input:checked[type=checkbox],.shiny-input-container .radio-inline input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio],.shiny-input-container .checkbox input:checked[type=radio],.shiny-input-container .checkbox-inline input:checked[type=radio],.shiny-input-container .radio input:checked[type=radio],.shiny-input-container .radio-inline input:checked[type=radio]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23000'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate,.shiny-input-container .checkbox input[type=checkbox]:indeterminate,.shiny-input-container .checkbox-inline input[type=checkbox]:indeterminate,.shiny-input-container .radio input[type=checkbox]:indeterminate,.shiny-input-container .radio-inline input[type=checkbox]:indeterminate{background-color:#e8e8fc;border-color:#e8e8fc;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled,.shiny-input-container .checkbox input:disabled,.shiny-input-container .checkbox-inline input:disabled,.shiny-input-container .radio input:disabled,.shiny-input-container .radio-inline input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input[disabled]~span,.form-check-input:disabled~.form-check-label,.form-check-input:disabled~span,.shiny-input-container .checkbox input[disabled]~.form-check-label,.shiny-input-container .checkbox input[disabled]~span,.shiny-input-container .checkbox input:disabled~.form-check-label,.shiny-input-container .checkbox input:disabled~span,.shiny-input-container .checkbox-inline input[disabled]~.form-check-label,.shiny-input-container .checkbox-inline input[disabled]~span,.shiny-input-container .checkbox-inline input:disabled~.form-check-label,.shiny-input-container .checkbox-inline input:disabled~span,.shiny-input-container .radio input[disabled]~.form-check-label,.shiny-input-container .radio input[disabled]~span,.shiny-input-container .radio input:disabled~.form-check-label,.shiny-input-container .radio input:disabled~span,.shiny-input-container .radio-inline input[disabled]~.form-check-label,.shiny-input-container .radio-inline input[disabled]~span,.shiny-input-container .radio-inline input:disabled~.form-check-label,.shiny-input-container .radio-inline input:disabled~span{cursor:default;opacity:.5}.form-check-label,.shiny-input-container .checkbox label,.shiny-input-container .checkbox-inline label,.shiny-input-container .radio label,.shiny-input-container .radio-inline label{cursor:pointer}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;transition:background-position .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23f4f4fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23000'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:rgba(0,0,0,0)}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(232,232,252,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(232,232,252,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#e8e8fc;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#f8f8fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#f8f9fa;border-color:rgba(0,0,0,0)}.form-range::-moz-range-thumb{width:1rem;height:1rem;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#e8e8fc;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:#f8f8fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#f8f9fa;border-color:rgba(0,0,0,0)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:rgba(0,0,0,.75)}.form-range:disabled::-moz-range-thumb{background-color:rgba(0,0,0,.75)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(1px * 2));min-height:calc(3.5rem + calc(1px * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid rgba(0,0,0,0);transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media(prefers-reduced-motion: reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-control-plaintext~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem .375rem;z-index:-1;height:1.5em;content:"";background-color:#fff}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.form-floating>:disabled~label,.form-floating>.form-control:disabled~label{color:#6c757d}.form-floating>:disabled~label::after,.form-floating>.form-control:disabled~label::after{background-color:#e9ecef}.input-group{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:stretch;-webkit-align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select,.input-group>.form-floating{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#000;text-align:center;white-space:nowrap;background-color:#f8f9fa;border:1px solid #dee2e6}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:0.875rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(1px*-1)}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#3fb618}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:#3fb618}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#3fb618;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:#3fb618}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-control-color:valid,.form-control-color.is-valid{width:calc(3rem + calc(1.5em + 0.75rem))}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:#3fb618}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:#3fb618}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:#3fb618}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#ff0039}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:#ff0039}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#ff0039;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:#ff0039}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-control-color:invalid,.form-control-color.is-invalid{width:calc(3rem + calc(1.5em + 0.75rem))}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:#ff0039}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:#ff0039}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:#ff0039}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid{z-index:4}.btn{--bs-btn-padding-x: 0.75rem;--bs-btn-padding-y: 0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.5;--bs-btn-color: #000000;--bs-btn-bg: transparent;--bs-btn-border-width: 1px;--bs-btn-border-color: transparent;--bs-btn-border-radius: 0.25rem;--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity: 0.65;--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;vertical-align:middle;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-default{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #2c3136;--bs-btn-hover-border-color: #2a2e33;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #2a2e33;--bs-btn-active-border-color: #272c30;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-primary{--bs-btn-color: #000;--bs-btn-bg: #E8E8FC;--bs-btn-border-color: #E8E8FC;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #ebebfc;--bs-btn-hover-border-color: #eaeafc;--bs-btn-focus-shadow-rgb: 197, 197, 214;--bs-btn-active-color: #000;--bs-btn-active-bg: #ededfd;--bs-btn-active-border-color: #eaeafc;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #E8E8FC;--bs-btn-disabled-border-color: #E8E8FC}.btn-secondary{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #2c3136;--bs-btn-hover-border-color: #2a2e33;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #2a2e33;--bs-btn-active-border-color: #272c30;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-success{--bs-btn-color: #fff;--bs-btn-bg: #3fb618;--bs-btn-border-color: #3fb618;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #369b14;--bs-btn-hover-border-color: #329213;--bs-btn-focus-shadow-rgb: 92, 193, 59;--bs-btn-active-color: #fff;--bs-btn-active-bg: #329213;--bs-btn-active-border-color: #2f8912;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #3fb618;--bs-btn-disabled-border-color: #3fb618}.btn-info{--bs-btn-color: #fff;--bs-btn-bg: #9954bb;--bs-btn-border-color: #9954bb;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #82479f;--bs-btn-hover-border-color: #7a4396;--bs-btn-focus-shadow-rgb: 168, 110, 197;--bs-btn-active-color: #fff;--bs-btn-active-bg: #7a4396;--bs-btn-active-border-color: #733f8c;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #9954bb;--bs-btn-disabled-border-color: #9954bb}.btn-warning{--bs-btn-color: #fff;--bs-btn-bg: #ff7518;--bs-btn-border-color: #ff7518;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #d96314;--bs-btn-hover-border-color: #cc5e13;--bs-btn-focus-shadow-rgb: 255, 138, 59;--bs-btn-active-color: #fff;--bs-btn-active-bg: #cc5e13;--bs-btn-active-border-color: #bf5812;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #ff7518;--bs-btn-disabled-border-color: #ff7518}.btn-danger{--bs-btn-color: #fff;--bs-btn-bg: #ff0039;--bs-btn-border-color: #ff0039;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #d90030;--bs-btn-hover-border-color: #cc002e;--bs-btn-focus-shadow-rgb: 255, 38, 87;--bs-btn-active-color: #fff;--bs-btn-active-bg: #cc002e;--bs-btn-active-border-color: #bf002b;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #ff0039;--bs-btn-disabled-border-color: #ff0039}.btn-light{--bs-btn-color: #000;--bs-btn-bg: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #d3d4d5;--bs-btn-hover-border-color: #c6c7c8;--bs-btn-focus-shadow-rgb: 211, 212, 213;--bs-btn-active-color: #000;--bs-btn-active-bg: #c6c7c8;--bs-btn-active-border-color: #babbbc;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #f8f9fa;--bs-btn-disabled-border-color: #f8f9fa}.btn-dark{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #52585d;--bs-btn-hover-border-color: #484e53;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #5d6166;--bs-btn-active-border-color: #484e53;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-outline-default{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-primary{--bs-btn-color: #E8E8FC;--bs-btn-border-color: #E8E8FC;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #E8E8FC;--bs-btn-hover-border-color: #E8E8FC;--bs-btn-focus-shadow-rgb: 232, 232, 252;--bs-btn-active-color: #000;--bs-btn-active-bg: #E8E8FC;--bs-btn-active-border-color: #E8E8FC;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #E8E8FC;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #E8E8FC;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-secondary{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-success{--bs-btn-color: #3fb618;--bs-btn-border-color: #3fb618;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #3fb618;--bs-btn-hover-border-color: #3fb618;--bs-btn-focus-shadow-rgb: 63, 182, 24;--bs-btn-active-color: #fff;--bs-btn-active-bg: #3fb618;--bs-btn-active-border-color: #3fb618;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #3fb618;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #3fb618;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-info{--bs-btn-color: #9954bb;--bs-btn-border-color: #9954bb;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #9954bb;--bs-btn-hover-border-color: #9954bb;--bs-btn-focus-shadow-rgb: 153, 84, 187;--bs-btn-active-color: #fff;--bs-btn-active-bg: #9954bb;--bs-btn-active-border-color: #9954bb;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #9954bb;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #9954bb;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-warning{--bs-btn-color: #ff7518;--bs-btn-border-color: #ff7518;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #ff7518;--bs-btn-hover-border-color: #ff7518;--bs-btn-focus-shadow-rgb: 255, 117, 24;--bs-btn-active-color: #fff;--bs-btn-active-bg: #ff7518;--bs-btn-active-border-color: #ff7518;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ff7518;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ff7518;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-danger{--bs-btn-color: #ff0039;--bs-btn-border-color: #ff0039;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #ff0039;--bs-btn-hover-border-color: #ff0039;--bs-btn-focus-shadow-rgb: 255, 0, 57;--bs-btn-active-color: #fff;--bs-btn-active-bg: #ff0039;--bs-btn-active-border-color: #ff0039;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ff0039;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ff0039;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-light{--bs-btn-color: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f9fa;--bs-btn-hover-border-color: #f8f9fa;--bs-btn-focus-shadow-rgb: 248, 249, 250;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f9fa;--bs-btn-active-border-color: #f8f9fa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #f8f9fa;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f9fa;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-dark{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: #4040BF;--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: #333399;--bs-btn-hover-border-color: transparent;--bs-btn-active-color: #333399;--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 93, 93, 201;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y: 0.5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius: 0.5rem}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y: 0.25rem;--bs-btn-padding-x: 0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius: 0.2em}.fade{transition:opacity .15s linear}@media(prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .2s ease}@media(prefers-reduced-motion: reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media(prefers-reduced-motion: reduce){.collapsing.collapse-horizontal{transition:none}}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid rgba(0,0,0,0);border-bottom:0;border-left:.3em solid rgba(0,0,0,0)}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex: 1000;--bs-dropdown-min-width: 10rem;--bs-dropdown-padding-x: 0;--bs-dropdown-padding-y: 0.5rem;--bs-dropdown-spacer: 0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color: #000000;--bs-dropdown-bg: #FFFFFF;--bs-dropdown-border-color: rgba(0, 0, 0, 0.175);--bs-dropdown-border-radius: 0.25rem;--bs-dropdown-border-width: 1px;--bs-dropdown-inner-border-radius: calc(0.25rem - 1px);--bs-dropdown-divider-bg: rgba(0, 0, 0, 0.175);--bs-dropdown-divider-margin-y: 0.5rem;--bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color: #000000;--bs-dropdown-link-hover-color: #000000;--bs-dropdown-link-hover-bg: #f8f9fa;--bs-dropdown-link-active-color: #000;--bs-dropdown-link-active-bg: #E8E8FC;--bs-dropdown-link-disabled-color: rgba(0, 0, 0, 0.5);--bs-dropdown-item-padding-x: 1rem;--bs-dropdown-item-padding-y: 0.25rem;--bs-dropdown-header-color: #6c757d;--bs-dropdown-header-padding-x: 1rem;--bs-dropdown-header-padding-y: 0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1400px){.dropdown-menu-xxl-start{--bs-position: start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position: end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid rgba(0,0,0,0);border-bottom:.3em solid;border-left:.3em solid rgba(0,0,0,0)}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:0;border-bottom:.3em solid rgba(0,0,0,0);border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:.3em solid;border-bottom:.3em solid rgba(0,0,0,0)}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap;background-color:rgba(0,0,0,0);border:0}.dropdown-item:hover,.dropdown-item:focus{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:rgba(0,0,0,0)}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:0.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color: #dee2e6;--bs-dropdown-bg: #343a40;--bs-dropdown-border-color: rgba(0, 0, 0, 0.175);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color: #dee2e6;--bs-dropdown-link-hover-color: #fff;--bs-dropdown-divider-bg: rgba(0, 0, 0, 0.175);--bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color: #000;--bs-dropdown-link-active-bg: #E8E8FC;--bs-dropdown-link-disabled-color: #adb5bd;--bs-dropdown-header-color: #adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;justify-content:flex-start;-webkit-justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(1px*-1)}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;-webkit-flex-direction:column;align-items:flex-start;-webkit-align-items:flex-start;justify-content:center;-webkit-justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(1px*-1)}.nav{--bs-nav-link-padding-x: 1rem;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: #4040BF;--bs-nav-link-hover-color: #333399;--bs-nav-link-disabled-color: rgba(0, 0, 0, 0.75);display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background:none;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media(prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(232,232,252,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: 1px;--bs-nav-tabs-border-color: #dee2e6;--bs-nav-tabs-border-radius: 0.25rem;--bs-nav-tabs-link-hover-border-color: #e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color: #000;--bs-nav-tabs-link-active-bg: #FFFFFF;--bs-nav-tabs-link-active-border-color: #dee2e6 #dee2e6 #FFFFFF;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1*var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid rgba(0,0,0,0)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1*var(--bs-nav-tabs-border-width))}.nav-pills{--bs-nav-pills-border-radius: 0.25rem;--bs-nav-pills-link-active-color: #000;--bs-nav-pills-link-active-bg: #E8E8FC}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap: 1rem;--bs-nav-underline-border-width: 0.125rem;--bs-nav-underline-link-active-color: #000;gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid rgba(0,0,0,0)}.nav-underline .nav-link:hover,.nav-underline .nav-link:focus{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;-webkit-flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: 0.5rem;--bs-navbar-color: #FFFFFF;--bs-navbar-hover-color: rgba(0, 0, 0, 0.8);--bs-navbar-disabled-color: rgba(255, 255, 255, 0.75);--bs-navbar-active-color: #000000;--bs-navbar-brand-padding-y: 0.3125rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 1.25rem;--bs-navbar-brand-color: #FFFFFF;--bs-navbar-brand-hover-color: #000000;--bs-navbar-nav-link-padding-x: 0.5rem;--bs-navbar-toggler-padding-y: 0.25;--bs-navbar-toggler-padding-x: 0;--bs-navbar-toggler-font-size: 1.25rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23FFFFFF' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0);--bs-navbar-toggler-border-radius: 0.25rem;--bs-navbar-toggler-focus-width: 0.25rem;--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-sm,.navbar>.container-md,.navbar>.container-lg,.navbar>.container-xl,.navbar>.container-xxl{display:flex;display:-webkit-flex;flex-wrap:inherit;-webkit-flex-wrap:inherit;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:hover,.navbar-text a:focus{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;-webkit-flex-basis:100%;flex-grow:1;-webkit-flex-grow:1;align-items:center;-webkit-align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:rgba(0,0,0,0);border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);transition:var(--bs-navbar-toggler-transition)}@media(prefers-reduced-motion: reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media(min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color: #FFFFFF;--bs-navbar-hover-color: rgba(0, 0, 0, 0.8);--bs-navbar-disabled-color: rgba(255, 255, 255, 0.75);--bs-navbar-active-color: #000000;--bs-navbar-brand-color: #FFFFFF;--bs-navbar-brand-hover-color: #000000;--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23FFFFFF' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23FFFFFF' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: 0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width: 1px;--bs-card-border-color: rgba(0, 0, 0, 0.175);--bs-card-border-radius: 0.25rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius: calc(0.25rem - 1px);--bs-card-cap-padding-y: 0.5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: rgba(52, 58, 64, 0.25);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: #FFFFFF;--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: 0.75rem;position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0}.card>.list-group:last-child{border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;-webkit-flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-0.5*var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header-tabs{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-bottom:calc(-1*var(--bs-card-cap-padding-y));margin-left:calc(-0.5*var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-left:calc(-0.5*var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding)}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media(min-width: 576px){.card-group{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap}.card-group>.card{flex:1 0 0%;-webkit-flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}}.accordion{--bs-accordion-color: #000000;--bs-accordion-bg: #FFFFFF;--bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;--bs-accordion-border-color: #dee2e6;--bs-accordion-border-width: 1px;--bs-accordion-border-radius: 0.25rem;--bs-accordion-inner-border-radius: calc(0.25rem - 1px);--bs-accordion-btn-padding-x: 1.25rem;--bs-accordion-btn-padding-y: 1rem;--bs-accordion-btn-color: #000000;--bs-accordion-btn-bg: #FFFFFF;--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000000'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width: 1.25rem;--bs-accordion-btn-icon-transform: rotate(-180deg);--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%235d5d65'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color: #f4f4fe;--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(232, 232, 252, 0.25);--bs-accordion-body-padding-x: 1.25rem;--bs-accordion-body-padding-y: 1rem;--bs-accordion-active-color: #5d5d65;--bs-accordion-active-bg: #fafafe}.accordion-button{position:relative;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media(prefers-reduced-motion: reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1*var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;-webkit-flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media(prefers-reduced-motion: reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:not(:first-of-type){border-top:0}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f1f1fd'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f1f1fd'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x: 0;--bs-breadcrumb-padding-y: 0;--bs-breadcrumb-margin-bottom: 1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color: rgba(0, 0, 0, 0.75);--bs-breadcrumb-item-padding-x: 0.5rem;--bs-breadcrumb-item-active-color: rgba(0, 0, 0, 0.75);display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, ">") /* rtl: var(--bs-breadcrumb-divider, ">") */}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x: 0.75rem;--bs-pagination-padding-y: 0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color: #4040BF;--bs-pagination-bg: #FFFFFF;--bs-pagination-border-width: 1px;--bs-pagination-border-color: #dee2e6;--bs-pagination-border-radius: 0.25rem;--bs-pagination-hover-color: #333399;--bs-pagination-hover-bg: #f8f9fa;--bs-pagination-hover-border-color: #dee2e6;--bs-pagination-focus-color: #333399;--bs-pagination-focus-bg: #e9ecef;--bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(232, 232, 252, 0.25);--bs-pagination-active-color: #000;--bs-pagination-active-bg: #E8E8FC;--bs-pagination-active-border-color: #E8E8FC;--bs-pagination-disabled-color: rgba(0, 0, 0, 0.75);--bs-pagination-disabled-bg: #e9ecef;--bs-pagination-disabled-border-color: #dee2e6;display:flex;display:-webkit-flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,.active>.page-link{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(1px*-1)}.pagination-lg{--bs-pagination-padding-x: 1.5rem;--bs-pagination-padding-y: 0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius: 0.5rem}.pagination-sm{--bs-pagination-padding-x: 0.5rem;--bs-pagination-padding-y: 0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius: 0.2em}.badge{--bs-badge-padding-x: 0.65em;--bs-badge-padding-y: 0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight: 700;--bs-badge-color: #fff;--bs-badge-border-radius: 0.25rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg: transparent;--bs-alert-padding-x: 1rem;--bs-alert-padding-y: 1rem;--bs-alert-margin-bottom: 1rem;--bs-alert-color: inherit;--bs-alert-border-color: transparent;--bs-alert-border: 0 solid var(--bs-alert-border-color);--bs-alert-border-radius: 0.25rem;--bs-alert-link-color: inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-default{--bs-alert-color: var(--bs-default-text-emphasis);--bs-alert-bg: var(--bs-default-bg-subtle);--bs-alert-border-color: var(--bs-default-border-subtle);--bs-alert-link-color: var(--bs-default-text-emphasis)}.alert-primary{--bs-alert-color: var(--bs-primary-text-emphasis);--bs-alert-bg: var(--bs-primary-bg-subtle);--bs-alert-border-color: var(--bs-primary-border-subtle);--bs-alert-link-color: var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color: var(--bs-secondary-text-emphasis);--bs-alert-bg: var(--bs-secondary-bg-subtle);--bs-alert-border-color: var(--bs-secondary-border-subtle);--bs-alert-link-color: var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color: var(--bs-success-text-emphasis);--bs-alert-bg: var(--bs-success-bg-subtle);--bs-alert-border-color: var(--bs-success-border-subtle);--bs-alert-link-color: var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color: var(--bs-info-text-emphasis);--bs-alert-bg: var(--bs-info-bg-subtle);--bs-alert-border-color: var(--bs-info-border-subtle);--bs-alert-link-color: var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color: var(--bs-warning-text-emphasis);--bs-alert-bg: var(--bs-warning-bg-subtle);--bs-alert-border-color: var(--bs-warning-border-subtle);--bs-alert-link-color: var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color: var(--bs-danger-text-emphasis);--bs-alert-bg: var(--bs-danger-bg-subtle);--bs-alert-border-color: var(--bs-danger-border-subtle);--bs-alert-link-color: var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color: var(--bs-light-text-emphasis);--bs-alert-bg: var(--bs-light-bg-subtle);--bs-alert-border-color: var(--bs-light-border-subtle);--bs-alert-link-color: var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color: var(--bs-dark-text-emphasis);--bs-alert-bg: var(--bs-dark-bg-subtle);--bs-alert-border-color: var(--bs-dark-border-subtle);--bs-alert-link-color: var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:.5rem}}.progress,.progress-stacked{--bs-progress-height: 0.5rem;--bs-progress-font-size:0.75rem;--bs-progress-bg: #e9ecef;--bs-progress-border-radius: 0.25rem;--bs-progress-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-progress-bar-color: #fff;--bs-progress-bar-bg: #E8E8FC;--bs-progress-bar-transition: width 0.6s ease;display:flex;display:-webkit-flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg)}.progress-bar{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media(prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media(prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color: #000000;--bs-list-group-bg: #FFFFFF;--bs-list-group-border-color: #dee2e6;--bs-list-group-border-width: 1px;--bs-list-group-border-radius: 0.25rem;--bs-list-group-item-padding-x: 1rem;--bs-list-group-item-padding-y: 0.5rem;--bs-list-group-action-color: rgba(0, 0, 0, 0.75);--bs-list-group-action-hover-color: #000;--bs-list-group-action-hover-bg: #f8f9fa;--bs-list-group-action-active-color: #000000;--bs-list-group-action-active-bg: #e9ecef;--bs-list-group-disabled-color: rgba(0, 0, 0, 0.75);--bs-list-group-disabled-bg: #FFFFFF;--bs-list-group-active-color: #000;--bs-list-group-active-bg: #E8E8FC;--bs-list-group-active-border-color: #E8E8FC;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1*var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media(min-width: 576px){.list-group-horizontal-sm{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 768px){.list-group-horizontal-md{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 992px){.list-group-horizontal-lg{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1200px){.list-group-horizontal-xl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1400px){.list-group-horizontal-xxl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-default{--bs-list-group-color: var(--bs-default-text-emphasis);--bs-list-group-bg: var(--bs-default-bg-subtle);--bs-list-group-border-color: var(--bs-default-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-default-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-default-border-subtle);--bs-list-group-active-color: var(--bs-default-bg-subtle);--bs-list-group-active-bg: var(--bs-default-text-emphasis);--bs-list-group-active-border-color: var(--bs-default-text-emphasis)}.list-group-item-primary{--bs-list-group-color: var(--bs-primary-text-emphasis);--bs-list-group-bg: var(--bs-primary-bg-subtle);--bs-list-group-border-color: var(--bs-primary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-primary-border-subtle);--bs-list-group-active-color: var(--bs-primary-bg-subtle);--bs-list-group-active-bg: var(--bs-primary-text-emphasis);--bs-list-group-active-border-color: var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color: var(--bs-secondary-text-emphasis);--bs-list-group-bg: var(--bs-secondary-bg-subtle);--bs-list-group-border-color: var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);--bs-list-group-active-color: var(--bs-secondary-bg-subtle);--bs-list-group-active-bg: var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color: var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color: var(--bs-success-text-emphasis);--bs-list-group-bg: var(--bs-success-bg-subtle);--bs-list-group-border-color: var(--bs-success-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-success-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-success-border-subtle);--bs-list-group-active-color: var(--bs-success-bg-subtle);--bs-list-group-active-bg: var(--bs-success-text-emphasis);--bs-list-group-active-border-color: var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color: var(--bs-info-text-emphasis);--bs-list-group-bg: var(--bs-info-bg-subtle);--bs-list-group-border-color: var(--bs-info-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-info-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-info-border-subtle);--bs-list-group-active-color: var(--bs-info-bg-subtle);--bs-list-group-active-bg: var(--bs-info-text-emphasis);--bs-list-group-active-border-color: var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color: var(--bs-warning-text-emphasis);--bs-list-group-bg: var(--bs-warning-bg-subtle);--bs-list-group-border-color: var(--bs-warning-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-warning-border-subtle);--bs-list-group-active-color: var(--bs-warning-bg-subtle);--bs-list-group-active-bg: var(--bs-warning-text-emphasis);--bs-list-group-active-border-color: var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color: var(--bs-danger-text-emphasis);--bs-list-group-bg: var(--bs-danger-bg-subtle);--bs-list-group-border-color: var(--bs-danger-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-danger-border-subtle);--bs-list-group-active-color: var(--bs-danger-bg-subtle);--bs-list-group-active-bg: var(--bs-danger-text-emphasis);--bs-list-group-active-border-color: var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color: var(--bs-light-text-emphasis);--bs-list-group-bg: var(--bs-light-bg-subtle);--bs-list-group-border-color: var(--bs-light-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-light-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-light-border-subtle);--bs-list-group-active-color: var(--bs-light-bg-subtle);--bs-list-group-active-bg: var(--bs-light-text-emphasis);--bs-list-group-active-border-color: var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color: var(--bs-dark-text-emphasis);--bs-list-group-bg: var(--bs-dark-bg-subtle);--bs-list-group-border-color: var(--bs-dark-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-dark-border-subtle);--bs-list-group-active-color: var(--bs-dark-bg-subtle);--bs-list-group-active-bg: var(--bs-dark-text-emphasis);--bs-list-group-active-border-color: var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color: #000;--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity: 0.5;--bs-btn-close-hover-opacity: 0.75;--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(232, 232, 252, 0.25);--bs-btn-close-focus-opacity: 1;--bs-btn-close-disabled-opacity: 0.25;--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:rgba(0,0,0,0) var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close:disabled,.btn-close.disabled{pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex: 1090;--bs-toast-padding-x: 0.75rem;--bs-toast-padding-y: 0.5rem;--bs-toast-spacing: 1.5rem;--bs-toast-max-width: 350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg: rgba(255, 255, 255, 0.85);--bs-toast-border-width: 1px;--bs-toast-border-color: rgba(0, 0, 0, 0.175);--bs-toast-border-radius: 0.25rem;--bs-toast-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-toast-header-color: rgba(0, 0, 0, 0.75);--bs-toast-header-bg: rgba(255, 255, 255, 0.85);--bs-toast-header-border-color: rgba(0, 0, 0, 0.175);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex: 1090;position:absolute;z-index:var(--bs-toast-zindex);width:max-content;width:-webkit-max-content;width:-moz-max-content;width:-ms-max-content;width:-o-max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color)}.toast-header .btn-close{margin-right:calc(-0.5*var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: 0.5rem;--bs-modal-color: ;--bs-modal-bg: #FFFFFF;--bs-modal-border-color: rgba(0, 0, 0, 0.175);--bs-modal-border-width: 1px;--bs-modal-border-radius: 0.5rem;--bs-modal-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius: calc(0.5rem - 1px);--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: #dee2e6;--bs-modal-header-border-width: 1px;--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: 0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: #dee2e6;--bs-modal-footer-border-width: 1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0, -50px)}@media(prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin)*2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;min-height:calc(100% - var(--bs-modal-margin)*2)}.modal-content{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: 0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;display:-webkit-flex;flex-shrink:0;-webkit-flex-shrink:0;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y)*.5) calc(var(--bs-modal-header-padding-x)*.5);margin:calc(-0.5*var(--bs-modal-header-padding-y)) calc(-0.5*var(--bs-modal-header-padding-x)) calc(-0.5*var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;display:-webkit-flex;flex-shrink:0;-webkit-flex-shrink:0;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:flex-end;-webkit-justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap)*.5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap)*.5)}@media(min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width: 300px}}@media(min-width: 992px){.modal-lg,.modal-xl{--bs-modal-width: 800px}}@media(min-width: 1200px){.modal-xl{--bs-modal-width: 1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width: 1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex: 1080;--bs-tooltip-max-width: 200px;--bs-tooltip-padding-x: 0.5rem;--bs-tooltip-padding-y: 0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color: #FFFFFF;--bs-tooltip-bg: #000;--bs-tooltip-border-radius: 0.25rem;--bs-tooltip-opacity: 0.9;--bs-tooltip-arrow-width: 0.8rem;--bs-tooltip-arrow-height: 0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-top .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-bottom .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) 0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg)}.popover{--bs-popover-zindex: 1070;--bs-popover-max-width: 276px;--bs-popover-font-size:0.875rem;--bs-popover-bg: #FFFFFF;--bs-popover-border-width: 1px;--bs-popover-border-color: rgba(0, 0, 0, 0.175);--bs-popover-border-radius: 0.5rem;--bs-popover-inner-border-radius: calc(0.5rem - 1px);--bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x: 1rem;--bs-popover-header-padding-y: 0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color: inherit;--bs-popover-header-bg: #e9ecef;--bs-popover-body-padding-x: 1rem;--bs-popover-body-padding-y: 1rem;--bs-popover-body-color: #000000;--bs-popover-arrow-width: 1rem;--bs-popover-arrow-height: 0.5rem;--bs-popover-arrow-border: var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::before,.popover .popover-arrow::after{position:absolute;display:block;content:"";border-color:rgba(0,0,0,0);border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{border-width:0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-bottom .popover-header::before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-0.5*var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) 0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y;-webkit-touch-action:pan-y;-moz-touch-action:pan-y;-ms-touch-action:pan-y;-o-touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;transition:transform .6s ease-in-out}@media(prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;display:-webkit-flex;justify-content:center;-webkit-justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;-webkit-flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid rgba(0,0,0,0);border-bottom:10px solid rgba(0,0,0,0);opacity:.5;transition:opacity .6s ease}@media(prefers-reduced-motion: reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-grow,.spinner-border{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}.spinner-border{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-border-width: 0.25em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:rgba(0,0,0,0)}.spinner-border-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem;--bs-spinner-border-width: 0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem}@media(prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed: 1.5s}}.offcanvas,.offcanvas-xxl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 400px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: #000000;--bs-offcanvas-bg: #FFFFFF;--bs-offcanvas-border-width: 1px;--bs-offcanvas-border-color: rgba(0, 0, 0, 0.175);--bs-offcanvas-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-offcanvas-transition: transform 0.3s ease-in-out;--bs-offcanvas-title-line-height: 1.5}@media(max-width: 575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 575.98px)and (prefers-reduced-motion: reduce){.offcanvas-sm{transition:none}}@media(max-width: 575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.showing,.offcanvas-sm.show:not(.hiding){transform:none}.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{visibility:visible}}@media(min-width: 576px){.offcanvas-sm{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 767.98px)and (prefers-reduced-motion: reduce){.offcanvas-md{transition:none}}@media(max-width: 767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.showing,.offcanvas-md.show:not(.hiding){transform:none}.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{visibility:visible}}@media(min-width: 768px){.offcanvas-md{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 991.98px)and (prefers-reduced-motion: reduce){.offcanvas-lg{transition:none}}@media(max-width: 991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.showing,.offcanvas-lg.show:not(.hiding){transform:none}.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{visibility:visible}}@media(min-width: 992px){.offcanvas-lg{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 1199.98px)and (prefers-reduced-motion: reduce){.offcanvas-xl{transition:none}}@media(max-width: 1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.showing,.offcanvas-xl.show:not(.hiding){transform:none}.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{visibility:visible}}@media(min-width: 1200px){.offcanvas-xl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 1399.98px)and (prefers-reduced-motion: reduce){.offcanvas-xxl{transition:none}}@media(max-width: 1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.showing,.offcanvas-xxl.show:not(.hiding){transform:none}.offcanvas-xxl.showing,.offcanvas-xxl.hiding,.offcanvas-xxl.show{visibility:visible}}@media(min-width: 1400px){.offcanvas-xxl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media(prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y)*.5) calc(var(--bs-offcanvas-padding-x)*.5);margin-top:calc(-0.5*var(--bs-offcanvas-padding-y));margin-right:calc(-0.5*var(--bs-offcanvas-padding-x));margin-bottom:calc(-0.5*var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;-webkit-flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);-webkit-mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);mask-size:200% 100%;-webkit-mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{mask-position:-200% 0%;-webkit-mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-default{color:#fff !important;background-color:RGBA(var(--bs-default-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-primary{color:#000 !important;background-color:RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-secondary{color:#fff !important;background-color:RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-success{color:#fff !important;background-color:RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-info{color:#fff !important;background-color:RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-warning{color:#fff !important;background-color:RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-danger{color:#fff !important;background-color:RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-light{color:#000 !important;background-color:RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-dark{color:#fff !important;background-color:RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important}.link-default{color:RGBA(var(--bs-default-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-default-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-default:hover,.link-default:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-primary{color:RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-primary:hover,.link-primary:focus{color:RGBA(237, 237, 253, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(237, 237, 253, var(--bs-link-underline-opacity, 1)) !important}.link-secondary{color:RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-secondary:hover,.link-secondary:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-success{color:RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-success:hover,.link-success:focus{color:RGBA(50, 146, 19, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(50, 146, 19, var(--bs-link-underline-opacity, 1)) !important}.link-info{color:RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-info:hover,.link-info:focus{color:RGBA(122, 67, 150, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(122, 67, 150, var(--bs-link-underline-opacity, 1)) !important}.link-warning{color:RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-warning:hover,.link-warning:focus{color:RGBA(204, 94, 19, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(204, 94, 19, var(--bs-link-underline-opacity, 1)) !important}.link-danger{color:RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-danger:hover,.link-danger:focus{color:RGBA(204, 0, 46, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(204, 0, 46, var(--bs-link-underline-opacity, 1)) !important}.link-light{color:RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-light:hover,.link-light:focus{color:RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important}.link-dark{color:RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-dark:hover,.link-dark:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis:hover,.link-body-emphasis:focus{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));text-underline-offset:.25em;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;-webkit-flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media(prefers-reduced-motion: reduce){.icon-link>.bi{transition:none}}.icon-link-hover:hover>.bi,.icon-link-hover:focus-visible>.bi{transform:var(--bs-icon-link-transform, translate3d(0.25em, 0, 0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media(min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;display:-webkit-flex;flex-direction:row;-webkit-flex-direction:row;align-items:center;-webkit-align-items:center;align-self:stretch;-webkit-align-self:stretch}.vstack{display:flex;display:-webkit-flex;flex:1 1 auto;-webkit-flex:1 1 auto;flex-direction:column;-webkit-flex-direction:column;align-self:stretch;-webkit-align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.visually-hidden:not(caption),.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption){position:absolute !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;-webkit-align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.object-fit-contain{object-fit:contain !important}.object-fit-cover{object-fit:cover !important}.object-fit-fill{object-fit:fill !important}.object-fit-scale{object-fit:scale-down !important}.object-fit-none{object-fit:none !important}.opacity-0{opacity:0 !important}.opacity-25{opacity:.25 !important}.opacity-50{opacity:.5 !important}.opacity-75{opacity:.75 !important}.opacity-100{opacity:1 !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.overflow-visible{overflow:visible !important}.overflow-scroll{overflow:scroll !important}.overflow-x-auto{overflow-x:auto !important}.overflow-x-hidden{overflow-x:hidden !important}.overflow-x-visible{overflow-x:visible !important}.overflow-x-scroll{overflow-x:scroll !important}.overflow-y-auto{overflow-y:auto !important}.overflow-y-hidden{overflow-y:hidden !important}.overflow-y-visible{overflow-y:visible !important}.overflow-y-scroll{overflow-y:scroll !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-grid{display:grid !important}.d-inline-grid{display:inline-grid !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}.d-none{display:none !important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}.shadow-none{box-shadow:none !important}.focus-ring-default{--bs-focus-ring-color: rgba(var(--bs-default-rgb), var(--bs-focus-ring-opacity))}.focus-ring-primary{--bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.top-0{top:0 !important}.top-50{top:50% !important}.top-100{top:100% !important}.bottom-0{bottom:0 !important}.bottom-50{bottom:50% !important}.bottom-100{bottom:100% !important}.start-0{left:0 !important}.start-50{left:50% !important}.start-100{left:100% !important}.end-0{right:0 !important}.end-50{right:50% !important}.end-100{right:100% !important}.translate-middle{transform:translate(-50%, -50%) !important}.translate-middle-x{transform:translateX(-50%) !important}.translate-middle-y{transform:translateY(-50%) !important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-0{border:0 !important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-top-0{border-top:0 !important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-end-0{border-right:0 !important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-bottom-0{border-bottom:0 !important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-start-0{border-left:0 !important}.border-default{--bs-border-opacity: 1;border-color:rgba(var(--bs-default-rgb), var(--bs-border-opacity)) !important}.border-primary{--bs-border-opacity: 1;border-color:rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important}.border-secondary{--bs-border-opacity: 1;border-color:rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important}.border-success{--bs-border-opacity: 1;border-color:rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important}.border-info{--bs-border-opacity: 1;border-color:rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important}.border-warning{--bs-border-opacity: 1;border-color:rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important}.border-danger{--bs-border-opacity: 1;border-color:rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important}.border-light{--bs-border-opacity: 1;border-color:rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important}.border-dark{--bs-border-opacity: 1;border-color:rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important}.border-black{--bs-border-opacity: 1;border-color:rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important}.border-white{--bs-border-opacity: 1;border-color:rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle) !important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle) !important}.border-success-subtle{border-color:var(--bs-success-border-subtle) !important}.border-info-subtle{border-color:var(--bs-info-border-subtle) !important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle) !important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle) !important}.border-light-subtle{border-color:var(--bs-light-border-subtle) !important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle) !important}.border-1{border-width:1px !important}.border-2{border-width:2px !important}.border-3{border-width:3px !important}.border-4{border-width:4px !important}.border-5{border-width:5px !important}.border-opacity-10{--bs-border-opacity: 0.1}.border-opacity-25{--bs-border-opacity: 0.25}.border-opacity-50{--bs-border-opacity: 0.5}.border-opacity-75{--bs-border-opacity: 0.75}.border-opacity-100{--bs-border-opacity: 1}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.mw-100{max-width:100% !important}.vw-100{width:100vw !important}.min-vw-100{min-width:100vw !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mh-100{max-height:100% !important}.vh-100{height:100vh !important}.min-vh-100{min-height:100vh !important}.flex-fill{flex:1 1 auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.justify-content-evenly{justify-content:space-evenly !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}.order-first{order:-1 !important}.order-0{order:0 !important}.order-1{order:1 !important}.order-2{order:2 !important}.order-3{order:3 !important}.order-4{order:4 !important}.order-5{order:5 !important}.order-last{order:6 !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.m-auto{margin:auto !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-3{margin-right:1rem !important;margin-left:1rem !important}.mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-5{margin-right:3rem !important;margin-left:3rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mt-auto{margin-top:auto !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.mb-auto{margin-bottom:auto !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.ms-auto{margin-left:auto !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.px-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-3{padding-right:1rem !important;padding-left:1rem !important}.px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-5{padding-right:3rem !important;padding-left:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.row-gap-0{row-gap:0 !important}.row-gap-1{row-gap:.25rem !important}.row-gap-2{row-gap:.5rem !important}.row-gap-3{row-gap:1rem !important}.row-gap-4{row-gap:1.5rem !important}.row-gap-5{row-gap:3rem !important}.column-gap-0{column-gap:0 !important}.column-gap-1{column-gap:.25rem !important}.column-gap-2{column-gap:.5rem !important}.column-gap-3{column-gap:1rem !important}.column-gap-4{column-gap:1.5rem !important}.column-gap-5{column-gap:3rem !important}.font-monospace{font-family:var(--bs-font-monospace) !important}.fs-1{font-size:calc(1.325rem + 0.9vw) !important}.fs-2{font-size:calc(1.29rem + 0.48vw) !important}.fs-3{font-size:calc(1.27rem + 0.24vw) !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1.1rem !important}.fs-6{font-size:1rem !important}.fst-italic{font-style:italic !important}.fst-normal{font-style:normal !important}.fw-lighter{font-weight:lighter !important}.fw-light{font-weight:300 !important}.fw-normal{font-weight:400 !important}.fw-medium{font-weight:500 !important}.fw-semibold{font-weight:600 !important}.fw-bold{font-weight:700 !important}.fw-bolder{font-weight:bolder !important}.lh-1{line-height:1 !important}.lh-sm{line-height:1.25 !important}.lh-base{line-height:1.5 !important}.lh-lg{line-height:2 !important}.text-start{text-align:left !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-decoration-underline{text-decoration:underline !important}.text-decoration-line-through{text-decoration:line-through !important}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-break{word-wrap:break-word !important;word-break:break-word !important}.text-default{--bs-text-opacity: 1;color:rgba(var(--bs-default-rgb), var(--bs-text-opacity)) !important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important}.text-dark{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important}.text-muted{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-black-50{--bs-text-opacity: 1;color:rgba(0,0,0,.5) !important}.text-white-50{--bs-text-opacity: 1;color:rgba(255,255,255,.5) !important}.text-body-secondary{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-body-tertiary{--bs-text-opacity: 1;color:var(--bs-tertiary-color) !important}.text-body-emphasis{--bs-text-opacity: 1;color:var(--bs-emphasis-color) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.text-opacity-25{--bs-text-opacity: 0.25}.text-opacity-50{--bs-text-opacity: 0.5}.text-opacity-75{--bs-text-opacity: 0.75}.text-opacity-100{--bs-text-opacity: 1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis) !important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis) !important}.text-success-emphasis{color:var(--bs-success-text-emphasis) !important}.text-info-emphasis{color:var(--bs-info-text-emphasis) !important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis) !important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis) !important}.text-light-emphasis{color:var(--bs-light-text-emphasis) !important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis) !important}.link-opacity-10{--bs-link-opacity: 0.1}.link-opacity-10-hover:hover{--bs-link-opacity: 0.1}.link-opacity-25{--bs-link-opacity: 0.25}.link-opacity-25-hover:hover{--bs-link-opacity: 0.25}.link-opacity-50{--bs-link-opacity: 0.5}.link-opacity-50-hover:hover{--bs-link-opacity: 0.5}.link-opacity-75{--bs-link-opacity: 0.75}.link-opacity-75-hover:hover{--bs-link-opacity: 0.75}.link-opacity-100{--bs-link-opacity: 1}.link-opacity-100-hover:hover{--bs-link-opacity: 1}.link-offset-1{text-underline-offset:.125em !important}.link-offset-1-hover:hover{text-underline-offset:.125em !important}.link-offset-2{text-underline-offset:.25em !important}.link-offset-2-hover:hover{text-underline-offset:.25em !important}.link-offset-3{text-underline-offset:.375em !important}.link-offset-3-hover:hover{text-underline-offset:.375em !important}.link-underline-default{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-default-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-primary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-secondary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-success{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-info{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-warning{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-danger{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-light{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-dark{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important}.link-underline{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-underline-opacity-0{--bs-link-underline-opacity: 0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity: 0}.link-underline-opacity-10{--bs-link-underline-opacity: 0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity: 0.1}.link-underline-opacity-25{--bs-link-underline-opacity: 0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity: 0.25}.link-underline-opacity-50{--bs-link-underline-opacity: 0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity: 0.5}.link-underline-opacity-75{--bs-link-underline-opacity: 0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity: 0.75}.link-underline-opacity-100{--bs-link-underline-opacity: 1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity: 1}.bg-default{--bs-bg-opacity: 1;background-color:rgba(var(--bs-default-rgb), var(--bs-bg-opacity)) !important}.bg-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important}.bg-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important}.bg-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important}.bg-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important}.bg-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important}.bg-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important}.bg-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important}.bg-transparent{--bs-bg-opacity: 1;background-color:rgba(0,0,0,0) !important}.bg-body-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-body-tertiary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-opacity-10{--bs-bg-opacity: 0.1}.bg-opacity-25{--bs-bg-opacity: 0.25}.bg-opacity-50{--bs-bg-opacity: 0.5}.bg-opacity-75{--bs-bg-opacity: 0.75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle) !important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle) !important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle) !important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle) !important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle) !important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle) !important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle) !important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle) !important}.bg-gradient{background-image:var(--bs-gradient) !important}.user-select-all{user-select:all !important}.user-select-auto{user-select:auto !important}.user-select-none{user-select:none !important}.pe-none{pointer-events:none !important}.pe-auto{pointer-events:auto !important}.rounded{border-radius:var(--bs-border-radius) !important}.rounded-0{border-radius:0 !important}.rounded-1{border-radius:var(--bs-border-radius-sm) !important}.rounded-2{border-radius:var(--bs-border-radius) !important}.rounded-3{border-radius:var(--bs-border-radius-lg) !important}.rounded-4{border-radius:var(--bs-border-radius-xl) !important}.rounded-5{border-radius:var(--bs-border-radius-xxl) !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:var(--bs-border-radius-pill) !important}.rounded-top{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm) !important;border-top-right-radius:var(--bs-border-radius-sm) !important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg) !important;border-top-right-radius:var(--bs-border-radius-lg) !important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl) !important;border-top-right-radius:var(--bs-border-radius-xl) !important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl) !important;border-top-right-radius:var(--bs-border-radius-xxl) !important}.rounded-top-circle{border-top-left-radius:50% !important;border-top-right-radius:50% !important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill) !important;border-top-right-radius:var(--bs-border-radius-pill) !important}.rounded-end{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm) !important;border-bottom-right-radius:var(--bs-border-radius-sm) !important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg) !important;border-bottom-right-radius:var(--bs-border-radius-lg) !important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl) !important;border-bottom-right-radius:var(--bs-border-radius-xl) !important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-right-radius:var(--bs-border-radius-xxl) !important}.rounded-end-circle{border-top-right-radius:50% !important;border-bottom-right-radius:50% !important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill) !important;border-bottom-right-radius:var(--bs-border-radius-pill) !important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm) !important;border-bottom-left-radius:var(--bs-border-radius-sm) !important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg) !important;border-bottom-left-radius:var(--bs-border-radius-lg) !important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl) !important;border-bottom-left-radius:var(--bs-border-radius-xl) !important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-left-radius:var(--bs-border-radius-xxl) !important}.rounded-bottom-circle{border-bottom-right-radius:50% !important;border-bottom-left-radius:50% !important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill) !important;border-bottom-left-radius:var(--bs-border-radius-pill) !important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm) !important;border-top-left-radius:var(--bs-border-radius-sm) !important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg) !important;border-top-left-radius:var(--bs-border-radius-lg) !important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl) !important;border-top-left-radius:var(--bs-border-radius-xl) !important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl) !important;border-top-left-radius:var(--bs-border-radius-xxl) !important}.rounded-start-circle{border-bottom-left-radius:50% !important;border-top-left-radius:50% !important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill) !important;border-top-left-radius:var(--bs-border-radius-pill) !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.z-n1{z-index:-1 !important}.z-0{z-index:0 !important}.z-1{z-index:1 !important}.z-2{z-index:2 !important}.z-3{z-index:3 !important}@media(min-width: 576px){.float-sm-start{float:left !important}.float-sm-end{float:right !important}.float-sm-none{float:none !important}.object-fit-sm-contain{object-fit:contain !important}.object-fit-sm-cover{object-fit:cover !important}.object-fit-sm-fill{object-fit:fill !important}.object-fit-sm-scale{object-fit:scale-down !important}.object-fit-sm-none{object-fit:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-grid{display:grid !important}.d-sm-inline-grid{display:inline-grid !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}.d-sm-none{display:none !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.justify-content-sm-evenly{justify-content:space-evenly !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}.order-sm-first{order:-1 !important}.order-sm-0{order:0 !important}.order-sm-1{order:1 !important}.order-sm-2{order:2 !important}.order-sm-3{order:3 !important}.order-sm-4{order:4 !important}.order-sm-5{order:5 !important}.order-sm-last{order:6 !important}.m-sm-0{margin:0 !important}.m-sm-1{margin:.25rem !important}.m-sm-2{margin:.5rem !important}.m-sm-3{margin:1rem !important}.m-sm-4{margin:1.5rem !important}.m-sm-5{margin:3rem !important}.m-sm-auto{margin:auto !important}.mx-sm-0{margin-right:0 !important;margin-left:0 !important}.mx-sm-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-sm-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-sm-3{margin-right:1rem !important;margin-left:1rem !important}.mx-sm-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-sm-5{margin-right:3rem !important;margin-left:3rem !important}.mx-sm-auto{margin-right:auto !important;margin-left:auto !important}.my-sm-0{margin-top:0 !important;margin-bottom:0 !important}.my-sm-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-sm-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-sm-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-sm-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-sm-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-sm-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-sm-0{margin-top:0 !important}.mt-sm-1{margin-top:.25rem !important}.mt-sm-2{margin-top:.5rem !important}.mt-sm-3{margin-top:1rem !important}.mt-sm-4{margin-top:1.5rem !important}.mt-sm-5{margin-top:3rem !important}.mt-sm-auto{margin-top:auto !important}.me-sm-0{margin-right:0 !important}.me-sm-1{margin-right:.25rem !important}.me-sm-2{margin-right:.5rem !important}.me-sm-3{margin-right:1rem !important}.me-sm-4{margin-right:1.5rem !important}.me-sm-5{margin-right:3rem !important}.me-sm-auto{margin-right:auto !important}.mb-sm-0{margin-bottom:0 !important}.mb-sm-1{margin-bottom:.25rem !important}.mb-sm-2{margin-bottom:.5rem !important}.mb-sm-3{margin-bottom:1rem !important}.mb-sm-4{margin-bottom:1.5rem !important}.mb-sm-5{margin-bottom:3rem !important}.mb-sm-auto{margin-bottom:auto !important}.ms-sm-0{margin-left:0 !important}.ms-sm-1{margin-left:.25rem !important}.ms-sm-2{margin-left:.5rem !important}.ms-sm-3{margin-left:1rem !important}.ms-sm-4{margin-left:1.5rem !important}.ms-sm-5{margin-left:3rem !important}.ms-sm-auto{margin-left:auto !important}.p-sm-0{padding:0 !important}.p-sm-1{padding:.25rem !important}.p-sm-2{padding:.5rem !important}.p-sm-3{padding:1rem !important}.p-sm-4{padding:1.5rem !important}.p-sm-5{padding:3rem !important}.px-sm-0{padding-right:0 !important;padding-left:0 !important}.px-sm-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-sm-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-sm-3{padding-right:1rem !important;padding-left:1rem !important}.px-sm-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-sm-5{padding-right:3rem !important;padding-left:3rem !important}.py-sm-0{padding-top:0 !important;padding-bottom:0 !important}.py-sm-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-sm-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-sm-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-sm-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-sm-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-sm-0{padding-top:0 !important}.pt-sm-1{padding-top:.25rem !important}.pt-sm-2{padding-top:.5rem !important}.pt-sm-3{padding-top:1rem !important}.pt-sm-4{padding-top:1.5rem !important}.pt-sm-5{padding-top:3rem !important}.pe-sm-0{padding-right:0 !important}.pe-sm-1{padding-right:.25rem !important}.pe-sm-2{padding-right:.5rem !important}.pe-sm-3{padding-right:1rem !important}.pe-sm-4{padding-right:1.5rem !important}.pe-sm-5{padding-right:3rem !important}.pb-sm-0{padding-bottom:0 !important}.pb-sm-1{padding-bottom:.25rem !important}.pb-sm-2{padding-bottom:.5rem !important}.pb-sm-3{padding-bottom:1rem !important}.pb-sm-4{padding-bottom:1.5rem !important}.pb-sm-5{padding-bottom:3rem !important}.ps-sm-0{padding-left:0 !important}.ps-sm-1{padding-left:.25rem !important}.ps-sm-2{padding-left:.5rem !important}.ps-sm-3{padding-left:1rem !important}.ps-sm-4{padding-left:1.5rem !important}.ps-sm-5{padding-left:3rem !important}.gap-sm-0{gap:0 !important}.gap-sm-1{gap:.25rem !important}.gap-sm-2{gap:.5rem !important}.gap-sm-3{gap:1rem !important}.gap-sm-4{gap:1.5rem !important}.gap-sm-5{gap:3rem !important}.row-gap-sm-0{row-gap:0 !important}.row-gap-sm-1{row-gap:.25rem !important}.row-gap-sm-2{row-gap:.5rem !important}.row-gap-sm-3{row-gap:1rem !important}.row-gap-sm-4{row-gap:1.5rem !important}.row-gap-sm-5{row-gap:3rem !important}.column-gap-sm-0{column-gap:0 !important}.column-gap-sm-1{column-gap:.25rem !important}.column-gap-sm-2{column-gap:.5rem !important}.column-gap-sm-3{column-gap:1rem !important}.column-gap-sm-4{column-gap:1.5rem !important}.column-gap-sm-5{column-gap:3rem !important}.text-sm-start{text-align:left !important}.text-sm-end{text-align:right !important}.text-sm-center{text-align:center !important}}@media(min-width: 768px){.float-md-start{float:left !important}.float-md-end{float:right !important}.float-md-none{float:none !important}.object-fit-md-contain{object-fit:contain !important}.object-fit-md-cover{object-fit:cover !important}.object-fit-md-fill{object-fit:fill !important}.object-fit-md-scale{object-fit:scale-down !important}.object-fit-md-none{object-fit:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-grid{display:grid !important}.d-md-inline-grid{display:inline-grid !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}.d-md-none{display:none !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.justify-content-md-evenly{justify-content:space-evenly !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}.order-md-first{order:-1 !important}.order-md-0{order:0 !important}.order-md-1{order:1 !important}.order-md-2{order:2 !important}.order-md-3{order:3 !important}.order-md-4{order:4 !important}.order-md-5{order:5 !important}.order-md-last{order:6 !important}.m-md-0{margin:0 !important}.m-md-1{margin:.25rem !important}.m-md-2{margin:.5rem !important}.m-md-3{margin:1rem !important}.m-md-4{margin:1.5rem !important}.m-md-5{margin:3rem !important}.m-md-auto{margin:auto !important}.mx-md-0{margin-right:0 !important;margin-left:0 !important}.mx-md-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-md-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-md-3{margin-right:1rem !important;margin-left:1rem !important}.mx-md-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-md-5{margin-right:3rem !important;margin-left:3rem !important}.mx-md-auto{margin-right:auto !important;margin-left:auto !important}.my-md-0{margin-top:0 !important;margin-bottom:0 !important}.my-md-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-md-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-md-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-md-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-md-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-md-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-md-0{margin-top:0 !important}.mt-md-1{margin-top:.25rem !important}.mt-md-2{margin-top:.5rem !important}.mt-md-3{margin-top:1rem !important}.mt-md-4{margin-top:1.5rem !important}.mt-md-5{margin-top:3rem !important}.mt-md-auto{margin-top:auto !important}.me-md-0{margin-right:0 !important}.me-md-1{margin-right:.25rem !important}.me-md-2{margin-right:.5rem !important}.me-md-3{margin-right:1rem !important}.me-md-4{margin-right:1.5rem !important}.me-md-5{margin-right:3rem !important}.me-md-auto{margin-right:auto !important}.mb-md-0{margin-bottom:0 !important}.mb-md-1{margin-bottom:.25rem !important}.mb-md-2{margin-bottom:.5rem !important}.mb-md-3{margin-bottom:1rem !important}.mb-md-4{margin-bottom:1.5rem !important}.mb-md-5{margin-bottom:3rem !important}.mb-md-auto{margin-bottom:auto !important}.ms-md-0{margin-left:0 !important}.ms-md-1{margin-left:.25rem !important}.ms-md-2{margin-left:.5rem !important}.ms-md-3{margin-left:1rem !important}.ms-md-4{margin-left:1.5rem !important}.ms-md-5{margin-left:3rem !important}.ms-md-auto{margin-left:auto !important}.p-md-0{padding:0 !important}.p-md-1{padding:.25rem !important}.p-md-2{padding:.5rem !important}.p-md-3{padding:1rem !important}.p-md-4{padding:1.5rem !important}.p-md-5{padding:3rem !important}.px-md-0{padding-right:0 !important;padding-left:0 !important}.px-md-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-md-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-md-3{padding-right:1rem !important;padding-left:1rem !important}.px-md-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-md-5{padding-right:3rem !important;padding-left:3rem !important}.py-md-0{padding-top:0 !important;padding-bottom:0 !important}.py-md-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-md-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-md-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-md-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-md-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-md-0{padding-top:0 !important}.pt-md-1{padding-top:.25rem !important}.pt-md-2{padding-top:.5rem !important}.pt-md-3{padding-top:1rem !important}.pt-md-4{padding-top:1.5rem !important}.pt-md-5{padding-top:3rem !important}.pe-md-0{padding-right:0 !important}.pe-md-1{padding-right:.25rem !important}.pe-md-2{padding-right:.5rem !important}.pe-md-3{padding-right:1rem !important}.pe-md-4{padding-right:1.5rem !important}.pe-md-5{padding-right:3rem !important}.pb-md-0{padding-bottom:0 !important}.pb-md-1{padding-bottom:.25rem !important}.pb-md-2{padding-bottom:.5rem !important}.pb-md-3{padding-bottom:1rem !important}.pb-md-4{padding-bottom:1.5rem !important}.pb-md-5{padding-bottom:3rem !important}.ps-md-0{padding-left:0 !important}.ps-md-1{padding-left:.25rem !important}.ps-md-2{padding-left:.5rem !important}.ps-md-3{padding-left:1rem !important}.ps-md-4{padding-left:1.5rem !important}.ps-md-5{padding-left:3rem !important}.gap-md-0{gap:0 !important}.gap-md-1{gap:.25rem !important}.gap-md-2{gap:.5rem !important}.gap-md-3{gap:1rem !important}.gap-md-4{gap:1.5rem !important}.gap-md-5{gap:3rem !important}.row-gap-md-0{row-gap:0 !important}.row-gap-md-1{row-gap:.25rem !important}.row-gap-md-2{row-gap:.5rem !important}.row-gap-md-3{row-gap:1rem !important}.row-gap-md-4{row-gap:1.5rem !important}.row-gap-md-5{row-gap:3rem !important}.column-gap-md-0{column-gap:0 !important}.column-gap-md-1{column-gap:.25rem !important}.column-gap-md-2{column-gap:.5rem !important}.column-gap-md-3{column-gap:1rem !important}.column-gap-md-4{column-gap:1.5rem !important}.column-gap-md-5{column-gap:3rem !important}.text-md-start{text-align:left !important}.text-md-end{text-align:right !important}.text-md-center{text-align:center !important}}@media(min-width: 992px){.float-lg-start{float:left !important}.float-lg-end{float:right !important}.float-lg-none{float:none !important}.object-fit-lg-contain{object-fit:contain !important}.object-fit-lg-cover{object-fit:cover !important}.object-fit-lg-fill{object-fit:fill !important}.object-fit-lg-scale{object-fit:scale-down !important}.object-fit-lg-none{object-fit:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-grid{display:grid !important}.d-lg-inline-grid{display:inline-grid !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}.d-lg-none{display:none !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.justify-content-lg-evenly{justify-content:space-evenly !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}.order-lg-first{order:-1 !important}.order-lg-0{order:0 !important}.order-lg-1{order:1 !important}.order-lg-2{order:2 !important}.order-lg-3{order:3 !important}.order-lg-4{order:4 !important}.order-lg-5{order:5 !important}.order-lg-last{order:6 !important}.m-lg-0{margin:0 !important}.m-lg-1{margin:.25rem !important}.m-lg-2{margin:.5rem !important}.m-lg-3{margin:1rem !important}.m-lg-4{margin:1.5rem !important}.m-lg-5{margin:3rem !important}.m-lg-auto{margin:auto !important}.mx-lg-0{margin-right:0 !important;margin-left:0 !important}.mx-lg-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-lg-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-lg-3{margin-right:1rem !important;margin-left:1rem !important}.mx-lg-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-lg-5{margin-right:3rem !important;margin-left:3rem !important}.mx-lg-auto{margin-right:auto !important;margin-left:auto !important}.my-lg-0{margin-top:0 !important;margin-bottom:0 !important}.my-lg-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-lg-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-lg-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-lg-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-lg-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-lg-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-lg-0{margin-top:0 !important}.mt-lg-1{margin-top:.25rem !important}.mt-lg-2{margin-top:.5rem !important}.mt-lg-3{margin-top:1rem !important}.mt-lg-4{margin-top:1.5rem !important}.mt-lg-5{margin-top:3rem !important}.mt-lg-auto{margin-top:auto !important}.me-lg-0{margin-right:0 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-2{margin-right:.5rem !important}.me-lg-3{margin-right:1rem !important}.me-lg-4{margin-right:1.5rem !important}.me-lg-5{margin-right:3rem !important}.me-lg-auto{margin-right:auto !important}.mb-lg-0{margin-bottom:0 !important}.mb-lg-1{margin-bottom:.25rem !important}.mb-lg-2{margin-bottom:.5rem !important}.mb-lg-3{margin-bottom:1rem !important}.mb-lg-4{margin-bottom:1.5rem !important}.mb-lg-5{margin-bottom:3rem !important}.mb-lg-auto{margin-bottom:auto !important}.ms-lg-0{margin-left:0 !important}.ms-lg-1{margin-left:.25rem !important}.ms-lg-2{margin-left:.5rem !important}.ms-lg-3{margin-left:1rem !important}.ms-lg-4{margin-left:1.5rem !important}.ms-lg-5{margin-left:3rem !important}.ms-lg-auto{margin-left:auto !important}.p-lg-0{padding:0 !important}.p-lg-1{padding:.25rem !important}.p-lg-2{padding:.5rem !important}.p-lg-3{padding:1rem !important}.p-lg-4{padding:1.5rem !important}.p-lg-5{padding:3rem !important}.px-lg-0{padding-right:0 !important;padding-left:0 !important}.px-lg-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-lg-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-lg-3{padding-right:1rem !important;padding-left:1rem !important}.px-lg-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-lg-5{padding-right:3rem !important;padding-left:3rem !important}.py-lg-0{padding-top:0 !important;padding-bottom:0 !important}.py-lg-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-lg-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-lg-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-lg-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-lg-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-lg-0{padding-top:0 !important}.pt-lg-1{padding-top:.25rem !important}.pt-lg-2{padding-top:.5rem !important}.pt-lg-3{padding-top:1rem !important}.pt-lg-4{padding-top:1.5rem !important}.pt-lg-5{padding-top:3rem !important}.pe-lg-0{padding-right:0 !important}.pe-lg-1{padding-right:.25rem !important}.pe-lg-2{padding-right:.5rem !important}.pe-lg-3{padding-right:1rem !important}.pe-lg-4{padding-right:1.5rem !important}.pe-lg-5{padding-right:3rem !important}.pb-lg-0{padding-bottom:0 !important}.pb-lg-1{padding-bottom:.25rem !important}.pb-lg-2{padding-bottom:.5rem !important}.pb-lg-3{padding-bottom:1rem !important}.pb-lg-4{padding-bottom:1.5rem !important}.pb-lg-5{padding-bottom:3rem !important}.ps-lg-0{padding-left:0 !important}.ps-lg-1{padding-left:.25rem !important}.ps-lg-2{padding-left:.5rem !important}.ps-lg-3{padding-left:1rem !important}.ps-lg-4{padding-left:1.5rem !important}.ps-lg-5{padding-left:3rem !important}.gap-lg-0{gap:0 !important}.gap-lg-1{gap:.25rem !important}.gap-lg-2{gap:.5rem !important}.gap-lg-3{gap:1rem !important}.gap-lg-4{gap:1.5rem !important}.gap-lg-5{gap:3rem !important}.row-gap-lg-0{row-gap:0 !important}.row-gap-lg-1{row-gap:.25rem !important}.row-gap-lg-2{row-gap:.5rem !important}.row-gap-lg-3{row-gap:1rem !important}.row-gap-lg-4{row-gap:1.5rem !important}.row-gap-lg-5{row-gap:3rem !important}.column-gap-lg-0{column-gap:0 !important}.column-gap-lg-1{column-gap:.25rem !important}.column-gap-lg-2{column-gap:.5rem !important}.column-gap-lg-3{column-gap:1rem !important}.column-gap-lg-4{column-gap:1.5rem !important}.column-gap-lg-5{column-gap:3rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}.text-lg-center{text-align:center !important}}@media(min-width: 1200px){.float-xl-start{float:left !important}.float-xl-end{float:right !important}.float-xl-none{float:none !important}.object-fit-xl-contain{object-fit:contain !important}.object-fit-xl-cover{object-fit:cover !important}.object-fit-xl-fill{object-fit:fill !important}.object-fit-xl-scale{object-fit:scale-down !important}.object-fit-xl-none{object-fit:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-grid{display:grid !important}.d-xl-inline-grid{display:inline-grid !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}.d-xl-none{display:none !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.justify-content-xl-evenly{justify-content:space-evenly !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}.order-xl-first{order:-1 !important}.order-xl-0{order:0 !important}.order-xl-1{order:1 !important}.order-xl-2{order:2 !important}.order-xl-3{order:3 !important}.order-xl-4{order:4 !important}.order-xl-5{order:5 !important}.order-xl-last{order:6 !important}.m-xl-0{margin:0 !important}.m-xl-1{margin:.25rem !important}.m-xl-2{margin:.5rem !important}.m-xl-3{margin:1rem !important}.m-xl-4{margin:1.5rem !important}.m-xl-5{margin:3rem !important}.m-xl-auto{margin:auto !important}.mx-xl-0{margin-right:0 !important;margin-left:0 !important}.mx-xl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}.my-xl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xl-0{margin-top:0 !important}.mt-xl-1{margin-top:.25rem !important}.mt-xl-2{margin-top:.5rem !important}.mt-xl-3{margin-top:1rem !important}.mt-xl-4{margin-top:1.5rem !important}.mt-xl-5{margin-top:3rem !important}.mt-xl-auto{margin-top:auto !important}.me-xl-0{margin-right:0 !important}.me-xl-1{margin-right:.25rem !important}.me-xl-2{margin-right:.5rem !important}.me-xl-3{margin-right:1rem !important}.me-xl-4{margin-right:1.5rem !important}.me-xl-5{margin-right:3rem !important}.me-xl-auto{margin-right:auto !important}.mb-xl-0{margin-bottom:0 !important}.mb-xl-1{margin-bottom:.25rem !important}.mb-xl-2{margin-bottom:.5rem !important}.mb-xl-3{margin-bottom:1rem !important}.mb-xl-4{margin-bottom:1.5rem !important}.mb-xl-5{margin-bottom:3rem !important}.mb-xl-auto{margin-bottom:auto !important}.ms-xl-0{margin-left:0 !important}.ms-xl-1{margin-left:.25rem !important}.ms-xl-2{margin-left:.5rem !important}.ms-xl-3{margin-left:1rem !important}.ms-xl-4{margin-left:1.5rem !important}.ms-xl-5{margin-left:3rem !important}.ms-xl-auto{margin-left:auto !important}.p-xl-0{padding:0 !important}.p-xl-1{padding:.25rem !important}.p-xl-2{padding:.5rem !important}.p-xl-3{padding:1rem !important}.p-xl-4{padding:1.5rem !important}.p-xl-5{padding:3rem !important}.px-xl-0{padding-right:0 !important;padding-left:0 !important}.px-xl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xl-0{padding-top:0 !important}.pt-xl-1{padding-top:.25rem !important}.pt-xl-2{padding-top:.5rem !important}.pt-xl-3{padding-top:1rem !important}.pt-xl-4{padding-top:1.5rem !important}.pt-xl-5{padding-top:3rem !important}.pe-xl-0{padding-right:0 !important}.pe-xl-1{padding-right:.25rem !important}.pe-xl-2{padding-right:.5rem !important}.pe-xl-3{padding-right:1rem !important}.pe-xl-4{padding-right:1.5rem !important}.pe-xl-5{padding-right:3rem !important}.pb-xl-0{padding-bottom:0 !important}.pb-xl-1{padding-bottom:.25rem !important}.pb-xl-2{padding-bottom:.5rem !important}.pb-xl-3{padding-bottom:1rem !important}.pb-xl-4{padding-bottom:1.5rem !important}.pb-xl-5{padding-bottom:3rem !important}.ps-xl-0{padding-left:0 !important}.ps-xl-1{padding-left:.25rem !important}.ps-xl-2{padding-left:.5rem !important}.ps-xl-3{padding-left:1rem !important}.ps-xl-4{padding-left:1.5rem !important}.ps-xl-5{padding-left:3rem !important}.gap-xl-0{gap:0 !important}.gap-xl-1{gap:.25rem !important}.gap-xl-2{gap:.5rem !important}.gap-xl-3{gap:1rem !important}.gap-xl-4{gap:1.5rem !important}.gap-xl-5{gap:3rem !important}.row-gap-xl-0{row-gap:0 !important}.row-gap-xl-1{row-gap:.25rem !important}.row-gap-xl-2{row-gap:.5rem !important}.row-gap-xl-3{row-gap:1rem !important}.row-gap-xl-4{row-gap:1.5rem !important}.row-gap-xl-5{row-gap:3rem !important}.column-gap-xl-0{column-gap:0 !important}.column-gap-xl-1{column-gap:.25rem !important}.column-gap-xl-2{column-gap:.5rem !important}.column-gap-xl-3{column-gap:1rem !important}.column-gap-xl-4{column-gap:1.5rem !important}.column-gap-xl-5{column-gap:3rem !important}.text-xl-start{text-align:left !important}.text-xl-end{text-align:right !important}.text-xl-center{text-align:center !important}}@media(min-width: 1400px){.float-xxl-start{float:left !important}.float-xxl-end{float:right !important}.float-xxl-none{float:none !important}.object-fit-xxl-contain{object-fit:contain !important}.object-fit-xxl-cover{object-fit:cover !important}.object-fit-xxl-fill{object-fit:fill !important}.object-fit-xxl-scale{object-fit:scale-down !important}.object-fit-xxl-none{object-fit:none !important}.d-xxl-inline{display:inline !important}.d-xxl-inline-block{display:inline-block !important}.d-xxl-block{display:block !important}.d-xxl-grid{display:grid !important}.d-xxl-inline-grid{display:inline-grid !important}.d-xxl-table{display:table !important}.d-xxl-table-row{display:table-row !important}.d-xxl-table-cell{display:table-cell !important}.d-xxl-flex{display:flex !important}.d-xxl-inline-flex{display:inline-flex !important}.d-xxl-none{display:none !important}.flex-xxl-fill{flex:1 1 auto !important}.flex-xxl-row{flex-direction:row !important}.flex-xxl-column{flex-direction:column !important}.flex-xxl-row-reverse{flex-direction:row-reverse !important}.flex-xxl-column-reverse{flex-direction:column-reverse !important}.flex-xxl-grow-0{flex-grow:0 !important}.flex-xxl-grow-1{flex-grow:1 !important}.flex-xxl-shrink-0{flex-shrink:0 !important}.flex-xxl-shrink-1{flex-shrink:1 !important}.flex-xxl-wrap{flex-wrap:wrap !important}.flex-xxl-nowrap{flex-wrap:nowrap !important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xxl-start{justify-content:flex-start !important}.justify-content-xxl-end{justify-content:flex-end !important}.justify-content-xxl-center{justify-content:center !important}.justify-content-xxl-between{justify-content:space-between !important}.justify-content-xxl-around{justify-content:space-around !important}.justify-content-xxl-evenly{justify-content:space-evenly !important}.align-items-xxl-start{align-items:flex-start !important}.align-items-xxl-end{align-items:flex-end !important}.align-items-xxl-center{align-items:center !important}.align-items-xxl-baseline{align-items:baseline !important}.align-items-xxl-stretch{align-items:stretch !important}.align-content-xxl-start{align-content:flex-start !important}.align-content-xxl-end{align-content:flex-end !important}.align-content-xxl-center{align-content:center !important}.align-content-xxl-between{align-content:space-between !important}.align-content-xxl-around{align-content:space-around !important}.align-content-xxl-stretch{align-content:stretch !important}.align-self-xxl-auto{align-self:auto !important}.align-self-xxl-start{align-self:flex-start !important}.align-self-xxl-end{align-self:flex-end !important}.align-self-xxl-center{align-self:center !important}.align-self-xxl-baseline{align-self:baseline !important}.align-self-xxl-stretch{align-self:stretch !important}.order-xxl-first{order:-1 !important}.order-xxl-0{order:0 !important}.order-xxl-1{order:1 !important}.order-xxl-2{order:2 !important}.order-xxl-3{order:3 !important}.order-xxl-4{order:4 !important}.order-xxl-5{order:5 !important}.order-xxl-last{order:6 !important}.m-xxl-0{margin:0 !important}.m-xxl-1{margin:.25rem !important}.m-xxl-2{margin:.5rem !important}.m-xxl-3{margin:1rem !important}.m-xxl-4{margin:1.5rem !important}.m-xxl-5{margin:3rem !important}.m-xxl-auto{margin:auto !important}.mx-xxl-0{margin-right:0 !important;margin-left:0 !important}.mx-xxl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xxl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xxl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xxl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xxl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xxl-auto{margin-right:auto !important;margin-left:auto !important}.my-xxl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xxl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xxl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xxl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xxl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xxl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xxl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xxl-0{margin-top:0 !important}.mt-xxl-1{margin-top:.25rem !important}.mt-xxl-2{margin-top:.5rem !important}.mt-xxl-3{margin-top:1rem !important}.mt-xxl-4{margin-top:1.5rem !important}.mt-xxl-5{margin-top:3rem !important}.mt-xxl-auto{margin-top:auto !important}.me-xxl-0{margin-right:0 !important}.me-xxl-1{margin-right:.25rem !important}.me-xxl-2{margin-right:.5rem !important}.me-xxl-3{margin-right:1rem !important}.me-xxl-4{margin-right:1.5rem !important}.me-xxl-5{margin-right:3rem !important}.me-xxl-auto{margin-right:auto !important}.mb-xxl-0{margin-bottom:0 !important}.mb-xxl-1{margin-bottom:.25rem !important}.mb-xxl-2{margin-bottom:.5rem !important}.mb-xxl-3{margin-bottom:1rem !important}.mb-xxl-4{margin-bottom:1.5rem !important}.mb-xxl-5{margin-bottom:3rem !important}.mb-xxl-auto{margin-bottom:auto !important}.ms-xxl-0{margin-left:0 !important}.ms-xxl-1{margin-left:.25rem !important}.ms-xxl-2{margin-left:.5rem !important}.ms-xxl-3{margin-left:1rem !important}.ms-xxl-4{margin-left:1.5rem !important}.ms-xxl-5{margin-left:3rem !important}.ms-xxl-auto{margin-left:auto !important}.p-xxl-0{padding:0 !important}.p-xxl-1{padding:.25rem !important}.p-xxl-2{padding:.5rem !important}.p-xxl-3{padding:1rem !important}.p-xxl-4{padding:1.5rem !important}.p-xxl-5{padding:3rem !important}.px-xxl-0{padding-right:0 !important;padding-left:0 !important}.px-xxl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xxl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xxl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xxl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xxl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xxl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xxl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xxl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xxl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xxl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xxl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xxl-0{padding-top:0 !important}.pt-xxl-1{padding-top:.25rem !important}.pt-xxl-2{padding-top:.5rem !important}.pt-xxl-3{padding-top:1rem !important}.pt-xxl-4{padding-top:1.5rem !important}.pt-xxl-5{padding-top:3rem !important}.pe-xxl-0{padding-right:0 !important}.pe-xxl-1{padding-right:.25rem !important}.pe-xxl-2{padding-right:.5rem !important}.pe-xxl-3{padding-right:1rem !important}.pe-xxl-4{padding-right:1.5rem !important}.pe-xxl-5{padding-right:3rem !important}.pb-xxl-0{padding-bottom:0 !important}.pb-xxl-1{padding-bottom:.25rem !important}.pb-xxl-2{padding-bottom:.5rem !important}.pb-xxl-3{padding-bottom:1rem !important}.pb-xxl-4{padding-bottom:1.5rem !important}.pb-xxl-5{padding-bottom:3rem !important}.ps-xxl-0{padding-left:0 !important}.ps-xxl-1{padding-left:.25rem !important}.ps-xxl-2{padding-left:.5rem !important}.ps-xxl-3{padding-left:1rem !important}.ps-xxl-4{padding-left:1.5rem !important}.ps-xxl-5{padding-left:3rem !important}.gap-xxl-0{gap:0 !important}.gap-xxl-1{gap:.25rem !important}.gap-xxl-2{gap:.5rem !important}.gap-xxl-3{gap:1rem !important}.gap-xxl-4{gap:1.5rem !important}.gap-xxl-5{gap:3rem !important}.row-gap-xxl-0{row-gap:0 !important}.row-gap-xxl-1{row-gap:.25rem !important}.row-gap-xxl-2{row-gap:.5rem !important}.row-gap-xxl-3{row-gap:1rem !important}.row-gap-xxl-4{row-gap:1.5rem !important}.row-gap-xxl-5{row-gap:3rem !important}.column-gap-xxl-0{column-gap:0 !important}.column-gap-xxl-1{column-gap:.25rem !important}.column-gap-xxl-2{column-gap:.5rem !important}.column-gap-xxl-3{column-gap:1rem !important}.column-gap-xxl-4{column-gap:1.5rem !important}.column-gap-xxl-5{column-gap:3rem !important}.text-xxl-start{text-align:left !important}.text-xxl-end{text-align:right !important}.text-xxl-center{text-align:center !important}}.bg-default{color:#fff}.bg-primary{color:#000}.bg-secondary{color:#fff}.bg-success{color:#fff}.bg-info{color:#fff}.bg-warning{color:#fff}.bg-danger{color:#fff}.bg-light{color:#000}.bg-dark{color:#fff}@media(min-width: 1200px){.fs-1{font-size:2rem !important}.fs-2{font-size:1.65rem !important}.fs-3{font-size:1.45rem !important}}@media print{.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-grid{display:grid !important}.d-print-inline-grid{display:inline-grid !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}.d-print-none{display:none !important}}:root{--bslib-spacer: 1rem;--bslib-mb-spacer: var(--bslib-spacer, 1rem)}.bslib-mb-spacing{margin-bottom:var(--bslib-mb-spacer)}.bslib-gap-spacing{gap:var(--bslib-mb-spacer)}.bslib-gap-spacing>.bslib-mb-spacing,.bslib-gap-spacing>.form-group,.bslib-gap-spacing>p,.bslib-gap-spacing>pre{margin-bottom:0}.html-fill-container>.html-fill-item.bslib-mb-spacing{margin-bottom:0}.tab-content>.tab-pane.html-fill-container{display:none}.tab-content>.active.html-fill-container{display:flex}.tab-content.html-fill-container{padding:0}.bg-blue{--bslib-color-bg: #2780e3;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-blue{--bslib-color-fg: #2780e3;color:var(--bslib-color-fg)}.bg-indigo{--bslib-color-bg: #6610f2;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-indigo{--bslib-color-fg: #6610f2;color:var(--bslib-color-fg)}.bg-purple{--bslib-color-bg: #613d7c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-purple{--bslib-color-fg: #613d7c;color:var(--bslib-color-fg)}.bg-pink{--bslib-color-bg: #e83e8c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-pink{--bslib-color-fg: #e83e8c;color:var(--bslib-color-fg)}.bg-red{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-red{--bslib-color-fg: #ff0039;color:var(--bslib-color-fg)}.bg-orange{--bslib-color-bg: #f0ad4e;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-orange{--bslib-color-fg: #f0ad4e;color:var(--bslib-color-fg)}.bg-yellow{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-yellow{--bslib-color-fg: #ff7518;color:var(--bslib-color-fg)}.bg-green{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-green{--bslib-color-fg: #3fb618;color:var(--bslib-color-fg)}.bg-teal{--bslib-color-bg: #20c997;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-teal{--bslib-color-fg: #20c997;color:var(--bslib-color-fg)}.bg-cyan{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-cyan{--bslib-color-fg: #9954bb;color:var(--bslib-color-fg)}.text-default{--bslib-color-fg: #343a40}.bg-default{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-primary{--bslib-color-fg: #E8E8FC}.bg-primary{--bslib-color-bg: #E8E8FC;--bslib-color-fg: #000}.text-secondary{--bslib-color-fg: #343a40}.bg-secondary{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-success{--bslib-color-fg: #3fb618}.bg-success{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff}.text-info{--bslib-color-fg: #9954bb}.bg-info{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff}.text-warning{--bslib-color-fg: #ff7518}.bg-warning{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff}.text-danger{--bslib-color-fg: #ff0039}.bg-danger{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff}.text-light{--bslib-color-fg: #f8f9fa}.bg-light{--bslib-color-bg: #f8f9fa;--bslib-color-fg: #000}.text-dark{--bslib-color-fg: #343a40}.bg-dark{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.bg-gradient-blue-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4053e9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4053e9;color:#fff}.bg-gradient-blue-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3e65ba;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3e65ba;color:#fff}.bg-gradient-blue-pink{--bslib-color-fg: #fff;--bslib-color-bg: #7466c0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #7466c0;color:#fff}.bg-gradient-blue-red{--bslib-color-fg: #fff;--bslib-color-bg: #7d4d9f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #7d4d9f;color:#fff}.bg-gradient-blue-orange{--bslib-color-fg: #fff;--bslib-color-bg: #7792a7;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #7792a7;color:#fff}.bg-gradient-blue-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #7d7c92;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #7d7c92;color:#fff}.bg-gradient-blue-green{--bslib-color-fg: #fff;--bslib-color-bg: #319692;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #319692;color:#fff}.bg-gradient-blue-teal{--bslib-color-fg: #fff;--bslib-color-bg: #249dc5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #249dc5;color:#fff}.bg-gradient-blue-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #556ed3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #556ed3;color:#fff}.bg-gradient-indigo-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4d3dec;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4d3dec;color:#fff}.bg-gradient-indigo-purple{--bslib-color-fg: #fff;--bslib-color-bg: #6422c3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #6422c3;color:#fff}.bg-gradient-indigo-pink{--bslib-color-fg: #fff;--bslib-color-bg: #9a22c9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #9a22c9;color:#fff}.bg-gradient-indigo-red{--bslib-color-fg: #fff;--bslib-color-bg: #a30aa8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a30aa8;color:#fff}.bg-gradient-indigo-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9d4fb0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9d4fb0;color:#fff}.bg-gradient-indigo-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a3389b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a3389b;color:#fff}.bg-gradient-indigo-green{--bslib-color-fg: #fff;--bslib-color-bg: #56529b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #56529b;color:#fff}.bg-gradient-indigo-teal{--bslib-color-fg: #fff;--bslib-color-bg: #4a5ace;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #4a5ace;color:#fff}.bg-gradient-indigo-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #7a2bdc;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #7a2bdc;color:#fff}.bg-gradient-purple-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4a58a5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4a58a5;color:#fff}.bg-gradient-purple-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #632bab;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #632bab;color:#fff}.bg-gradient-purple-pink{--bslib-color-fg: #fff;--bslib-color-bg: #973d82;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #973d82;color:#fff}.bg-gradient-purple-red{--bslib-color-fg: #fff;--bslib-color-bg: #a02561;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a02561;color:#fff}.bg-gradient-purple-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9a6a6a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9a6a6a;color:#fff}.bg-gradient-purple-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a05354;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a05354;color:#fff}.bg-gradient-purple-green{--bslib-color-fg: #fff;--bslib-color-bg: #536d54;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #536d54;color:#fff}.bg-gradient-purple-teal{--bslib-color-fg: #fff;--bslib-color-bg: #477587;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #477587;color:#fff}.bg-gradient-purple-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #774695;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #774695;color:#fff}.bg-gradient-pink-blue{--bslib-color-fg: #fff;--bslib-color-bg: #9b58af;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #9b58af;color:#fff}.bg-gradient-pink-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b42cb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b42cb5;color:#fff}.bg-gradient-pink-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b23e86;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b23e86;color:#fff}.bg-gradient-pink-red{--bslib-color-fg: #fff;--bslib-color-bg: #f1256b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f1256b;color:#fff}.bg-gradient-pink-orange{--bslib-color-fg: #fff;--bslib-color-bg: #eb6a73;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #eb6a73;color:#fff}.bg-gradient-pink-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #f1545e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f1545e;color:#fff}.bg-gradient-pink-green{--bslib-color-fg: #fff;--bslib-color-bg: #a46e5e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a46e5e;color:#fff}.bg-gradient-pink-teal{--bslib-color-fg: #fff;--bslib-color-bg: #987690;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #987690;color:#fff}.bg-gradient-pink-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #c8479f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #c8479f;color:#fff}.bg-gradient-red-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a9337d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a9337d;color:#fff}.bg-gradient-red-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c20683;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c20683;color:#fff}.bg-gradient-red-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c01854;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c01854;color:#fff}.bg-gradient-red-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f6195a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f6195a;color:#fff}.bg-gradient-red-orange{--bslib-color-fg: #fff;--bslib-color-bg: #f94541;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f94541;color:#fff}.bg-gradient-red-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #ff2f2c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #ff2f2c;color:#fff}.bg-gradient-red-green{--bslib-color-fg: #fff;--bslib-color-bg: #b2492c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b2492c;color:#fff}.bg-gradient-red-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6505f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6505f;color:#fff}.bg-gradient-red-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d6226d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d6226d;color:#fff}.bg-gradient-orange-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a09b8a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a09b8a;color:#fff}.bg-gradient-orange-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b96e90;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b96e90;color:#fff}.bg-gradient-orange-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b78060;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b78060;color:#fff}.bg-gradient-orange-pink{--bslib-color-fg: #fff;--bslib-color-bg: #ed8167;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #ed8167;color:#fff}.bg-gradient-orange-red{--bslib-color-fg: #fff;--bslib-color-bg: #f66846;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f66846;color:#fff}.bg-gradient-orange-yellow{--bslib-color-fg: #000;--bslib-color-bg: #f69738;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f69738;color:#000}.bg-gradient-orange-green{--bslib-color-fg: #000;--bslib-color-bg: #a9b138;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a9b138;color:#000}.bg-gradient-orange-teal{--bslib-color-fg: #000;--bslib-color-bg: #9db86b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #9db86b;color:#000}.bg-gradient-orange-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #cd897a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #cd897a;color:#fff}.bg-gradient-yellow-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a97969;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a97969;color:#fff}.bg-gradient-yellow-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c24d6f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c24d6f;color:#fff}.bg-gradient-yellow-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c05f40;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c05f40;color:#fff}.bg-gradient-yellow-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f65f46;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f65f46;color:#fff}.bg-gradient-yellow-red{--bslib-color-fg: #fff;--bslib-color-bg: #ff4625;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #ff4625;color:#fff}.bg-gradient-yellow-orange{--bslib-color-fg: #000;--bslib-color-bg: #f98b2e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f98b2e;color:#000}.bg-gradient-yellow-green{--bslib-color-fg: #fff;--bslib-color-bg: #b28f18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b28f18;color:#fff}.bg-gradient-yellow-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6974b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6974b;color:#fff}.bg-gradient-yellow-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d66859;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d66859;color:#fff}.bg-gradient-green-blue{--bslib-color-fg: #fff;--bslib-color-bg: #35a069;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #35a069;color:#fff}.bg-gradient-green-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4f746f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4f746f;color:#fff}.bg-gradient-green-purple{--bslib-color-fg: #fff;--bslib-color-bg: #4d8640;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #4d8640;color:#fff}.bg-gradient-green-pink{--bslib-color-fg: #fff;--bslib-color-bg: #838646;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #838646;color:#fff}.bg-gradient-green-red{--bslib-color-fg: #fff;--bslib-color-bg: #8c6d25;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #8c6d25;color:#fff}.bg-gradient-green-orange{--bslib-color-fg: #000;--bslib-color-bg: #86b22e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #86b22e;color:#000}.bg-gradient-green-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #8c9c18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #8c9c18;color:#fff}.bg-gradient-green-teal{--bslib-color-fg: #000;--bslib-color-bg: #33be4b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #33be4b;color:#000}.bg-gradient-green-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #638f59;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #638f59;color:#fff}.bg-gradient-teal-blue{--bslib-color-fg: #fff;--bslib-color-bg: #23acb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #23acb5;color:#fff}.bg-gradient-teal-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #3c7fbb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #3c7fbb;color:#fff}.bg-gradient-teal-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3a918c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3a918c;color:#fff}.bg-gradient-teal-pink{--bslib-color-fg: #fff;--bslib-color-bg: #709193;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #709193;color:#fff}.bg-gradient-teal-red{--bslib-color-fg: #fff;--bslib-color-bg: #797971;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #797971;color:#fff}.bg-gradient-teal-orange{--bslib-color-fg: #000;--bslib-color-bg: #73be7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #73be7a;color:#000}.bg-gradient-teal-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #79a764;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #79a764;color:#fff}.bg-gradient-teal-green{--bslib-color-fg: #000;--bslib-color-bg: #2cc164;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #2cc164;color:#000}.bg-gradient-teal-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #509aa5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #509aa5;color:#fff}.bg-gradient-cyan-blue{--bslib-color-fg: #fff;--bslib-color-bg: #6b66cb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #6b66cb;color:#fff}.bg-gradient-cyan-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #8539d1;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #8539d1;color:#fff}.bg-gradient-cyan-purple{--bslib-color-fg: #fff;--bslib-color-bg: #834ba2;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #834ba2;color:#fff}.bg-gradient-cyan-pink{--bslib-color-fg: #fff;--bslib-color-bg: #b94ba8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #b94ba8;color:#fff}.bg-gradient-cyan-red{--bslib-color-fg: #fff;--bslib-color-bg: #c23287;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #c23287;color:#fff}.bg-gradient-cyan-orange{--bslib-color-fg: #fff;--bslib-color-bg: #bc788f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #bc788f;color:#fff}.bg-gradient-cyan-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #c2617a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #c2617a;color:#fff}.bg-gradient-cyan-green{--bslib-color-fg: #fff;--bslib-color-bg: #757b7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #757b7a;color:#fff}.bg-gradient-cyan-teal{--bslib-color-fg: #fff;--bslib-color-bg: #6983ad;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #6983ad;color:#fff}.tab-content>.tab-pane.html-fill-container{display:none}.tab-content>.active.html-fill-container{display:flex}.tab-content.html-fill-container{padding:0}:root{--bslib-spacer: 1rem;--bslib-mb-spacer: var(--bslib-spacer, 1rem)}.bslib-mb-spacing{margin-bottom:var(--bslib-mb-spacer)}.bslib-gap-spacing{gap:var(--bslib-mb-spacer)}.bslib-gap-spacing>.bslib-mb-spacing,.bslib-gap-spacing>.form-group,.bslib-gap-spacing>p,.bslib-gap-spacing>pre{margin-bottom:0}.html-fill-container>.html-fill-item.bslib-mb-spacing{margin-bottom:0}.bg-blue{--bslib-color-bg: #2780e3;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-blue{--bslib-color-fg: #2780e3;color:var(--bslib-color-fg)}.bg-indigo{--bslib-color-bg: #6610f2;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-indigo{--bslib-color-fg: #6610f2;color:var(--bslib-color-fg)}.bg-purple{--bslib-color-bg: #613d7c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-purple{--bslib-color-fg: #613d7c;color:var(--bslib-color-fg)}.bg-pink{--bslib-color-bg: #e83e8c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-pink{--bslib-color-fg: #e83e8c;color:var(--bslib-color-fg)}.bg-red{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-red{--bslib-color-fg: #ff0039;color:var(--bslib-color-fg)}.bg-orange{--bslib-color-bg: #f0ad4e;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-orange{--bslib-color-fg: #f0ad4e;color:var(--bslib-color-fg)}.bg-yellow{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-yellow{--bslib-color-fg: #ff7518;color:var(--bslib-color-fg)}.bg-green{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-green{--bslib-color-fg: #3fb618;color:var(--bslib-color-fg)}.bg-teal{--bslib-color-bg: #20c997;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-teal{--bslib-color-fg: #20c997;color:var(--bslib-color-fg)}.bg-cyan{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-cyan{--bslib-color-fg: #9954bb;color:var(--bslib-color-fg)}.text-default{--bslib-color-fg: #343a40}.bg-default{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-primary{--bslib-color-fg: #E8E8FC}.bg-primary{--bslib-color-bg: #E8E8FC;--bslib-color-fg: #000}.text-secondary{--bslib-color-fg: #343a40}.bg-secondary{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-success{--bslib-color-fg: #3fb618}.bg-success{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff}.text-info{--bslib-color-fg: #9954bb}.bg-info{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff}.text-warning{--bslib-color-fg: #ff7518}.bg-warning{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff}.text-danger{--bslib-color-fg: #ff0039}.bg-danger{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff}.text-light{--bslib-color-fg: #f8f9fa}.bg-light{--bslib-color-bg: #f8f9fa;--bslib-color-fg: #000}.text-dark{--bslib-color-fg: #343a40}.bg-dark{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.bg-gradient-blue-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4053e9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4053e9;color:#fff}.bg-gradient-blue-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3e65ba;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3e65ba;color:#fff}.bg-gradient-blue-pink{--bslib-color-fg: #fff;--bslib-color-bg: #7466c0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #7466c0;color:#fff}.bg-gradient-blue-red{--bslib-color-fg: #fff;--bslib-color-bg: #7d4d9f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #7d4d9f;color:#fff}.bg-gradient-blue-orange{--bslib-color-fg: #fff;--bslib-color-bg: #7792a7;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #7792a7;color:#fff}.bg-gradient-blue-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #7d7c92;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #7d7c92;color:#fff}.bg-gradient-blue-green{--bslib-color-fg: #fff;--bslib-color-bg: #319692;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #319692;color:#fff}.bg-gradient-blue-teal{--bslib-color-fg: #fff;--bslib-color-bg: #249dc5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #249dc5;color:#fff}.bg-gradient-blue-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #556ed3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #556ed3;color:#fff}.bg-gradient-indigo-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4d3dec;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4d3dec;color:#fff}.bg-gradient-indigo-purple{--bslib-color-fg: #fff;--bslib-color-bg: #6422c3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #6422c3;color:#fff}.bg-gradient-indigo-pink{--bslib-color-fg: #fff;--bslib-color-bg: #9a22c9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #9a22c9;color:#fff}.bg-gradient-indigo-red{--bslib-color-fg: #fff;--bslib-color-bg: #a30aa8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a30aa8;color:#fff}.bg-gradient-indigo-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9d4fb0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9d4fb0;color:#fff}.bg-gradient-indigo-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a3389b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a3389b;color:#fff}.bg-gradient-indigo-green{--bslib-color-fg: #fff;--bslib-color-bg: #56529b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #56529b;color:#fff}.bg-gradient-indigo-teal{--bslib-color-fg: #fff;--bslib-color-bg: #4a5ace;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #4a5ace;color:#fff}.bg-gradient-indigo-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #7a2bdc;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #7a2bdc;color:#fff}.bg-gradient-purple-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4a58a5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4a58a5;color:#fff}.bg-gradient-purple-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #632bab;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #632bab;color:#fff}.bg-gradient-purple-pink{--bslib-color-fg: #fff;--bslib-color-bg: #973d82;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #973d82;color:#fff}.bg-gradient-purple-red{--bslib-color-fg: #fff;--bslib-color-bg: #a02561;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a02561;color:#fff}.bg-gradient-purple-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9a6a6a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9a6a6a;color:#fff}.bg-gradient-purple-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a05354;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a05354;color:#fff}.bg-gradient-purple-green{--bslib-color-fg: #fff;--bslib-color-bg: #536d54;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #536d54;color:#fff}.bg-gradient-purple-teal{--bslib-color-fg: #fff;--bslib-color-bg: #477587;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #477587;color:#fff}.bg-gradient-purple-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #774695;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #774695;color:#fff}.bg-gradient-pink-blue{--bslib-color-fg: #fff;--bslib-color-bg: #9b58af;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #9b58af;color:#fff}.bg-gradient-pink-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b42cb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b42cb5;color:#fff}.bg-gradient-pink-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b23e86;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b23e86;color:#fff}.bg-gradient-pink-red{--bslib-color-fg: #fff;--bslib-color-bg: #f1256b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f1256b;color:#fff}.bg-gradient-pink-orange{--bslib-color-fg: #fff;--bslib-color-bg: #eb6a73;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #eb6a73;color:#fff}.bg-gradient-pink-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #f1545e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f1545e;color:#fff}.bg-gradient-pink-green{--bslib-color-fg: #fff;--bslib-color-bg: #a46e5e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a46e5e;color:#fff}.bg-gradient-pink-teal{--bslib-color-fg: #fff;--bslib-color-bg: #987690;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #987690;color:#fff}.bg-gradient-pink-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #c8479f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #c8479f;color:#fff}.bg-gradient-red-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a9337d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a9337d;color:#fff}.bg-gradient-red-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c20683;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c20683;color:#fff}.bg-gradient-red-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c01854;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c01854;color:#fff}.bg-gradient-red-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f6195a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f6195a;color:#fff}.bg-gradient-red-orange{--bslib-color-fg: #fff;--bslib-color-bg: #f94541;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f94541;color:#fff}.bg-gradient-red-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #ff2f2c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #ff2f2c;color:#fff}.bg-gradient-red-green{--bslib-color-fg: #fff;--bslib-color-bg: #b2492c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b2492c;color:#fff}.bg-gradient-red-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6505f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6505f;color:#fff}.bg-gradient-red-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d6226d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d6226d;color:#fff}.bg-gradient-orange-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a09b8a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a09b8a;color:#fff}.bg-gradient-orange-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b96e90;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b96e90;color:#fff}.bg-gradient-orange-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b78060;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b78060;color:#fff}.bg-gradient-orange-pink{--bslib-color-fg: #fff;--bslib-color-bg: #ed8167;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #ed8167;color:#fff}.bg-gradient-orange-red{--bslib-color-fg: #fff;--bslib-color-bg: #f66846;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f66846;color:#fff}.bg-gradient-orange-yellow{--bslib-color-fg: #000;--bslib-color-bg: #f69738;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f69738;color:#000}.bg-gradient-orange-green{--bslib-color-fg: #000;--bslib-color-bg: #a9b138;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a9b138;color:#000}.bg-gradient-orange-teal{--bslib-color-fg: #000;--bslib-color-bg: #9db86b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #9db86b;color:#000}.bg-gradient-orange-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #cd897a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #cd897a;color:#fff}.bg-gradient-yellow-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a97969;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a97969;color:#fff}.bg-gradient-yellow-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c24d6f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c24d6f;color:#fff}.bg-gradient-yellow-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c05f40;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c05f40;color:#fff}.bg-gradient-yellow-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f65f46;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f65f46;color:#fff}.bg-gradient-yellow-red{--bslib-color-fg: #fff;--bslib-color-bg: #ff4625;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #ff4625;color:#fff}.bg-gradient-yellow-orange{--bslib-color-fg: #000;--bslib-color-bg: #f98b2e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f98b2e;color:#000}.bg-gradient-yellow-green{--bslib-color-fg: #fff;--bslib-color-bg: #b28f18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b28f18;color:#fff}.bg-gradient-yellow-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6974b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6974b;color:#fff}.bg-gradient-yellow-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d66859;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d66859;color:#fff}.bg-gradient-green-blue{--bslib-color-fg: #fff;--bslib-color-bg: #35a069;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #35a069;color:#fff}.bg-gradient-green-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4f746f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4f746f;color:#fff}.bg-gradient-green-purple{--bslib-color-fg: #fff;--bslib-color-bg: #4d8640;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #4d8640;color:#fff}.bg-gradient-green-pink{--bslib-color-fg: #fff;--bslib-color-bg: #838646;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #838646;color:#fff}.bg-gradient-green-red{--bslib-color-fg: #fff;--bslib-color-bg: #8c6d25;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #8c6d25;color:#fff}.bg-gradient-green-orange{--bslib-color-fg: #000;--bslib-color-bg: #86b22e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #86b22e;color:#000}.bg-gradient-green-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #8c9c18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #8c9c18;color:#fff}.bg-gradient-green-teal{--bslib-color-fg: #000;--bslib-color-bg: #33be4b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #33be4b;color:#000}.bg-gradient-green-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #638f59;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #638f59;color:#fff}.bg-gradient-teal-blue{--bslib-color-fg: #fff;--bslib-color-bg: #23acb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #23acb5;color:#fff}.bg-gradient-teal-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #3c7fbb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #3c7fbb;color:#fff}.bg-gradient-teal-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3a918c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3a918c;color:#fff}.bg-gradient-teal-pink{--bslib-color-fg: #fff;--bslib-color-bg: #709193;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #709193;color:#fff}.bg-gradient-teal-red{--bslib-color-fg: #fff;--bslib-color-bg: #797971;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #797971;color:#fff}.bg-gradient-teal-orange{--bslib-color-fg: #000;--bslib-color-bg: #73be7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #73be7a;color:#000}.bg-gradient-teal-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #79a764;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #79a764;color:#fff}.bg-gradient-teal-green{--bslib-color-fg: #000;--bslib-color-bg: #2cc164;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #2cc164;color:#000}.bg-gradient-teal-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #509aa5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #509aa5;color:#fff}.bg-gradient-cyan-blue{--bslib-color-fg: #fff;--bslib-color-bg: #6b66cb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #6b66cb;color:#fff}.bg-gradient-cyan-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #8539d1;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #8539d1;color:#fff}.bg-gradient-cyan-purple{--bslib-color-fg: #fff;--bslib-color-bg: #834ba2;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #834ba2;color:#fff}.bg-gradient-cyan-pink{--bslib-color-fg: #fff;--bslib-color-bg: #b94ba8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #b94ba8;color:#fff}.bg-gradient-cyan-red{--bslib-color-fg: #fff;--bslib-color-bg: #c23287;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #c23287;color:#fff}.bg-gradient-cyan-orange{--bslib-color-fg: #fff;--bslib-color-bg: #bc788f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #bc788f;color:#fff}.bg-gradient-cyan-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #c2617a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #c2617a;color:#fff}.bg-gradient-cyan-green{--bslib-color-fg: #fff;--bslib-color-bg: #757b7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #757b7a;color:#fff}.bg-gradient-cyan-teal{--bslib-color-fg: #fff;--bslib-color-bg: #6983ad;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #6983ad;color:#fff}:root{--bslib-value-box-shadow: none;--bslib-value-box-border-width-auto-yes: var(--bslib-value-box-border-width-baseline);--bslib-value-box-border-width-auto-no: 0;--bslib-value-box-border-width-baseline: 1px}.bslib-value-box{border-width:var(--bslib-value-box-border-width-auto-no, var(--bslib-value-box-border-width-baseline));container-name:bslib-value-box;container-type:inline-size}.bslib-value-box.card{box-shadow:var(--bslib-value-box-shadow)}.bslib-value-box.border-auto{border-width:var(--bslib-value-box-border-width-auto-yes, var(--bslib-value-box-border-width-baseline))}.bslib-value-box.default{--bslib-value-box-bg-default: var(--bs-card-bg, #FFFFFF);--bslib-value-box-border-color-default: var(--bs-card-border-color, rgba(0, 0, 0, 0.175));color:var(--bslib-value-box-color);background-color:var(--bslib-value-box-bg, var(--bslib-value-box-bg-default));border-color:var(--bslib-value-box-border-color, var(--bslib-value-box-border-color-default))}.bslib-value-box .value-box-grid{display:grid;grid-template-areas:"left right";align-items:center;overflow:hidden}.bslib-value-box .value-box-showcase{height:100%;max-height:var(---bslib-value-box-showcase-max-h, 100%)}.bslib-value-box .value-box-showcase,.bslib-value-box .value-box-showcase>.html-fill-item{width:100%}.bslib-value-box[data-full-screen=true] .value-box-showcase{max-height:var(---bslib-value-box-showcase-max-h-fs, 100%)}@media screen and (min-width: 575.98px){@container bslib-value-box (max-width: 300px){.bslib-value-box:not(.showcase-bottom) .value-box-grid{grid-template-columns:1fr !important;grid-template-rows:auto auto;grid-template-areas:"top" "bottom"}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-showcase{grid-area:top !important}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-area{grid-area:bottom !important;justify-content:end}}}.bslib-value-box .value-box-area{justify-content:center;padding:1.5rem 1rem;font-size:.9rem;font-weight:500}.bslib-value-box .value-box-area *{margin-bottom:0;margin-top:0}.bslib-value-box .value-box-title{font-size:1rem;margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}.bslib-value-box .value-box-title:empty::after{content:" "}.bslib-value-box .value-box-value{font-size:calc(1.29rem + 0.48vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}@media(min-width: 1200px){.bslib-value-box .value-box-value{font-size:1.65rem}}.bslib-value-box .value-box-value:empty::after{content:" "}.bslib-value-box .value-box-showcase{align-items:center;justify-content:center;margin-top:auto;margin-bottom:auto;padding:1rem}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{opacity:.85;min-width:50px;max-width:125%}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{font-size:4rem}.bslib-value-box.showcase-top-right .value-box-grid{grid-template-columns:1fr var(---bslib-value-box-showcase-w, 50%)}.bslib-value-box.showcase-top-right .value-box-grid .value-box-showcase{grid-area:right;margin-left:auto;align-self:start;align-items:end;padding-left:0;padding-bottom:0}.bslib-value-box.showcase-top-right .value-box-grid .value-box-area{grid-area:left;align-self:end}.bslib-value-box.showcase-top-right[data-full-screen=true] .value-box-grid{grid-template-columns:auto var(---bslib-value-box-showcase-w-fs, 1fr)}.bslib-value-box.showcase-top-right[data-full-screen=true] .value-box-grid>div{align-self:center}.bslib-value-box.showcase-top-right:not([data-full-screen=true]) .value-box-showcase{margin-top:0}@container bslib-value-box (max-width: 300px){.bslib-value-box.showcase-top-right:not([data-full-screen=true]) .value-box-grid .value-box-showcase{padding-left:1rem}}.bslib-value-box.showcase-left-center .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w, 30%) auto}.bslib-value-box.showcase-left-center[data-full-screen=true] .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w-fs, 1fr) auto}.bslib-value-box.showcase-left-center:not([data-fill-screen=true]) .value-box-grid .value-box-showcase{grid-area:left}.bslib-value-box.showcase-left-center:not([data-fill-screen=true]) .value-box-grid .value-box-area{grid-area:right}.bslib-value-box.showcase-bottom .value-box-grid{grid-template-columns:1fr;grid-template-rows:1fr var(---bslib-value-box-showcase-h, auto);grid-template-areas:"top" "bottom";overflow:hidden}.bslib-value-box.showcase-bottom .value-box-grid .value-box-showcase{grid-area:bottom;padding:0;margin:0}.bslib-value-box.showcase-bottom .value-box-grid .value-box-area{grid-area:top}.bslib-value-box.showcase-bottom[data-full-screen=true] .value-box-grid{grid-template-rows:1fr var(---bslib-value-box-showcase-h-fs, 2fr)}.bslib-value-box.showcase-bottom[data-full-screen=true] .value-box-grid .value-box-showcase{padding:1rem}[data-bs-theme=dark] .bslib-value-box{--bslib-value-box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 50%)}@media(min-width: 576px){.nav:not(.nav-hidden){display:flex !important;display:-webkit-flex !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column){float:none !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.bslib-nav-spacer{margin-left:auto !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.form-inline{margin-top:auto;margin-bottom:auto}.nav:not(.nav-hidden).nav-stacked{flex-direction:column;-webkit-flex-direction:column;height:100%}.nav:not(.nav-hidden).nav-stacked>.bslib-nav-spacer{margin-top:auto !important}}.bslib-card{overflow:auto}.bslib-card .card-body+.card-body{padding-top:0}.bslib-card .card-body{overflow:auto}.bslib-card .card-body p{margin-top:0}.bslib-card .card-body p:last-child{margin-bottom:0}.bslib-card .card-body{max-height:var(--bslib-card-body-max-height, none)}.bslib-card[data-full-screen=true]>.card-body{max-height:var(--bslib-card-body-max-height-full-screen, none)}.bslib-card .card-header .form-group{margin-bottom:0}.bslib-card .card-header .selectize-control{margin-bottom:0}.bslib-card .card-header .selectize-control .item{margin-right:1.15rem}.bslib-card .card-footer{margin-top:auto}.bslib-card .bslib-navs-card-title{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center}.bslib-card .bslib-navs-card-title .nav{margin-left:auto}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border=true]){border:none}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border-radius=true]){border-top-left-radius:0;border-top-right-radius:0}[data-full-screen=true]{position:fixed;inset:3.5rem 1rem 1rem;height:auto !important;max-height:none !important;width:auto !important;z-index:1070}.bslib-full-screen-enter{display:none;position:absolute;bottom:var(--bslib-full-screen-enter-bottom, 0.2rem);right:var(--bslib-full-screen-enter-right, 0);top:var(--bslib-full-screen-enter-top);left:var(--bslib-full-screen-enter-left);color:var(--bslib-color-fg, var(--bs-card-color));background-color:var(--bslib-color-bg, var(--bs-card-bg, var(--bs-body-bg)));border:var(--bs-card-border-width) solid var(--bslib-color-fg, var(--bs-card-border-color));box-shadow:0 2px 4px rgba(0,0,0,.15);margin:.2rem .4rem;padding:.55rem !important;font-size:.8rem;cursor:pointer;opacity:.7;z-index:1070}.bslib-full-screen-enter:hover{opacity:1}.card[data-full-screen=false]:hover>*>.bslib-full-screen-enter{display:block}.bslib-has-full-screen .card:hover>*>.bslib-full-screen-enter{display:none}@media(max-width: 575.98px){.bslib-full-screen-enter{display:none !important}}.bslib-full-screen-exit{position:relative;top:1.35rem;font-size:.9rem;cursor:pointer;text-decoration:none;display:flex;float:right;margin-right:2.15rem;align-items:center;color:rgba(var(--bs-body-bg-rgb), 0.8)}.bslib-full-screen-exit:hover{color:rgba(var(--bs-body-bg-rgb), 1)}.bslib-full-screen-exit svg{margin-left:.5rem;font-size:1.5rem}#bslib-full-screen-overlay{position:fixed;inset:0;background-color:rgba(var(--bs-body-color-rgb), 0.6);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);z-index:1069;animation:bslib-full-screen-overlay-enter 400ms cubic-bezier(0.6, 0.02, 0.65, 1) forwards}@keyframes bslib-full-screen-overlay-enter{0%{opacity:0}100%{opacity:1}}.bslib-grid{display:grid !important;gap:var(--bslib-spacer, 1rem);height:var(--bslib-grid-height)}.bslib-grid.grid{grid-template-columns:repeat(var(--bs-columns, 12), minmax(0, 1fr));grid-template-rows:unset;grid-auto-rows:var(--bslib-grid--row-heights);--bslib-grid--row-heights--xs: unset;--bslib-grid--row-heights--sm: unset;--bslib-grid--row-heights--md: unset;--bslib-grid--row-heights--lg: unset;--bslib-grid--row-heights--xl: unset;--bslib-grid--row-heights--xxl: unset}.bslib-grid.grid.bslib-grid--row-heights--xs{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xs)}@media(min-width: 576px){.bslib-grid.grid.bslib-grid--row-heights--sm{--bslib-grid--row-heights: var(--bslib-grid--row-heights--sm)}}@media(min-width: 768px){.bslib-grid.grid.bslib-grid--row-heights--md{--bslib-grid--row-heights: var(--bslib-grid--row-heights--md)}}@media(min-width: 992px){.bslib-grid.grid.bslib-grid--row-heights--lg{--bslib-grid--row-heights: var(--bslib-grid--row-heights--lg)}}@media(min-width: 1200px){.bslib-grid.grid.bslib-grid--row-heights--xl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xl)}}@media(min-width: 1400px){.bslib-grid.grid.bslib-grid--row-heights--xxl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xxl)}}.bslib-grid>*>.shiny-input-container{width:100%}.bslib-grid-item{grid-column:auto/span 1}@media(max-width: 767.98px){.bslib-grid-item{grid-column:1/-1}}@media(max-width: 575.98px){.bslib-grid{grid-template-columns:1fr !important;height:var(--bslib-grid-height-mobile)}.bslib-grid.grid{height:unset !important;grid-auto-rows:var(--bslib-grid--row-heights--xs, auto)}}.accordion .accordion-header{font-size:calc(1.29rem + 0.48vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color);margin-bottom:0}@media(min-width: 1200px){.accordion .accordion-header{font-size:1.65rem}}.accordion .accordion-icon:not(:empty){margin-right:.75rem;display:flex}.accordion .accordion-button:not(.collapsed){box-shadow:none}.accordion .accordion-button:not(.collapsed):focus{box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.navbar+.container-fluid:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-sm:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-md:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-lg:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-xl:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-xxl:has(>.tab-content>.tab-pane.active.html-fill-container){padding-left:0;padding-right:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container{padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child){padding:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]){border-left:none;border-right:none;border-bottom:none}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]){border-radius:0}.navbar+div>.bslib-sidebar-layout{border-top:var(--bslib-sidebar-border)}html{height:100%}.bslib-page-fill{width:100%;height:100%;margin:0;padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}@media(max-width: 575.98px){.bslib-page-fill{height:var(--bslib-page-fill-mobile-height, auto)}}:root{--bslib-page-sidebar-title-bg: #3CDD8C;--bslib-page-sidebar-title-color: #000}.bslib-page-title{background-color:var(--bslib-page-sidebar-title-bg);color:var(--bslib-page-sidebar-title-color);font-size:1.25rem;font-weight:300;padding:var(--bslib-spacer, 1rem);padding-left:1.5rem;margin-bottom:0;border-bottom:1px solid #dee2e6}.bslib-sidebar-layout{--bslib-sidebar-transition-duration: 500ms;--bslib-sidebar-transition-easing-x: cubic-bezier(0.8, 0.78, 0.22, 1.07);--bslib-sidebar-border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0, 0, 0, 0.175));--bslib-sidebar-border-radius: var(--bs-border-radius);--bslib-sidebar-vert-border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0, 0, 0, 0.175));--bslib-sidebar-bg: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);--bslib-sidebar-fg: var(--bs-emphasis-color, black);--bslib-sidebar-main-fg: var(--bs-card-color, var(--bs-body-color));--bslib-sidebar-main-bg: var(--bs-card-bg, var(--bs-body-bg));--bslib-sidebar-toggle-bg: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.1);--bslib-sidebar-padding: calc(var(--bslib-spacer) * 1.5);--bslib-sidebar-icon-size: var(--bslib-spacer, 1rem);--bslib-sidebar-icon-button-size: calc(var(--bslib-sidebar-icon-size, 1rem) * 2);--bslib-sidebar-padding-icon: calc(var(--bslib-sidebar-icon-button-size, 2rem) * 1.5);--bslib-collapse-toggle-border-radius: var(--bs-border-radius, 0.25rem);--bslib-collapse-toggle-transform: 0deg;--bslib-sidebar-toggle-transition-easing: cubic-bezier(1, 0, 0, 1);--bslib-collapse-toggle-right-transform: 180deg;--bslib-sidebar-column-main: minmax(0, 1fr);display:grid !important;grid-template-columns:min(100% - var(--bslib-sidebar-icon-size),var(--bslib-sidebar-width, 250px)) var(--bslib-sidebar-column-main);position:relative;transition:grid-template-columns ease-in-out var(--bslib-sidebar-transition-duration);border:var(--bslib-sidebar-border);border-radius:var(--bslib-sidebar-border-radius)}@media(prefers-reduced-motion: reduce){.bslib-sidebar-layout{transition:none}}.bslib-sidebar-layout[data-bslib-sidebar-border=false]{border:none}.bslib-sidebar-layout[data-bslib-sidebar-border-radius=false]{border-radius:initial}.bslib-sidebar-layout>.main,.bslib-sidebar-layout>.sidebar{grid-row:1/2;border-radius:inherit;overflow:auto}.bslib-sidebar-layout>.main{grid-column:2/3;border-top-left-radius:0;border-bottom-left-radius:0;padding:var(--bslib-sidebar-padding);transition:padding var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration);color:var(--bslib-sidebar-main-fg);background-color:var(--bslib-sidebar-main-bg)}.bslib-sidebar-layout>.sidebar{grid-column:1/2;width:100%;height:100%;border-right:var(--bslib-sidebar-vert-border);border-top-right-radius:0;border-bottom-right-radius:0;color:var(--bslib-sidebar-fg);background-color:var(--bslib-sidebar-bg);backdrop-filter:blur(5px)}.bslib-sidebar-layout>.sidebar>.sidebar-content{display:flex;flex-direction:column;gap:var(--bslib-spacer, 1rem);padding:var(--bslib-sidebar-padding);padding-top:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout>.sidebar>.sidebar-content>:last-child:not(.sidebar-title){margin-bottom:0}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion{margin-left:calc(-1*var(--bslib-sidebar-padding));margin-right:calc(-1*var(--bslib-sidebar-padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:last-child{margin-bottom:calc(-1*var(--bslib-sidebar-padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child){margin-bottom:1rem}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion .accordion-body{display:flex;flex-direction:column}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:first-child) .accordion-item:first-child{border-top:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child) .accordion-item:last-child{border-bottom:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content.has-accordion>.sidebar-title{border-bottom:none;padding-bottom:0}.bslib-sidebar-layout>.sidebar .shiny-input-container{width:100%}.bslib-sidebar-layout[data-bslib-sidebar-open=always]>.sidebar>.sidebar-content{padding-top:var(--bslib-sidebar-padding)}.bslib-sidebar-layout>.collapse-toggle{grid-row:1/2;grid-column:1/2;display:inline-flex;align-items:center;position:absolute;right:calc(var(--bslib-sidebar-icon-size));top:calc(var(--bslib-sidebar-icon-size, 1rem)/2);border:none;border-radius:var(--bslib-collapse-toggle-border-radius);height:var(--bslib-sidebar-icon-button-size, 2rem);width:var(--bslib-sidebar-icon-button-size, 2rem);display:flex;align-items:center;justify-content:center;padding:0;color:var(--bslib-sidebar-fg);background-color:unset;transition:color var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),top var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),right var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),left var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover{background-color:var(--bslib-sidebar-toggle-bg)}.bslib-sidebar-layout>.collapse-toggle>.collapse-icon{opacity:.8;width:var(--bslib-sidebar-icon-size);height:var(--bslib-sidebar-icon-size);transform:rotateY(var(--bslib-collapse-toggle-transform));transition:transform var(--bslib-sidebar-toggle-transition-easing) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover>.collapse-icon{opacity:1}.bslib-sidebar-layout .sidebar-title{font-size:1.25rem;line-height:1.25;margin-top:0;margin-bottom:1rem;padding-bottom:1rem;border-bottom:var(--bslib-sidebar-border)}.bslib-sidebar-layout.sidebar-right{grid-template-columns:var(--bslib-sidebar-column-main) min(100% - var(--bslib-sidebar-icon-size),var(--bslib-sidebar-width, 250px))}.bslib-sidebar-layout.sidebar-right>.main{grid-column:1/2;border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:inherit;border-bottom-left-radius:inherit}.bslib-sidebar-layout.sidebar-right>.sidebar{grid-column:2/3;border-right:none;border-left:var(--bslib-sidebar-vert-border);border-top-left-radius:0;border-bottom-left-radius:0}.bslib-sidebar-layout.sidebar-right>.collapse-toggle{grid-column:2/3;left:var(--bslib-sidebar-icon-size);right:unset;border:var(--bslib-collapse-toggle-border)}.bslib-sidebar-layout.sidebar-right>.collapse-toggle>.collapse-icon{transform:rotateY(var(--bslib-collapse-toggle-right-transform))}.bslib-sidebar-layout.sidebar-collapsed{--bslib-collapse-toggle-transform: 180deg;--bslib-collapse-toggle-right-transform: 0deg;--bslib-sidebar-vert-border: none;grid-template-columns:0 minmax(0, 1fr)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right{grid-template-columns:minmax(0, 1fr) 0}.bslib-sidebar-layout.sidebar-collapsed:not(.transitioning)>.sidebar>*{display:none}.bslib-sidebar-layout.sidebar-collapsed>.main{border-radius:inherit}.bslib-sidebar-layout.sidebar-collapsed:not(.sidebar-right)>.main{padding-left:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.main{padding-right:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout.sidebar-collapsed>.collapse-toggle{color:var(--bslib-sidebar-main-fg);top:calc(var(--bslib-sidebar-overlap-counter, 0)*(var(--bslib-sidebar-icon-size) + var(--bslib-sidebar-padding)) + var(--bslib-sidebar-icon-size, 1rem)/2);right:calc(-2.5*var(--bslib-sidebar-icon-size) - var(--bs-card-border-width, 1px))}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.collapse-toggle{left:calc(-2.5*var(--bslib-sidebar-icon-size) - var(--bs-card-border-width, 1px));right:unset}@media(min-width: 576px){.bslib-sidebar-layout.transitioning>.sidebar>.sidebar-content{display:none}}@media(max-width: 575.98px){.bslib-sidebar-layout[data-bslib-sidebar-open=desktop]{--bslib-sidebar-js-init-collapsed: true}.bslib-sidebar-layout>.sidebar,.bslib-sidebar-layout.sidebar-right>.sidebar{border:none}.bslib-sidebar-layout>.main,.bslib-sidebar-layout.sidebar-right>.main{grid-column:1/3}.bslib-sidebar-layout[data-bslib-sidebar-open=always]{display:block !important}.bslib-sidebar-layout[data-bslib-sidebar-open=always]>.sidebar{max-height:var(--bslib-sidebar-max-height-mobile);overflow-y:auto;border-top:var(--bslib-sidebar-vert-border)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]){grid-template-columns:100% 0}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-collapsed)>.sidebar{z-index:1}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-collapsed)>.collapse-toggle{z-index:1}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-right{grid-template-columns:0 100%}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed{grid-template-columns:0 100%}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed.sidebar-right{grid-template-columns:100% 0}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-right)>.main{padding-left:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-right>.main{padding-right:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always])>.main{opacity:0;transition:opacity var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed>.main{opacity:1}}.html-fill-container{display:flex;flex-direction:column;min-height:0;min-width:0}.html-fill-container>.html-fill-item{flex:1 1 auto;min-height:0;min-width:0}.html-fill-container>:not(.html-fill-item){flex:0 0 auto}.quarto-container{min-height:calc(100vh - 132px)}body.hypothesis-enabled #quarto-header{margin-right:16px}footer.footer .nav-footer,#quarto-header>nav{padding-left:1em;padding-right:1em}footer.footer div.nav-footer p:first-child{margin-top:0}footer.footer div.nav-footer p:last-child{margin-bottom:0}#quarto-content>*{padding-top:14px}#quarto-content>#quarto-sidebar-glass{padding-top:0px}@media(max-width: 991.98px){#quarto-content>*{padding-top:0}#quarto-content .subtitle{padding-top:14px}#quarto-content section:first-of-type h2:first-of-type,#quarto-content section:first-of-type .h2:first-of-type{margin-top:1rem}}.headroom-target,header.headroom{will-change:transform;transition:position 200ms linear;transition:all 200ms linear}header.headroom--pinned{transform:translateY(0%)}header.headroom--unpinned{transform:translateY(-100%)}.navbar-container{width:100%}.navbar-brand{overflow:hidden;text-overflow:ellipsis}.navbar-brand-container{max-width:calc(100% - 115px);min-width:0;display:flex;align-items:center}@media(min-width: 992px){.navbar-brand-container{margin-right:1em}}.navbar-brand.navbar-brand-logo{margin-right:4px;display:inline-flex}.navbar-toggler{flex-basis:content;flex-shrink:0}.navbar .navbar-brand-container{order:2}.navbar .navbar-toggler{order:1}.navbar .navbar-container>.navbar-nav{order:20}.navbar .navbar-container>.navbar-brand-container{margin-left:0 !important;margin-right:0 !important}.navbar .navbar-collapse{order:20}.navbar #quarto-search{order:4;margin-left:auto}.navbar .navbar-toggler{margin-right:.5em}.navbar-collapse .quarto-navbar-tools{margin-left:.5em}.navbar-logo{max-height:24px;width:auto;padding-right:4px}nav .nav-item:not(.compact){padding-top:1px}nav .nav-link i,nav .dropdown-item i{padding-right:1px}.navbar-expand-lg .navbar-nav .nav-link{padding-left:.6rem;padding-right:.6rem}nav .nav-item.compact .nav-link{padding-left:.5rem;padding-right:.5rem;font-size:1.1rem}.navbar .quarto-navbar-tools{order:3}.navbar .quarto-navbar-tools div.dropdown{display:inline-block}.navbar .quarto-navbar-tools .quarto-navigation-tool{color:#fff}.navbar .quarto-navbar-tools .quarto-navigation-tool:hover{color:#000}.navbar-nav .dropdown-menu{min-width:220px;font-size:.9rem}.navbar .navbar-nav .nav-link.dropdown-toggle::after{opacity:.75;vertical-align:.175em}.navbar ul.dropdown-menu{padding-top:0;padding-bottom:0}.navbar .dropdown-header{text-transform:uppercase;font-size:.8rem;padding:0 .5rem}.navbar .dropdown-item{padding:.4rem .5rem}.navbar .dropdown-item>i.bi{margin-left:.1rem;margin-right:.25em}.sidebar #quarto-search{margin-top:-1px}.sidebar #quarto-search svg.aa-SubmitIcon{width:16px;height:16px}.sidebar-navigation a{color:inherit}.sidebar-title{margin-top:.25rem;padding-bottom:.5rem;font-size:1.3rem;line-height:1.6rem;visibility:visible}.sidebar-title>a{font-size:inherit;text-decoration:none}.sidebar-title .sidebar-tools-main{margin-top:-6px}@media(max-width: 991.98px){#quarto-sidebar div.sidebar-header{padding-top:.2em}}.sidebar-header-stacked .sidebar-title{margin-top:.6rem}.sidebar-logo{max-width:90%;padding-bottom:.5rem}.sidebar-logo-link{text-decoration:none}.sidebar-navigation li a{text-decoration:none}.sidebar-navigation .quarto-navigation-tool{opacity:.7;font-size:.875rem}#quarto-sidebar>nav>.sidebar-tools-main{margin-left:14px}.sidebar-tools-main{display:inline-flex;margin-left:0px;order:2}.sidebar-tools-main:not(.tools-wide){vertical-align:middle}.sidebar-navigation .quarto-navigation-tool.dropdown-toggle::after{display:none}.sidebar.sidebar-navigation>*{padding-top:1em}.sidebar-item{margin-bottom:.2em;line-height:1rem;margin-top:.4rem}.sidebar-section{padding-left:.5em;padding-bottom:.2em}.sidebar-item .sidebar-item-container{display:flex;justify-content:space-between;cursor:pointer}.sidebar-item-toggle:hover{cursor:pointer}.sidebar-item .sidebar-item-toggle .bi{font-size:.7rem;text-align:center}.sidebar-item .sidebar-item-toggle .bi-chevron-right::before{transition:transform 200ms ease}.sidebar-item .sidebar-item-toggle[aria-expanded=false] .bi-chevron-right::before{transform:none}.sidebar-item .sidebar-item-toggle[aria-expanded=true] .bi-chevron-right::before{transform:rotate(90deg)}.sidebar-item-text{width:100%}.sidebar-navigation .sidebar-divider{margin-left:0;margin-right:0;margin-top:.5rem;margin-bottom:.5rem}@media(max-width: 991.98px){.quarto-secondary-nav{display:block}.quarto-secondary-nav button.quarto-search-button{padding-right:0em;padding-left:2em}.quarto-secondary-nav button.quarto-btn-toggle{margin-left:-0.75rem;margin-right:.15rem}.quarto-secondary-nav nav.quarto-title-breadcrumbs{display:none}.quarto-secondary-nav nav.quarto-page-breadcrumbs{display:flex;align-items:center;padding-right:1em;margin-left:-0.25em}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{text-decoration:none}.quarto-secondary-nav nav.quarto-page-breadcrumbs ol.breadcrumb{margin-bottom:0}}@media(min-width: 992px){.quarto-secondary-nav{display:none}}.quarto-title-breadcrumbs .breadcrumb{margin-bottom:.5em;font-size:.9rem}.quarto-title-breadcrumbs .breadcrumb li:last-of-type a{color:#6c757d}.quarto-secondary-nav .quarto-btn-toggle{color:#595959}.quarto-secondary-nav[aria-expanded=false] .quarto-btn-toggle .bi-chevron-right::before{transform:none}.quarto-secondary-nav[aria-expanded=true] .quarto-btn-toggle .bi-chevron-right::before{transform:rotate(90deg)}.quarto-secondary-nav .quarto-btn-toggle .bi-chevron-right::before{transition:transform 200ms ease}.quarto-secondary-nav{cursor:pointer}.no-decor{text-decoration:none}.quarto-secondary-nav-title{margin-top:.3em;color:#595959;padding-top:4px}.quarto-secondary-nav nav.quarto-page-breadcrumbs{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a:hover{color:rgba(64,64,191,.8)}.quarto-secondary-nav nav.quarto-page-breadcrumbs .breadcrumb-item::before{color:#8c8c8c}.breadcrumb-item{line-height:1.2rem}div.sidebar-item-container{color:#595959}div.sidebar-item-container:hover,div.sidebar-item-container:focus{color:rgba(64,64,191,.8)}div.sidebar-item-container.disabled{color:rgba(89,89,89,.75)}div.sidebar-item-container .active,div.sidebar-item-container .show>.nav-link,div.sidebar-item-container .sidebar-link>code{color:#4040bf}div.sidebar.sidebar-navigation.rollup.quarto-sidebar-toggle-contents,nav.sidebar.sidebar-navigation:not(.rollup){background-color:#fff}@media(max-width: 991.98px){.sidebar-navigation .sidebar-item a,.nav-page .nav-page-text,.sidebar-navigation{font-size:1rem}.sidebar-navigation ul.sidebar-section.depth1 .sidebar-section-item{font-size:1.1rem}.sidebar-logo{display:none}.sidebar.sidebar-navigation{position:static;border-bottom:1px solid #dee2e6}.sidebar.sidebar-navigation.collapsing{position:fixed;z-index:1000}.sidebar.sidebar-navigation.show{position:fixed;z-index:1000}.sidebar.sidebar-navigation{min-height:100%}nav.quarto-secondary-nav{background-color:#fff;border-bottom:1px solid #dee2e6}.quarto-banner nav.quarto-secondary-nav{background-color:#3cdd8c;color:#fff;border-top:1px solid #dee2e6}.sidebar .sidebar-footer{visibility:visible;padding-top:1rem;position:inherit}.sidebar-tools-collapse{display:block}}#quarto-sidebar{transition:width .15s ease-in}#quarto-sidebar>*{padding-right:1em}@media(max-width: 991.98px){#quarto-sidebar .sidebar-menu-container{white-space:nowrap;min-width:225px}#quarto-sidebar.show{transition:width .15s ease-out}}@media(min-width: 992px){#quarto-sidebar{display:flex;flex-direction:column}.nav-page .nav-page-text,.sidebar-navigation .sidebar-section .sidebar-item{font-size:.875rem}.sidebar-navigation .sidebar-item{font-size:.925rem}.sidebar.sidebar-navigation{display:block;position:sticky}.sidebar-search{width:100%}.sidebar .sidebar-footer{visibility:visible}}@media(min-width: 992px){#quarto-sidebar-glass{display:none}}@media(max-width: 991.98px){#quarto-sidebar-glass{position:fixed;top:0;bottom:0;left:0;right:0;background-color:rgba(255,255,255,0);transition:background-color .15s ease-in;z-index:-1}#quarto-sidebar-glass.collapsing{z-index:1000}#quarto-sidebar-glass.show{transition:background-color .15s ease-out;background-color:rgba(102,102,102,.4);z-index:1000}}.sidebar .sidebar-footer{padding:.5rem 1rem;align-self:flex-end;color:#6c757d;width:100%}.quarto-page-breadcrumbs .breadcrumb-item+.breadcrumb-item,.quarto-page-breadcrumbs .breadcrumb-item{padding-right:.33em;padding-left:0}.quarto-page-breadcrumbs .breadcrumb-item::before{padding-right:.33em}.quarto-sidebar-footer{font-size:.875em}.sidebar-section .bi-chevron-right{vertical-align:middle}.sidebar-section .bi-chevron-right::before{font-size:.9em}.notransition{-webkit-transition:none !important;-moz-transition:none !important;-o-transition:none !important;transition:none !important}.btn:focus:not(:focus-visible){box-shadow:none}.page-navigation{display:flex;justify-content:space-between}.nav-page{padding-bottom:.75em}.nav-page .bi{font-size:1.8rem;vertical-align:middle}.nav-page .nav-page-text{padding-left:.25em;padding-right:.25em}.nav-page a{color:#6c757d;text-decoration:none;display:flex;align-items:center}.nav-page a:hover{color:#339}.nav-footer .toc-actions{padding-bottom:.5em;padding-top:.5em}.nav-footer .toc-actions a,.nav-footer .toc-actions a:hover{text-decoration:none}.nav-footer .toc-actions ul{display:flex;list-style:none}.nav-footer .toc-actions ul :first-child{margin-left:auto}.nav-footer .toc-actions ul :last-child{margin-right:auto}.nav-footer .toc-actions ul li{padding-right:1.5em}.nav-footer .toc-actions ul li i.bi{padding-right:.4em}.nav-footer .toc-actions ul li:last-of-type{padding-right:0}.nav-footer{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:baseline;text-align:center;padding-top:.5rem;padding-bottom:.5rem;background-color:#fff}body.nav-fixed{padding-top:64px}.nav-footer-contents{color:#6c757d;margin-top:.25rem}.nav-footer{min-height:3.5em;color:#757575}.nav-footer a{color:#757575}.nav-footer .nav-footer-left{font-size:.825em}.nav-footer .nav-footer-center{font-size:.825em}.nav-footer .nav-footer-right{font-size:.825em}.nav-footer-left .footer-items,.nav-footer-center .footer-items,.nav-footer-right .footer-items{display:inline-flex;padding-top:.3em;padding-bottom:.3em;margin-bottom:0em}.nav-footer-left .footer-items .nav-link,.nav-footer-center .footer-items .nav-link,.nav-footer-right .footer-items .nav-link{padding-left:.6em;padding-right:.6em}@media(min-width: 768px){.nav-footer-left{flex:1 1 0px;text-align:left}}@media(max-width: 575.98px){.nav-footer-left{margin-bottom:1em;flex:100%}}@media(min-width: 768px){.nav-footer-right{flex:1 1 0px;text-align:right}}@media(max-width: 575.98px){.nav-footer-right{margin-bottom:1em;flex:100%}}.nav-footer-center{text-align:center;min-height:3em}@media(min-width: 768px){.nav-footer-center{flex:1 1 0px}}.nav-footer-center .footer-items{justify-content:center}@media(max-width: 767.98px){.nav-footer-center{margin-bottom:1em;flex:100%}}@media(max-width: 767.98px){.nav-footer-center{margin-top:3em;order:10}}.navbar .quarto-reader-toggle.reader .quarto-reader-toggle-btn{background-color:#fff;border-radius:3px}@media(max-width: 991.98px){.quarto-reader-toggle{display:none}}.quarto-reader-toggle.reader.quarto-navigation-tool .quarto-reader-toggle-btn{background-color:#595959;border-radius:3px}.quarto-reader-toggle .quarto-reader-toggle-btn{display:inline-flex;padding-left:.2em;padding-right:.2em;margin-left:-0.2em;margin-right:-0.2em;text-align:center}.navbar .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}#quarto-back-to-top{display:none;position:fixed;bottom:50px;background-color:#fff;border-radius:.25rem;box-shadow:0 .2rem .5rem #6c757d,0 0 .05rem #6c757d;color:#6c757d;text-decoration:none;font-size:.9em;text-align:center;left:50%;padding:.4rem .8rem;transform:translate(-50%, 0)}#quarto-announcement{padding:.5em;display:flex;justify-content:space-between;margin-bottom:0;font-size:.9em}#quarto-announcement .quarto-announcement-content{margin-right:auto}#quarto-announcement .quarto-announcement-content p{margin-bottom:0}#quarto-announcement .quarto-announcement-icon{margin-right:.5em;font-size:1.2em;margin-top:-0.15em}#quarto-announcement .quarto-announcement-action{cursor:pointer}.aa-DetachedSearchButtonQuery{display:none}.aa-DetachedOverlay ul.aa-List,#quarto-search-results ul.aa-List{list-style:none;padding-left:0}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{background-color:#fff;position:absolute;z-index:2000}#quarto-search-results .aa-Panel{max-width:400px}#quarto-search input{font-size:.925rem}@media(min-width: 992px){.navbar #quarto-search{margin-left:.25rem;order:999}}.navbar.navbar-expand-sm #quarto-search,.navbar.navbar-expand-md #quarto-search{order:999}@media(min-width: 992px){.navbar .quarto-navbar-tools{order:900}}@media(min-width: 992px){.navbar .quarto-navbar-tools.tools-end{margin-left:auto !important}}@media(max-width: 991.98px){#quarto-sidebar .sidebar-search{display:none}}#quarto-sidebar .sidebar-search .aa-Autocomplete{width:100%}.navbar .aa-Autocomplete .aa-Form{width:180px}.navbar #quarto-search.type-overlay .aa-Autocomplete{width:40px}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form{background-color:inherit;border:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form:focus-within{box-shadow:none;outline:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper{display:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper:focus-within{display:inherit}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-Label svg,.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-LoadingIndicator svg{width:26px;height:26px;color:#fff;opacity:1}.navbar #quarto-search.type-overlay .aa-Autocomplete svg.aa-SubmitIcon{width:26px;height:26px;color:#fff;opacity:1}.aa-Autocomplete .aa-Form,.aa-DetachedFormContainer .aa-Form{align-items:center;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;color:#000;display:flex;line-height:1em;margin:0;position:relative;width:100%}.aa-Autocomplete .aa-Form:focus-within,.aa-DetachedFormContainer .aa-Form:focus-within{box-shadow:rgba(232,232,252,.6) 0 0 0 1px;outline:currentColor none medium}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix{align-items:center;display:flex;flex-shrink:0;order:1}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{cursor:initial;flex-shrink:0;padding:0;text-align:left}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg{color:#000;opacity:.5}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton{appearance:none;background:none;border:0;margin:0}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{align-items:center;display:flex;justify-content:center}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapper,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper{order:3;position:relative;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input{appearance:none;background:none;border:0;color:#000;font:inherit;height:calc(1.5em + .1rem + 2px);padding:0;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::placeholder,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::placeholder{color:#000;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input:focus{border-color:none;box-shadow:none;outline:none}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix{align-items:center;display:flex;order:4}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton{align-items:center;background:none;border:0;color:#000;opacity:.8;cursor:pointer;display:flex;margin:0;width:calc(1.5em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus{color:#000;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg{width:calc(1.5em + 0.75rem + calc(1px * 2))}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton{border:none;align-items:center;background:none;color:#000;opacity:.4;font-size:.7rem;cursor:pointer;display:none;margin:0;width:calc(1em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus{color:#000;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden]{display:none}.aa-PanelLayout:empty{display:none}.quarto-search-no-results.no-query{display:none}.aa-Source:has(.no-query){display:none}#quarto-search-results .aa-Panel{border:solid #dee2e6 1px}#quarto-search-results .aa-SourceNoResults{width:398px}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{max-height:65vh;overflow-y:auto;font-size:.925rem}.aa-DetachedOverlay .aa-SourceNoResults,#quarto-search-results .aa-SourceNoResults{height:60px;display:flex;justify-content:center;align-items:center}.aa-DetachedOverlay .search-error,#quarto-search-results .search-error{padding-top:10px;padding-left:20px;padding-right:20px;cursor:default}.aa-DetachedOverlay .search-error .search-error-title,#quarto-search-results .search-error .search-error-title{font-size:1.1rem;margin-bottom:.5rem}.aa-DetachedOverlay .search-error .search-error-title .search-error-icon,#quarto-search-results .search-error .search-error-title .search-error-icon{margin-right:8px}.aa-DetachedOverlay .search-error .search-error-text,#quarto-search-results .search-error .search-error-text{font-weight:300}.aa-DetachedOverlay .search-result-text,#quarto-search-results .search-result-text{font-weight:300;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.2rem;max-height:2.4rem}.aa-DetachedOverlay .aa-SourceHeader .search-result-header,#quarto-search-results .aa-SourceHeader .search-result-header{font-size:.875rem;background-color:#f2f2f2;padding-left:14px;padding-bottom:4px;padding-top:4px}.aa-DetachedOverlay .aa-SourceHeader .search-result-header-no-results,#quarto-search-results .aa-SourceHeader .search-result-header-no-results{display:none}.aa-DetachedOverlay .aa-SourceFooter .algolia-search-logo,#quarto-search-results .aa-SourceFooter .algolia-search-logo{width:110px;opacity:.85;margin:8px;float:right}.aa-DetachedOverlay .search-result-section,#quarto-search-results .search-result-section{font-size:.925em}.aa-DetachedOverlay a.search-result-link,#quarto-search-results a.search-result-link{color:inherit;text-decoration:none}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item,#quarto-search-results li.aa-Item[aria-selected=true] .search-item{background-color:#e8e8fc}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text-container{color:#000;background-color:#e8e8fc}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=true] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-match.mark{color:#000;background-color:#fff}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item,#quarto-search-results li.aa-Item[aria-selected=false] .search-item{background-color:#fff}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text-container{color:#000}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=false] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-match.mark{color:inherit;background-color:#fff}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container{background-color:#fff;color:#000}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container{padding-top:0px}.aa-DetachedOverlay li.aa-Item .search-result-doc.document-selectable .search-result-text-container,#quarto-search-results li.aa-Item .search-result-doc.document-selectable .search-result-text-container{margin-top:-4px}.aa-DetachedOverlay .aa-Item,#quarto-search-results .aa-Item{cursor:pointer}.aa-DetachedOverlay .aa-Item .search-item,#quarto-search-results .aa-Item .search-item{border-left:none;border-right:none;border-top:none;background-color:#fff;border-color:#dee2e6;color:#000}.aa-DetachedOverlay .aa-Item .search-item p,#quarto-search-results .aa-Item .search-item p{margin-top:0;margin-bottom:0}.aa-DetachedOverlay .aa-Item .search-item i.bi,#quarto-search-results .aa-Item .search-item i.bi{padding-left:8px;padding-right:8px;font-size:1.3em}.aa-DetachedOverlay .aa-Item .search-item .search-result-title,#quarto-search-results .aa-Item .search-item .search-result-title{margin-top:.3em;margin-bottom:0em}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs,#quarto-search-results .aa-Item .search-item .search-result-crumbs{white-space:nowrap;text-overflow:ellipsis;font-size:.8em;font-weight:300;margin-right:1em}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs:not(.search-result-crumbs-wrap),#quarto-search-results .aa-Item .search-item .search-result-crumbs:not(.search-result-crumbs-wrap){max-width:30%;margin-left:auto;margin-top:.5em;margin-bottom:.1rem}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs.search-result-crumbs-wrap,#quarto-search-results .aa-Item .search-item .search-result-crumbs.search-result-crumbs-wrap{flex-basis:100%;margin-top:0em;margin-bottom:.2em;margin-left:37px}.aa-DetachedOverlay .aa-Item .search-result-title-container,#quarto-search-results .aa-Item .search-result-title-container{font-size:1em;display:flex;flex-wrap:wrap;padding:6px 4px 6px 4px}.aa-DetachedOverlay .aa-Item .search-result-text-container,#quarto-search-results .aa-Item .search-result-text-container{padding-bottom:8px;padding-right:8px;margin-left:42px}.aa-DetachedOverlay .aa-Item .search-result-doc-section,.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-doc-section,#quarto-search-results .aa-Item .search-result-more{padding-top:8px;padding-bottom:8px;padding-left:44px}.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-more{font-size:.8em;font-weight:400}.aa-DetachedOverlay .aa-Item .search-result-doc,#quarto-search-results .aa-Item .search-result-doc{border-top:1px solid #dee2e6}.aa-DetachedSearchButton{background:none;border:none}.aa-DetachedSearchButton .aa-DetachedSearchButtonPlaceholder{display:none}.navbar .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#fff}.sidebar-tools-collapse #quarto-search,.sidebar-tools-main #quarto-search{display:inline}.sidebar-tools-collapse #quarto-search .aa-Autocomplete,.sidebar-tools-main #quarto-search .aa-Autocomplete{display:inline}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton{padding-left:4px;padding-right:4px}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#595959}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon{margin-top:-3px}.aa-DetachedContainer{background:rgba(255,255,255,.65);width:90%;bottom:0;box-shadow:rgba(222,226,230,.6) 0 0 0 1px;outline:currentColor none medium;display:flex;flex-direction:column;left:0;margin:0;overflow:hidden;padding:0;position:fixed;right:0;top:0;z-index:1101}.aa-DetachedContainer::after{height:32px}.aa-DetachedContainer .aa-SourceHeader{margin:var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px}.aa-DetachedContainer .aa-Panel{background-color:#fff;border-radius:0;box-shadow:none;flex-grow:1;margin:0;padding:0;position:relative}.aa-DetachedContainer .aa-PanelLayout{bottom:0;box-shadow:none;left:0;margin:0;max-height:none;overflow-y:auto;position:absolute;right:0;top:0;width:100%}.aa-DetachedFormContainer{background-color:#fff;border-bottom:1px solid #dee2e6;display:flex;flex-direction:row;justify-content:space-between;margin:0;padding:.5em}.aa-DetachedCancelButton{background:none;font-size:.8em;border:0;border-radius:3px;color:#000;cursor:pointer;margin:0 0 0 .5em;padding:0 .5em}.aa-DetachedCancelButton:hover,.aa-DetachedCancelButton:focus{box-shadow:rgba(232,232,252,.6) 0 0 0 1px;outline:currentColor none medium}.aa-DetachedContainer--modal{bottom:inherit;height:auto;margin:0 auto;position:absolute;top:100px;border-radius:6px;max-width:850px}@media(max-width: 575.98px){.aa-DetachedContainer--modal{width:100%;top:0px;border-radius:0px;border:none}}.aa-DetachedContainer--modal .aa-PanelLayout{max-height:var(--aa-detached-modal-max-height);padding-bottom:var(--aa-spacing-half);position:static}.aa-Detached{height:100vh;overflow:hidden}.aa-DetachedOverlay{background-color:rgba(0,0,0,.4);position:fixed;left:0;right:0;top:0;margin:0;padding:0;height:100vh;z-index:1100}.quarto-dashboard.nav-fixed.dashboard-sidebar #quarto-content.quarto-dashboard-content{padding:0em}.quarto-dashboard #quarto-content.quarto-dashboard-content{padding:1em}.quarto-dashboard #quarto-content.quarto-dashboard-content>*{padding-top:0}@media(min-width: 576px){.quarto-dashboard{height:100%}}.quarto-dashboard .card.valuebox.bslib-card.bg-primary{background-color:#5397e9 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-secondary{background-color:#343a40 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-success{background-color:#3aa716 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-info{background-color:rgba(153,84,187,.7019607843) !important}.quarto-dashboard .card.valuebox.bslib-card.bg-warning{background-color:#fa6400 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-danger{background-color:rgba(255,0,57,.7019607843) !important}.quarto-dashboard .card.valuebox.bslib-card.bg-light{background-color:#f8f9fa !important}.quarto-dashboard .card.valuebox.bslib-card.bg-dark{background-color:#343a40 !important}.quarto-dashboard.dashboard-fill{display:flex;flex-direction:column}.quarto-dashboard #quarto-appendix{display:none}.quarto-dashboard #quarto-header #quarto-dashboard-header{border-top:solid 1px #22c472;border-bottom:solid 1px #22c472}.quarto-dashboard #quarto-header #quarto-dashboard-header>nav{padding-left:1em;padding-right:1em}.quarto-dashboard #quarto-header #quarto-dashboard-header>nav .navbar-brand-container{padding-left:0}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-toggler{margin-right:0}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-toggler-icon{height:1em;width:1em;background-image:url('data:image/svg+xml,')}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-brand-container{padding-right:1em}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-title{font-size:1.1em}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-nav{font-size:.9em}.quarto-dashboard #quarto-dashboard-header .navbar{padding:0}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-container{padding-left:1em}.quarto-dashboard #quarto-dashboard-header .navbar.slim .navbar-brand-container .nav-link,.quarto-dashboard #quarto-dashboard-header .navbar.slim .navbar-nav .nav-link{padding:.7em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-color-scheme-toggle{order:9}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-toggler{margin-left:.5em;order:10}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-nav .nav-link{padding:.5em;height:100%;display:flex;align-items:center}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-nav .active{background-color:#24cd78}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-brand-container{padding:.5em .5em .5em 0;display:flex;flex-direction:row;margin-right:2em;align-items:center}@media(max-width: 767.98px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-brand-container{margin-right:auto}}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{align-self:stretch}@media(min-width: 768px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{order:8}}@media(max-width: 767.98px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{order:1000;padding-bottom:.5em}}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse .navbar-nav{align-self:stretch}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title{font-size:1.25em;line-height:1.1em;display:flex;flex-direction:row;flex-wrap:wrap;align-items:baseline}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title .navbar-title-text{margin-right:.4em}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title a{text-decoration:none;color:inherit}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-subtitle,.quarto-dashboard #quarto-dashboard-header .navbar .navbar-author{font-size:.9rem;margin-right:.5em}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-author{margin-left:auto}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-logo{max-height:48px;min-height:30px;object-fit:cover;margin-right:1em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-links{order:9;padding-right:1em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-link-text{margin-left:.25em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-link{padding-right:0em;padding-left:.7em;text-decoration:none;color:#fff}.quarto-dashboard .page-layout-custom .tab-content{padding:0;border:none}.quarto-dashboard-img-contain{height:100%;width:100%;object-fit:contain}@media(max-width: 575.98px){.quarto-dashboard .bslib-grid{grid-template-rows:minmax(1em, max-content) !important}.quarto-dashboard .sidebar-content{height:inherit}.quarto-dashboard .page-layout-custom{min-height:100vh}}.quarto-dashboard.dashboard-toolbar>.page-layout-custom,.quarto-dashboard.dashboard-sidebar>.page-layout-custom{padding:0}.quarto-dashboard .quarto-dashboard-content.quarto-dashboard-pages{padding:0}.quarto-dashboard .callout{margin-bottom:0;margin-top:0}.quarto-dashboard .html-fill-container figure{overflow:hidden}.quarto-dashboard bslib-tooltip .rounded-pill{border:solid #6c757d 1px}.quarto-dashboard bslib-tooltip .rounded-pill .svg{fill:#000}.quarto-dashboard .tabset .dashboard-card-no-title .nav-tabs{margin-left:0;margin-right:auto}.quarto-dashboard .tabset .tab-content{border:none}.quarto-dashboard .tabset .card-header .nav-link[role=tab]{margin-top:-6px;padding-top:6px;padding-bottom:6px}.quarto-dashboard .card.valuebox,.quarto-dashboard .card.bslib-value-box{min-height:3rem}.quarto-dashboard .card.valuebox .card-body,.quarto-dashboard .card.bslib-value-box .card-body{padding:0}.quarto-dashboard .bslib-value-box .value-box-value{font-size:clamp(.1em,15cqw,5em)}.quarto-dashboard .bslib-value-box .value-box-showcase .bi{font-size:clamp(.1em,max(18cqw,5.2cqh),5em);text-align:center;height:1em}.quarto-dashboard .bslib-value-box .value-box-showcase .bi::before{vertical-align:1em}.quarto-dashboard .bslib-value-box .value-box-area{margin-top:auto;margin-bottom:auto}.quarto-dashboard .card figure.quarto-float{display:flex;flex-direction:column;align-items:center}.quarto-dashboard .dashboard-scrolling{padding:1em}.quarto-dashboard .full-height{height:100%}.quarto-dashboard .showcase-bottom .value-box-grid{display:grid;grid-template-columns:1fr;grid-template-rows:1fr auto;grid-template-areas:"top" "bottom"}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-showcase{grid-area:bottom;padding:0;margin:0}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-showcase i.bi{font-size:4rem}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-area{grid-area:top}.quarto-dashboard .tab-content{margin-bottom:0}.quarto-dashboard .bslib-card .bslib-navs-card-title{justify-content:stretch;align-items:end}.quarto-dashboard .card-header{display:flex;flex-wrap:wrap;justify-content:space-between}.quarto-dashboard .card-header .card-title{display:flex;flex-direction:column;justify-content:center;margin-bottom:0}.quarto-dashboard .tabset .card-toolbar{margin-bottom:1em}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout{border:none;gap:var(--bslib-spacer, 1rem)}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.main{padding:0}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.sidebar{border-radius:.25rem;border:1px solid rgba(0,0,0,.175)}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.collapse-toggle{display:none}@media(max-width: 767.98px){.quarto-dashboard .bslib-grid>.bslib-sidebar-layout{grid-template-columns:1fr;grid-template-rows:max-content 1fr}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.main{grid-column:1;grid-row:2}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout .sidebar{grid-column:1;grid-row:1}}.quarto-dashboard .sidebar-right .sidebar{padding-left:2.5em}.quarto-dashboard .sidebar-right .collapse-toggle{left:2px}.quarto-dashboard .quarto-dashboard .sidebar-right button.collapse-toggle:not(.transitioning){left:unset}.quarto-dashboard aside.sidebar{padding-left:1em;padding-right:1em;background-color:rgba(52,58,64,.25);color:#000}.quarto-dashboard .bslib-sidebar-layout>div.main{padding:.7em}.quarto-dashboard .bslib-sidebar-layout button.collapse-toggle{margin-top:.3em}.quarto-dashboard .bslib-sidebar-layout .collapse-toggle{top:0}.quarto-dashboard .bslib-sidebar-layout.sidebar-collapsed:not(.transitioning):not(.sidebar-right) .collapse-toggle{left:2px}.quarto-dashboard .sidebar>section>.h3:first-of-type{margin-top:0em}.quarto-dashboard .sidebar .h3,.quarto-dashboard .sidebar .h4,.quarto-dashboard .sidebar .h5,.quarto-dashboard .sidebar .h6{margin-top:.5em}.quarto-dashboard .sidebar form{flex-direction:column;align-items:start;margin-bottom:1em}.quarto-dashboard .sidebar form div[class*=oi-][class$=-input]{flex-direction:column}.quarto-dashboard .sidebar form[class*=oi-][class$=-toggle]{flex-direction:row-reverse;align-items:center;justify-content:start}.quarto-dashboard .sidebar form input[type=range]{margin-top:.5em;margin-right:.8em;margin-left:1em}.quarto-dashboard .sidebar label{width:fit-content}.quarto-dashboard .sidebar .card-body{margin-bottom:2em}.quarto-dashboard .sidebar .shiny-input-container{margin-bottom:1em}.quarto-dashboard .sidebar .shiny-options-group{margin-top:0}.quarto-dashboard .sidebar .control-label{margin-bottom:.3em}.quarto-dashboard .card .card-body .quarto-layout-row{align-items:stretch}.quarto-dashboard .toolbar{font-size:.9em;display:flex;flex-direction:row;border-top:solid 1px #bcbfc0;padding:1em;flex-wrap:wrap;background-color:rgba(52,58,64,.25)}.quarto-dashboard .toolbar .cell-output-display{display:flex}.quarto-dashboard .toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .toolbar>*:last-child{margin-right:0}.quarto-dashboard .toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .toolbar .shiny-input-container{padding-bottom:0;margin-bottom:0}.quarto-dashboard .toolbar .shiny-input-container>*{flex-shrink:0;flex-grow:0}.quarto-dashboard .toolbar .form-group.shiny-input-container:not([role=group])>label{margin-bottom:0}.quarto-dashboard .toolbar .shiny-input-container.no-baseline{align-items:start;padding-top:6px}.quarto-dashboard .toolbar .shiny-input-container{display:flex;align-items:baseline}.quarto-dashboard .toolbar .shiny-input-container label{padding-right:.4em}.quarto-dashboard .toolbar .shiny-input-container .bslib-input-switch{margin-top:6px}.quarto-dashboard .toolbar input[type=text]{line-height:1;width:inherit}.quarto-dashboard .toolbar .input-daterange{width:inherit}.quarto-dashboard .toolbar .input-daterange input[type=text]{height:2.4em;width:10em}.quarto-dashboard .toolbar .input-daterange .input-group-addon{height:auto;padding:0;margin-left:-5px !important;margin-right:-5px}.quarto-dashboard .toolbar .input-daterange .input-group-addon .input-group-text{padding-top:0;padding-bottom:0;height:100%}.quarto-dashboard .toolbar span.irs.irs--shiny{width:10em}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-line{top:9px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-min,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-max,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-from,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-to,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-single{top:20px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-bar{top:8px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-handle{top:0px}.quarto-dashboard .toolbar .shiny-input-checkboxgroup>label{margin-top:6px}.quarto-dashboard .toolbar .shiny-input-checkboxgroup>.shiny-options-group{margin-top:0;align-items:baseline}.quarto-dashboard .toolbar .shiny-input-radiogroup>label{margin-top:6px}.quarto-dashboard .toolbar .shiny-input-radiogroup>.shiny-options-group{align-items:baseline;margin-top:0}.quarto-dashboard .toolbar .shiny-input-radiogroup>.shiny-options-group>.radio{margin-right:.3em}.quarto-dashboard .toolbar .form-select{padding-top:.2em;padding-bottom:.2em}.quarto-dashboard .toolbar .shiny-input-select{min-width:6em}.quarto-dashboard .toolbar div.checkbox{margin-bottom:0px}.quarto-dashboard .toolbar>.checkbox:first-child{margin-top:6px}.quarto-dashboard .toolbar form{width:fit-content}.quarto-dashboard .toolbar form label{padding-top:.2em;padding-bottom:.2em;width:fit-content}.quarto-dashboard .toolbar form input[type=date]{width:fit-content}.quarto-dashboard .toolbar form input[type=color]{width:3em}.quarto-dashboard .toolbar form button{padding:.4em}.quarto-dashboard .toolbar form select{width:fit-content}.quarto-dashboard .toolbar>*{font-size:.9em;flex-grow:0}.quarto-dashboard .toolbar .shiny-input-container label{margin-bottom:1px}.quarto-dashboard .toolbar-bottom{margin-top:1em;margin-bottom:0 !important;order:2}.quarto-dashboard .quarto-dashboard-content>.dashboard-toolbar-container>.toolbar-content>.tab-content>.tab-pane>*:not(.bslib-sidebar-layout){padding:1em}.quarto-dashboard .quarto-dashboard-content>.dashboard-toolbar-container>.toolbar-content>*:not(.tab-content){padding:1em}.quarto-dashboard .quarto-dashboard-content>.tab-content>.dashboard-page>.dashboard-toolbar-container>.toolbar-content,.quarto-dashboard .quarto-dashboard-content>.tab-content>.dashboard-page:not(.dashboard-sidebar-container)>*:not(.dashboard-toolbar-container){padding:1em}.quarto-dashboard .toolbar-content{padding:0}.quarto-dashboard .quarto-dashboard-content.quarto-dashboard-pages .tab-pane>.dashboard-toolbar-container .toolbar{border-radius:0;margin-bottom:0}.quarto-dashboard .dashboard-toolbar-container.toolbar-toplevel .toolbar{border-bottom:1px solid rgba(0,0,0,.175)}.quarto-dashboard .dashboard-toolbar-container.toolbar-toplevel .toolbar-bottom{margin-top:0}.quarto-dashboard .dashboard-toolbar-container:not(.toolbar-toplevel) .toolbar{margin-bottom:1em;border-top:none;border-radius:.25rem;border:1px solid rgba(0,0,0,.175)}.quarto-dashboard .vega-embed.has-actions details{width:1.7em;height:2em;position:absolute !important;top:0;right:0}.quarto-dashboard .dashboard-toolbar-container{padding:0}.quarto-dashboard .card .card-header p:last-child,.quarto-dashboard .card .card-footer p:last-child{margin-bottom:0}.quarto-dashboard .card .card-body>.h4:first-child{margin-top:0}.quarto-dashboard .card .card-body{z-index:4}@media(max-width: 767.98px){.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_length,.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_info,.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_paginate{text-align:initial}.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_filter{text-align:right}.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_paginate ul.pagination{justify-content:initial}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;padding-top:0}.quarto-dashboard .card .card-body .itables .dataTables_wrapper table{flex-shrink:0}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons{margin-bottom:.5em;margin-left:auto;width:fit-content;float:right}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons.btn-group{background:#fff;border:none}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons .btn-secondary{background-color:#fff;background-image:none;border:solid #dee2e6 1px;padding:.2em .7em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons .btn span{font-size:.8em;color:#000}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{margin-left:.5em;margin-bottom:.5em;padding-top:0}@media(min-width: 768px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{font-size:.875em}}@media(max-width: 767.98px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{font-size:.8em}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_filter{margin-bottom:.5em;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_filter input[type=search]{padding:1px 5px 1px 5px;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_length{flex-basis:1 1 50%;margin-bottom:.5em;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_length select{padding:.4em 3em .4em .5em;font-size:.875em;margin-left:.2em;margin-right:.2em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate{flex-shrink:0}@media(min-width: 768px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate{margin-left:auto}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate ul.pagination .paginate_button .page-link{font-size:.8em}.quarto-dashboard .card .card-footer{font-size:.9em}.quarto-dashboard .card .card-toolbar{display:flex;flex-grow:1;flex-direction:row;width:100%;flex-wrap:wrap}.quarto-dashboard .card .card-toolbar>*{font-size:.8em;flex-grow:0}.quarto-dashboard .card .card-toolbar>.card-title{font-size:1em;flex-grow:1;align-self:flex-start;margin-top:.1em}.quarto-dashboard .card .card-toolbar .cell-output-display{display:flex}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .card .card-toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card .card-toolbar>*:last-child{margin-right:0}.quarto-dashboard .card .card-toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .card .card-toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .card .card-toolbar form{width:fit-content}.quarto-dashboard .card .card-toolbar form label{padding-top:.2em;padding-bottom:.2em;width:fit-content}.quarto-dashboard .card .card-toolbar form input[type=date]{width:fit-content}.quarto-dashboard .card .card-toolbar form input[type=color]{width:3em}.quarto-dashboard .card .card-toolbar form button{padding:.4em}.quarto-dashboard .card .card-toolbar form select{width:fit-content}.quarto-dashboard .card .card-toolbar .cell-output-display{display:flex}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .card .card-toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card .card-toolbar>*:last-child{margin-right:0}.quarto-dashboard .card .card-toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .card .card-toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:0;margin-bottom:0}.quarto-dashboard .card .card-toolbar .shiny-input-container>*{flex-shrink:0;flex-grow:0}.quarto-dashboard .card .card-toolbar .form-group.shiny-input-container:not([role=group])>label{margin-bottom:0}.quarto-dashboard .card .card-toolbar .shiny-input-container.no-baseline{align-items:start;padding-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-container{display:flex;align-items:baseline}.quarto-dashboard .card .card-toolbar .shiny-input-container label{padding-right:.4em}.quarto-dashboard .card .card-toolbar .shiny-input-container .bslib-input-switch{margin-top:6px}.quarto-dashboard .card .card-toolbar input[type=text]{line-height:1;width:inherit}.quarto-dashboard .card .card-toolbar .input-daterange{width:inherit}.quarto-dashboard .card .card-toolbar .input-daterange input[type=text]{height:2.4em;width:10em}.quarto-dashboard .card .card-toolbar .input-daterange .input-group-addon{height:auto;padding:0;margin-left:-5px !important;margin-right:-5px}.quarto-dashboard .card .card-toolbar .input-daterange .input-group-addon .input-group-text{padding-top:0;padding-bottom:0;height:100%}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny{width:10em}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-line{top:9px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-min,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-max,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-from,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-to,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-single{top:20px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-bar{top:8px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-handle{top:0px}.quarto-dashboard .card .card-toolbar .shiny-input-checkboxgroup>label{margin-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-checkboxgroup>.shiny-options-group{margin-top:0;align-items:baseline}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>label{margin-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>.shiny-options-group{align-items:baseline;margin-top:0}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>.shiny-options-group>.radio{margin-right:.3em}.quarto-dashboard .card .card-toolbar .form-select{padding-top:.2em;padding-bottom:.2em}.quarto-dashboard .card .card-toolbar .shiny-input-select{min-width:6em}.quarto-dashboard .card .card-toolbar div.checkbox{margin-bottom:0px}.quarto-dashboard .card .card-toolbar>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card-body>table>thead{border-top:none}.quarto-dashboard .card-body>.table>:not(caption)>*>*{background-color:#fff}.tableFloatingHeaderOriginal{background-color:#fff;position:sticky !important;top:0 !important}.dashboard-data-table{margin-top:-1px}div.value-box-area span.observablehq--number{font-size:calc(clamp(.1em,15cqw,5em)*1.25);line-height:1.2;color:inherit;font-family:var(--bs-body-font-family)}.quarto-listing{padding-bottom:1em}.listing-pagination{padding-top:.5em}ul.pagination{float:right;padding-left:8px;padding-top:.5em}ul.pagination li{padding-right:.75em}ul.pagination li.disabled a,ul.pagination li.active a{color:#000;text-decoration:none}ul.pagination li:last-of-type{padding-right:0}.listing-actions-group{display:flex}.quarto-listing-filter{margin-bottom:1em;width:200px;margin-left:auto}.quarto-listing-sort{margin-bottom:1em;margin-right:auto;width:auto}.quarto-listing-sort .input-group-text{font-size:.8em}.input-group-text{border-right:none}.quarto-listing-sort select.form-select{font-size:.8em}.listing-no-matching{text-align:center;padding-top:2em;padding-bottom:3em;font-size:1em}#quarto-margin-sidebar .quarto-listing-category{padding-top:0;font-size:1rem}#quarto-margin-sidebar .quarto-listing-category-title{cursor:pointer;font-weight:600;font-size:1rem}.quarto-listing-category .category{cursor:pointer}.quarto-listing-category .category.active{font-weight:600}.quarto-listing-category.category-cloud{display:flex;flex-wrap:wrap;align-items:baseline}.quarto-listing-category.category-cloud .category{padding-right:5px}.quarto-listing-category.category-cloud .category-cloud-1{font-size:.75em}.quarto-listing-category.category-cloud .category-cloud-2{font-size:.95em}.quarto-listing-category.category-cloud .category-cloud-3{font-size:1.15em}.quarto-listing-category.category-cloud .category-cloud-4{font-size:1.35em}.quarto-listing-category.category-cloud .category-cloud-5{font-size:1.55em}.quarto-listing-category.category-cloud .category-cloud-6{font-size:1.75em}.quarto-listing-category.category-cloud .category-cloud-7{font-size:1.95em}.quarto-listing-category.category-cloud .category-cloud-8{font-size:2.15em}.quarto-listing-category.category-cloud .category-cloud-9{font-size:2.35em}.quarto-listing-category.category-cloud .category-cloud-10{font-size:2.55em}.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-1{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-2{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-3{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-3{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-4{grid-template-columns:repeat(4, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-4{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-4{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-5{grid-template-columns:repeat(5, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-5{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-5{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-6{grid-template-columns:repeat(6, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-6{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-6{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-7{grid-template-columns:repeat(7, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-7{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-7{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-8{grid-template-columns:repeat(8, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-8{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-8{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-9{grid-template-columns:repeat(9, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-9{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-9{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-10{grid-template-columns:repeat(10, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-10{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-10{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-11{grid-template-columns:repeat(11, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-11{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-11{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-12{grid-template-columns:repeat(12, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-12{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-12{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-grid{gap:1.5em}.quarto-grid-item.borderless{border:none}.quarto-grid-item.borderless .listing-categories .listing-category:last-of-type,.quarto-grid-item.borderless .listing-categories .listing-category:first-of-type{padding-left:0}.quarto-grid-item.borderless .listing-categories .listing-category{border:0}.quarto-grid-link{text-decoration:none;color:inherit}.quarto-grid-link:hover{text-decoration:none;color:inherit}.quarto-grid-item h5.title,.quarto-grid-item .title.h5{margin-top:0;margin-bottom:0}.quarto-grid-item .card-footer{display:flex;justify-content:space-between;font-size:.8em}.quarto-grid-item .card-footer p{margin-bottom:0}.quarto-grid-item p.card-img-top{margin-bottom:0}.quarto-grid-item p.card-img-top>img{object-fit:cover}.quarto-grid-item .card-other-values{margin-top:.5em;font-size:.8em}.quarto-grid-item .card-other-values tr{margin-bottom:.5em}.quarto-grid-item .card-other-values tr>td:first-of-type{font-weight:600;padding-right:1em;padding-left:1em;vertical-align:top}.quarto-grid-item div.post-contents{display:flex;flex-direction:column;text-decoration:none;height:100%}.quarto-grid-item .listing-item-img-placeholder{background-color:rgba(52,58,64,.25);flex-shrink:0}.quarto-grid-item .card-attribution{padding-top:1em;display:flex;gap:1em;text-transform:uppercase;color:#6c757d;font-weight:500;flex-grow:10;align-items:flex-end}.quarto-grid-item .description{padding-bottom:1em}.quarto-grid-item .card-attribution .date{align-self:flex-end}.quarto-grid-item .card-attribution.justify{justify-content:space-between}.quarto-grid-item .card-attribution.start{justify-content:flex-start}.quarto-grid-item .card-attribution.end{justify-content:flex-end}.quarto-grid-item .card-title{margin-bottom:.1em}.quarto-grid-item .card-subtitle{padding-top:.25em}.quarto-grid-item .card-text{font-size:.9em}.quarto-grid-item .listing-reading-time{padding-bottom:.25em}.quarto-grid-item .card-text-small{font-size:.8em}.quarto-grid-item .card-subtitle.subtitle{font-size:.9em;font-weight:600;padding-bottom:.5em}.quarto-grid-item .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}.quarto-grid-item .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}.quarto-grid-item.card-right{text-align:right}.quarto-grid-item.card-right .listing-categories{justify-content:flex-end}.quarto-grid-item.card-left{text-align:left}.quarto-grid-item.card-center{text-align:center}.quarto-grid-item.card-center .listing-description{text-align:justify}.quarto-grid-item.card-center .listing-categories{justify-content:center}table.quarto-listing-table td.image{padding:0px}table.quarto-listing-table td.image img{width:100%;max-width:50px;object-fit:contain}table.quarto-listing-table a{text-decoration:none;word-break:keep-all}table.quarto-listing-table th a{color:inherit}table.quarto-listing-table th a.asc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table th a.desc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table.table-hover td{cursor:pointer}.quarto-post.image-left{flex-direction:row}.quarto-post.image-right{flex-direction:row-reverse}@media(max-width: 767.98px){.quarto-post.image-right,.quarto-post.image-left{gap:0em;flex-direction:column}.quarto-post .metadata{padding-bottom:1em;order:2}.quarto-post .body{order:1}.quarto-post .thumbnail{order:3}}.list.quarto-listing-default div:last-of-type{border-bottom:none}@media(min-width: 992px){.quarto-listing-container-default{margin-right:2em}}div.quarto-post{display:flex;gap:2em;margin-bottom:1.5em;border-bottom:1px solid #dee2e6}@media(max-width: 767.98px){div.quarto-post{padding-bottom:1em}}div.quarto-post .metadata{flex-basis:20%;flex-grow:0;margin-top:.2em;flex-shrink:10}div.quarto-post .thumbnail{flex-basis:30%;flex-grow:0;flex-shrink:0}div.quarto-post .thumbnail img{margin-top:.4em;width:100%;object-fit:cover}div.quarto-post .body{flex-basis:45%;flex-grow:1;flex-shrink:0}div.quarto-post .body h3.listing-title,div.quarto-post .body .listing-title.h3{margin-top:0px;margin-bottom:0px;border-bottom:none}div.quarto-post .body .listing-subtitle{font-size:.875em;margin-bottom:.5em;margin-top:.2em}div.quarto-post .body .description{font-size:.9em}div.quarto-post .body pre code{white-space:pre-wrap}div.quarto-post a{color:#000;text-decoration:none}div.quarto-post .metadata{display:flex;flex-direction:column;font-size:.8em;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";flex-basis:33%}div.quarto-post .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}div.quarto-post .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}div.quarto-post .listing-description{margin-bottom:.5em}div.quarto-about-jolla{display:flex !important;flex-direction:column;align-items:center;margin-top:10%;padding-bottom:1em}div.quarto-about-jolla .about-image{object-fit:cover;margin-left:auto;margin-right:auto;margin-bottom:1.5em}div.quarto-about-jolla img.round{border-radius:50%}div.quarto-about-jolla img.rounded{border-radius:10px}div.quarto-about-jolla .quarto-title h1.title,div.quarto-about-jolla .quarto-title .title.h1{text-align:center}div.quarto-about-jolla .quarto-title .description{text-align:center}div.quarto-about-jolla h2,div.quarto-about-jolla .h2{border-bottom:none}div.quarto-about-jolla .about-sep{width:60%}div.quarto-about-jolla main{text-align:center}div.quarto-about-jolla .about-links{display:flex}@media(min-width: 992px){div.quarto-about-jolla .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-jolla .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-jolla .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-jolla .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-jolla .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-jolla .about-link:hover{color:#4040bf}div.quarto-about-jolla .about-link i.bi{margin-right:.15em}div.quarto-about-solana{display:flex !important;flex-direction:column;padding-top:3em !important;padding-bottom:1em}div.quarto-about-solana .about-entity{display:flex !important;align-items:start;justify-content:space-between}@media(min-width: 992px){div.quarto-about-solana .about-entity{flex-direction:row}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity{flex-direction:column-reverse;align-items:center;text-align:center}}div.quarto-about-solana .about-entity .entity-contents{display:flex;flex-direction:column}@media(max-width: 767.98px){div.quarto-about-solana .about-entity .entity-contents{width:100%}}div.quarto-about-solana .about-entity .about-image{object-fit:cover}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-image{margin-bottom:1.5em}}div.quarto-about-solana .about-entity img.round{border-radius:50%}div.quarto-about-solana .about-entity img.rounded{border-radius:10px}div.quarto-about-solana .about-entity .about-links{display:flex;justify-content:left;padding-bottom:1.2em}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-solana .about-entity .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-solana .about-entity .about-link:hover{color:#4040bf}div.quarto-about-solana .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-solana .about-contents{padding-right:1.5em;flex-basis:0;flex-grow:1}div.quarto-about-solana .about-contents main.content{margin-top:0}div.quarto-about-solana .about-contents h2,div.quarto-about-solana .about-contents .h2{border-bottom:none}div.quarto-about-trestles{display:flex !important;flex-direction:row;padding-top:3em !important;padding-bottom:1em}@media(max-width: 991.98px){div.quarto-about-trestles{flex-direction:column;padding-top:0em !important}}div.quarto-about-trestles .about-entity{display:flex !important;flex-direction:column;align-items:center;text-align:center;padding-right:1em}@media(min-width: 992px){div.quarto-about-trestles .about-entity{flex:0 0 42%}}div.quarto-about-trestles .about-entity .about-image{object-fit:cover;margin-bottom:1.5em}div.quarto-about-trestles .about-entity img.round{border-radius:50%}div.quarto-about-trestles .about-entity img.rounded{border-radius:10px}div.quarto-about-trestles .about-entity .about-links{display:flex;justify-content:center}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-trestles .about-entity .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-trestles .about-entity .about-link:hover{color:#4040bf}div.quarto-about-trestles .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-trestles .about-contents{flex-basis:0;flex-grow:1}div.quarto-about-trestles .about-contents h2,div.quarto-about-trestles .about-contents .h2{border-bottom:none}@media(min-width: 992px){div.quarto-about-trestles .about-contents{border-left:solid 1px #dee2e6;padding-left:1.5em}}div.quarto-about-trestles .about-contents main.content{margin-top:0}div.quarto-about-marquee{padding-bottom:1em}div.quarto-about-marquee .about-contents{display:flex;flex-direction:column}div.quarto-about-marquee .about-image{max-height:550px;margin-bottom:1.5em;object-fit:cover}div.quarto-about-marquee img.round{border-radius:50%}div.quarto-about-marquee img.rounded{border-radius:10px}div.quarto-about-marquee h2,div.quarto-about-marquee .h2{border-bottom:none}div.quarto-about-marquee .about-links{display:flex;justify-content:center;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-marquee .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-marquee .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-marquee .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-marquee .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-marquee .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-marquee .about-link:hover{color:#4040bf}div.quarto-about-marquee .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-marquee .about-link{border:none}}div.quarto-about-broadside{display:flex;flex-direction:column;padding-bottom:1em}div.quarto-about-broadside .about-main{display:flex !important;padding-top:0 !important}@media(min-width: 992px){div.quarto-about-broadside .about-main{flex-direction:row;align-items:flex-start}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main{flex-direction:column}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main .about-entity{flex-shrink:0;width:100%;height:450px;margin-bottom:1.5em;background-size:cover;background-repeat:no-repeat}}@media(min-width: 992px){div.quarto-about-broadside .about-main .about-entity{flex:0 10 50%;margin-right:1.5em;width:100%;height:100%;background-size:100%;background-repeat:no-repeat}}div.quarto-about-broadside .about-main .about-contents{padding-top:14px;flex:0 0 50%}div.quarto-about-broadside h2,div.quarto-about-broadside .h2{border-bottom:none}div.quarto-about-broadside .about-sep{margin-top:1.5em;width:60%;align-self:center}div.quarto-about-broadside .about-links{display:flex;justify-content:center;column-gap:20px;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-broadside .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-broadside .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-broadside .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-broadside .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-broadside .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-broadside .about-link:hover{color:#4040bf}div.quarto-about-broadside .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-broadside .about-link{border:none}}.tippy-box[data-theme~=quarto]{background-color:#fff;border:solid 1px #dee2e6;border-radius:.25rem;color:#000;font-size:.875rem}.tippy-box[data-theme~=quarto]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=quarto]>.tippy-arrow:after,.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{content:"";position:absolute;z-index:-1}.tippy-box[data-theme~=quarto]>.tippy-arrow:after{border-color:rgba(0,0,0,0);border-style:solid}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-6px}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-6px}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-6px}.tippy-box[data-placement^=left]>.tippy-arrow:before{right:-6px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:after{border-top-color:#dee2e6;border-width:7px 7px 0;top:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow>svg{top:16px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow:after{top:17px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff;bottom:16px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:after{border-bottom-color:#dee2e6;border-width:0 7px 7px;bottom:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:15px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow:after{bottom:17px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:after{border-left-color:#dee2e6;border-width:7px 0 7px 7px;left:17px;top:1px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow>svg{left:11px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow:after{left:12px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff;right:16px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:after{border-width:7px 7px 7px 0;right:17px;top:1px;border-right-color:#dee2e6}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow>svg{right:11px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow:after{right:12px}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow{fill:#000}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMCA2czEuNzk2LS4wMTMgNC42Ny0zLjYxNUM1Ljg1MS45IDYuOTMuMDA2IDggMGMxLjA3LS4wMDYgMi4xNDguODg3IDMuMzQzIDIuMzg1QzE0LjIzMyA2LjAwNSAxNiA2IDE2IDZIMHoiIGZpbGw9InJnYmEoMCwgOCwgMTYsIDAuMikiLz48L3N2Zz4=);background-size:16px 6px;width:16px;height:6px}.top-right{position:absolute;top:1em;right:1em}.visually-hidden{border:0;clip:rect(0 0 0 0);height:auto;margin:0;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap}.hidden{display:none !important}.zindex-bottom{z-index:-1 !important}figure.figure{display:block}.quarto-layout-panel{margin-bottom:1em}.quarto-layout-panel>figure{width:100%}.quarto-layout-panel>figure>figcaption,.quarto-layout-panel>.panel-caption{margin-top:10pt}.quarto-layout-panel>.table-caption{margin-top:0px}.table-caption p{margin-bottom:.5em}.quarto-layout-row{display:flex;flex-direction:row;align-items:flex-start}.quarto-layout-valign-top{align-items:flex-start}.quarto-layout-valign-bottom{align-items:flex-end}.quarto-layout-valign-center{align-items:center}.quarto-layout-cell{position:relative;margin-right:20px}.quarto-layout-cell:last-child{margin-right:0}.quarto-layout-cell figure,.quarto-layout-cell>p{margin:.2em}.quarto-layout-cell img{max-width:100%}.quarto-layout-cell .html-widget{width:100% !important}.quarto-layout-cell div figure p{margin:0}.quarto-layout-cell figure{display:block;margin-inline-start:0;margin-inline-end:0}.quarto-layout-cell table{display:inline-table}.quarto-layout-cell-subref figcaption,figure .quarto-layout-row figure figcaption{text-align:center;font-style:italic}.quarto-figure{position:relative;margin-bottom:1em}.quarto-figure>figure{width:100%;margin-bottom:0}.quarto-figure-left>figure>p,.quarto-figure-left>figure>div{text-align:left}.quarto-figure-center>figure>p,.quarto-figure-center>figure>div{text-align:center}.quarto-figure-right>figure>p,.quarto-figure-right>figure>div{text-align:right}.quarto-figure>figure>div.cell-annotation,.quarto-figure>figure>div code{text-align:left}figure>p:empty{display:none}figure>p:first-child{margin-top:0;margin-bottom:0}figure>figcaption.quarto-float-caption-bottom{margin-bottom:.5em}figure>figcaption.quarto-float-caption-top{margin-top:.5em}div[id^=tbl-]{position:relative}.quarto-figure>.anchorjs-link{position:absolute;top:.6em;right:.5em}div[id^=tbl-]>.anchorjs-link{position:absolute;top:.7em;right:.3em}.quarto-figure:hover>.anchorjs-link,div[id^=tbl-]:hover>.anchorjs-link,h2:hover>.anchorjs-link,.h2:hover>.anchorjs-link,h3:hover>.anchorjs-link,.h3:hover>.anchorjs-link,h4:hover>.anchorjs-link,.h4:hover>.anchorjs-link,h5:hover>.anchorjs-link,.h5:hover>.anchorjs-link,h6:hover>.anchorjs-link,.h6:hover>.anchorjs-link,.reveal-anchorjs-link>.anchorjs-link{opacity:1}#title-block-header{margin-block-end:1rem;position:relative;margin-top:-1px}#title-block-header .abstract{margin-block-start:1rem}#title-block-header .abstract .abstract-title{font-weight:600}#title-block-header a{text-decoration:none}#title-block-header .author,#title-block-header .date,#title-block-header .doi{margin-block-end:.2rem}#title-block-header .quarto-title-block>div{display:flex}#title-block-header .quarto-title-block>div>h1,#title-block-header .quarto-title-block>div>.h1{flex-grow:1}#title-block-header .quarto-title-block>div>button{flex-shrink:0;height:2.25rem;margin-top:0}@media(min-width: 992px){#title-block-header .quarto-title-block>div>button{margin-top:5px}}tr.header>th>p:last-of-type{margin-bottom:0px}table,table.table{margin-top:.5rem;margin-bottom:.5rem}caption,.table-caption{padding-top:.5rem;padding-bottom:.5rem;text-align:center}figure.quarto-float-tbl figcaption.quarto-float-caption-top{margin-top:.5rem;margin-bottom:.25rem;text-align:center}figure.quarto-float-tbl figcaption.quarto-float-caption-bottom{padding-top:.25rem;margin-bottom:.5rem;text-align:center}.utterances{max-width:none;margin-left:-8px}iframe{margin-bottom:1em}details{margin-bottom:1em}details[show]{margin-bottom:0}details>summary{color:#6c757d}details>summary>p:only-child{display:inline}pre.sourceCode,code.sourceCode{position:relative}dd code:not(.sourceCode),p code:not(.sourceCode){white-space:pre-wrap}code{white-space:pre}@media print{code{white-space:pre-wrap}}pre>code{display:block}pre>code.sourceCode{white-space:pre}pre>code.sourceCode>span>a:first-child::before{text-decoration:none}pre.code-overflow-wrap>code.sourceCode{white-space:pre-wrap}pre.code-overflow-scroll>code.sourceCode{white-space:pre}code a:any-link{color:inherit;text-decoration:none}code a:hover{color:inherit;text-decoration:underline}ul.task-list{padding-left:1em}[data-tippy-root]{display:inline-block}.tippy-content .footnote-back{display:none}.footnote-back{margin-left:.2em}.tippy-content{overflow-x:auto}.quarto-embedded-source-code{display:none}.quarto-unresolved-ref{font-weight:600}.quarto-cover-image{max-width:35%;float:right;margin-left:30px}.cell-output-display .widget-subarea{margin-bottom:1em}.cell-output-display:not(.no-overflow-x),.knitsql-table:not(.no-overflow-x){overflow-x:auto}.panel-input{margin-bottom:1em}.panel-input>div,.panel-input>div>div{display:inline-block;vertical-align:top;padding-right:12px}.panel-input>p:last-child{margin-bottom:0}.layout-sidebar{margin-bottom:1em}.layout-sidebar .tab-content{border:none}.tab-content>.page-columns.active{display:grid}div.sourceCode>iframe{width:100%;height:300px;margin-bottom:-0.5em}a{text-underline-offset:3px}.callout pre.sourceCode{padding-left:0}div.ansi-escaped-output{font-family:monospace;display:block}/*! +* +* ansi colors from IPython notebook's +* +* we also add `bright-[color]-` synonyms for the `-[color]-intense` classes since +* that seems to be what ansi_up emits +* +*/.ansi-black-fg{color:#3e424d}.ansi-black-bg{background-color:#3e424d}.ansi-black-intense-black,.ansi-bright-black-fg{color:#282c36}.ansi-black-intense-black,.ansi-bright-black-bg{background-color:#282c36}.ansi-red-fg{color:#e75c58}.ansi-red-bg{background-color:#e75c58}.ansi-red-intense-red,.ansi-bright-red-fg{color:#b22b31}.ansi-red-intense-red,.ansi-bright-red-bg{background-color:#b22b31}.ansi-green-fg{color:#00a250}.ansi-green-bg{background-color:#00a250}.ansi-green-intense-green,.ansi-bright-green-fg{color:#007427}.ansi-green-intense-green,.ansi-bright-green-bg{background-color:#007427}.ansi-yellow-fg{color:#ddb62b}.ansi-yellow-bg{background-color:#ddb62b}.ansi-yellow-intense-yellow,.ansi-bright-yellow-fg{color:#b27d12}.ansi-yellow-intense-yellow,.ansi-bright-yellow-bg{background-color:#b27d12}.ansi-blue-fg{color:#208ffb}.ansi-blue-bg{background-color:#208ffb}.ansi-blue-intense-blue,.ansi-bright-blue-fg{color:#0065ca}.ansi-blue-intense-blue,.ansi-bright-blue-bg{background-color:#0065ca}.ansi-magenta-fg{color:#d160c4}.ansi-magenta-bg{background-color:#d160c4}.ansi-magenta-intense-magenta,.ansi-bright-magenta-fg{color:#a03196}.ansi-magenta-intense-magenta,.ansi-bright-magenta-bg{background-color:#a03196}.ansi-cyan-fg{color:#60c6c8}.ansi-cyan-bg{background-color:#60c6c8}.ansi-cyan-intense-cyan,.ansi-bright-cyan-fg{color:#258f8f}.ansi-cyan-intense-cyan,.ansi-bright-cyan-bg{background-color:#258f8f}.ansi-white-fg{color:#c5c1b4}.ansi-white-bg{background-color:#c5c1b4}.ansi-white-intense-white,.ansi-bright-white-fg{color:#a1a6b2}.ansi-white-intense-white,.ansi-bright-white-bg{background-color:#a1a6b2}.ansi-default-inverse-fg{color:#fff}.ansi-default-inverse-bg{background-color:#000}.ansi-bold{font-weight:bold}.ansi-underline{text-decoration:underline}:root{--quarto-body-bg: #FFFFFF;--quarto-body-color: #000000;--quarto-text-muted: #6c757d;--quarto-border-color: #dee2e6;--quarto-border-width: 1px}table.gt_table{color:var(--quarto-body-color);font-size:1em;width:100%;background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_column_spanner_outer{color:var(--quarto-body-color);background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_col_heading{color:var(--quarto-body-color);font-weight:bold;background-color:rgba(0,0,0,0)}table.gt_table thead.gt_col_headings{border-bottom:1px solid currentColor;border-top-width:inherit;border-top-color:var(--quarto-border-color)}table.gt_table thead.gt_col_headings:not(:first-child){border-top-width:1px;border-top-color:var(--quarto-border-color)}table.gt_table td.gt_row{border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-width:0px}table.gt_table tbody.gt_table_body{border-top-width:1px;border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-color:currentColor}div.columns{display:initial;gap:initial}div.column{display:inline-block;overflow-x:initial;vertical-align:top;width:50%}.code-annotation-tip-content{word-wrap:break-word}.code-annotation-container-hidden{display:none !important}dl.code-annotation-container-grid{display:grid;grid-template-columns:min-content auto}dl.code-annotation-container-grid dt{grid-column:1}dl.code-annotation-container-grid dd{grid-column:2}pre.sourceCode.code-annotation-code{padding-right:0}code.sourceCode .code-annotation-anchor{z-index:100;position:relative;float:right;background-color:rgba(0,0,0,0)}input[type=checkbox]{margin-right:.5ch}:root{--mermaid-bg-color: #FFFFFF;--mermaid-edge-color: #343a40;--mermaid-node-fg-color: #000000;--mermaid-fg-color: #000000;--mermaid-fg-color--lighter: #1a1a1a;--mermaid-fg-color--lightest: #333333;--mermaid-font-family: Source Sans Pro, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--mermaid-label-bg-color: #FFFFFF;--mermaid-label-fg-color: #E8E8FC;--mermaid-node-bg-color: rgba(232, 232, 252, 0.1);--mermaid-node-fg-color: #000000}@media print{:root{font-size:11pt}#quarto-sidebar,#TOC,.nav-page{display:none}.page-columns .content{grid-column-start:page-start}.fixed-top{position:relative}.panel-caption,.figure-caption,figcaption{color:#666}}.code-copy-button{position:absolute;top:0;right:0;border:0;margin-top:5px;margin-right:5px;background-color:rgba(0,0,0,0);z-index:3}.code-copy-button:focus{outline:none}.code-copy-button-tooltip{font-size:.75em}pre.sourceCode:hover>.code-copy-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}pre.sourceCode:hover>.code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button-checked:hover>.bi::before{background-image:url('data:image/svg+xml,')}main ol ol,main ul ul,main ol ul,main ul ol{margin-bottom:1em}ul>li:not(:has(>p))>ul,ol>li:not(:has(>p))>ul,ul>li:not(:has(>p))>ol,ol>li:not(:has(>p))>ol{margin-bottom:0}ul>li:not(:has(>p))>ul>li:has(>p),ol>li:not(:has(>p))>ul>li:has(>p),ul>li:not(:has(>p))>ol>li:has(>p),ol>li:not(:has(>p))>ol>li:has(>p){margin-top:1rem}body{margin:0}main.page-columns>header>h1.title,main.page-columns>header>.title.h1{margin-bottom:0}@media(min-width: 992px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] 35px [page-end-inset page-end] 5fr [screen-end-inset] 1.5em}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 3em [body-end] 50px [body-end-outset] minmax(0px, 250px) [page-end-inset] minmax(50px, 100px) [page-end] 1fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 100px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 150px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 991.98px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(1250px - 3em)) [body-content-end body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1.5em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 767.98px){body .page-columns,body.fullcontent:not(.floating):not(.docked) .page-columns,body.slimcontent:not(.floating):not(.docked) .page-columns,body.docked .page-columns,body.docked.slimcontent .page-columns,body.docked.fullcontent .page-columns,body.floating .page-columns,body.floating.slimcontent .page-columns,body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}nav[role=doc-toc]{display:none}}body,.page-row-navigation{grid-template-rows:[page-top] max-content [contents-top] max-content [contents-bottom] max-content [page-bottom]}.page-rows-contents{grid-template-rows:[content-top] minmax(max-content, 1fr) [content-bottom] minmax(60px, max-content) [page-bottom]}.page-full{grid-column:screen-start/screen-end !important}.page-columns>*{grid-column:body-content-start/body-content-end}.page-columns.column-page>*{grid-column:page-start/page-end}.page-columns.column-page-left .page-columns.page-full>*,.page-columns.column-page-left>*{grid-column:page-start/body-content-end}.page-columns.column-page-right .page-columns.page-full>*,.page-columns.column-page-right>*{grid-column:body-content-start/page-end}.page-rows{grid-auto-rows:auto}.header{grid-column:screen-start/screen-end;grid-row:page-top/contents-top}#quarto-content{padding:0;grid-column:screen-start/screen-end;grid-row:contents-top/contents-bottom}body.floating .sidebar.sidebar-navigation{grid-column:page-start/body-start;grid-row:content-top/page-bottom}body.docked .sidebar.sidebar-navigation{grid-column:screen-start/body-start;grid-row:content-top/page-bottom}.sidebar.toc-left{grid-column:page-start/body-start;grid-row:content-top/page-bottom}.sidebar.margin-sidebar{grid-column:body-end/page-end;grid-row:content-top/page-bottom}.page-columns .content{grid-column:body-content-start/body-content-end;grid-row:content-top/content-bottom;align-content:flex-start}.page-columns .page-navigation{grid-column:body-content-start/body-content-end;grid-row:content-bottom/page-bottom}.page-columns .footer{grid-column:screen-start/screen-end;grid-row:contents-bottom/page-bottom}.page-columns .column-body{grid-column:body-content-start/body-content-end}.page-columns .column-body-fullbleed{grid-column:body-start/body-end}.page-columns .column-body-outset{grid-column:body-start-outset/body-end-outset;z-index:998;opacity:.999}.page-columns .column-body-outset table{background:#fff}.page-columns .column-body-outset-left{grid-column:body-start-outset/body-content-end;z-index:998;opacity:.999}.page-columns .column-body-outset-left table{background:#fff}.page-columns .column-body-outset-right{grid-column:body-content-start/body-end-outset;z-index:998;opacity:.999}.page-columns .column-body-outset-right table{background:#fff}.page-columns .column-page{grid-column:page-start/page-end;z-index:998;opacity:.999}.page-columns .column-page table{background:#fff}.page-columns .column-page-inset{grid-column:page-start-inset/page-end-inset;z-index:998;opacity:.999}.page-columns .column-page-inset table{background:#fff}.page-columns .column-page-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-page-inset-left table{background:#fff}.page-columns .column-page-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;opacity:.999}.page-columns .column-page-inset-right figcaption table{background:#fff}.page-columns .column-page-left{grid-column:page-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-page-left table{background:#fff}.page-columns .column-page-right{grid-column:body-content-start/page-end;z-index:998;opacity:.999}.page-columns .column-page-right figcaption table{background:#fff}#quarto-content.page-columns #quarto-margin-sidebar,#quarto-content.page-columns #quarto-sidebar{z-index:1}@media(max-width: 991.98px){#quarto-content.page-columns #quarto-margin-sidebar.collapse,#quarto-content.page-columns #quarto-sidebar.collapse,#quarto-content.page-columns #quarto-margin-sidebar.collapsing,#quarto-content.page-columns #quarto-sidebar.collapsing{z-index:1055}}#quarto-content.page-columns main.column-page,#quarto-content.page-columns main.column-page-right,#quarto-content.page-columns main.column-page-left{z-index:0}.page-columns .column-screen-inset{grid-column:screen-start-inset/screen-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:screen-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/screen-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:screen-start/screen-end;z-index:998;opacity:.999}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:screen-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/screen-end;z-index:998;opacity:.999}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:screen-start/screen-end;padding:1em;background:#f8f9fa;z-index:998;opacity:.999;margin-bottom:1em}.zindex-content{z-index:998;opacity:.999}.zindex-modal{z-index:1055;opacity:.999}.zindex-over-content{z-index:999;opacity:.999}img.img-fluid.column-screen,img.img-fluid.column-screen-inset-shaded,img.img-fluid.column-screen-inset,img.img-fluid.column-screen-inset-left,img.img-fluid.column-screen-inset-right,img.img-fluid.column-screen-left,img.img-fluid.column-screen-right{width:100%}@media(min-width: 992px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-end/page-end !important;z-index:998}.column-sidebar{grid-column:page-start/body-start !important;z-index:998}.column-leftmargin{grid-column:screen-start-inset/body-start !important;z-index:998}.no-row-height{height:1em;overflow:visible}}@media(max-width: 991.98px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-end/page-end !important;z-index:998}.no-row-height{height:1em;overflow:visible}.page-columns.page-full{overflow:visible}.page-columns.toc-left .margin-caption,.page-columns.toc-left div.aside,.page-columns.toc-left aside:not(.footnotes):not(.sidebar),.page-columns.toc-left .column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;opacity:.999}.page-columns.toc-left .no-row-height{height:initial;overflow:initial}}@media(max-width: 767.98px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;opacity:.999}.no-row-height{height:initial;overflow:initial}#quarto-margin-sidebar{display:none}#quarto-sidebar-toc-left{display:none}.hidden-sm{display:none}}.panel-grid{display:grid;grid-template-rows:repeat(1, 1fr);grid-template-columns:repeat(24, 1fr);gap:1em}.panel-grid .g-col-1{grid-column:auto/span 1}.panel-grid .g-col-2{grid-column:auto/span 2}.panel-grid .g-col-3{grid-column:auto/span 3}.panel-grid .g-col-4{grid-column:auto/span 4}.panel-grid .g-col-5{grid-column:auto/span 5}.panel-grid .g-col-6{grid-column:auto/span 6}.panel-grid .g-col-7{grid-column:auto/span 7}.panel-grid .g-col-8{grid-column:auto/span 8}.panel-grid .g-col-9{grid-column:auto/span 9}.panel-grid .g-col-10{grid-column:auto/span 10}.panel-grid .g-col-11{grid-column:auto/span 11}.panel-grid .g-col-12{grid-column:auto/span 12}.panel-grid .g-col-13{grid-column:auto/span 13}.panel-grid .g-col-14{grid-column:auto/span 14}.panel-grid .g-col-15{grid-column:auto/span 15}.panel-grid .g-col-16{grid-column:auto/span 16}.panel-grid .g-col-17{grid-column:auto/span 17}.panel-grid .g-col-18{grid-column:auto/span 18}.panel-grid .g-col-19{grid-column:auto/span 19}.panel-grid .g-col-20{grid-column:auto/span 20}.panel-grid .g-col-21{grid-column:auto/span 21}.panel-grid .g-col-22{grid-column:auto/span 22}.panel-grid .g-col-23{grid-column:auto/span 23}.panel-grid .g-col-24{grid-column:auto/span 24}.panel-grid .g-start-1{grid-column-start:1}.panel-grid .g-start-2{grid-column-start:2}.panel-grid .g-start-3{grid-column-start:3}.panel-grid .g-start-4{grid-column-start:4}.panel-grid .g-start-5{grid-column-start:5}.panel-grid .g-start-6{grid-column-start:6}.panel-grid .g-start-7{grid-column-start:7}.panel-grid .g-start-8{grid-column-start:8}.panel-grid .g-start-9{grid-column-start:9}.panel-grid .g-start-10{grid-column-start:10}.panel-grid .g-start-11{grid-column-start:11}.panel-grid .g-start-12{grid-column-start:12}.panel-grid .g-start-13{grid-column-start:13}.panel-grid .g-start-14{grid-column-start:14}.panel-grid .g-start-15{grid-column-start:15}.panel-grid .g-start-16{grid-column-start:16}.panel-grid .g-start-17{grid-column-start:17}.panel-grid .g-start-18{grid-column-start:18}.panel-grid .g-start-19{grid-column-start:19}.panel-grid .g-start-20{grid-column-start:20}.panel-grid .g-start-21{grid-column-start:21}.panel-grid .g-start-22{grid-column-start:22}.panel-grid .g-start-23{grid-column-start:23}@media(min-width: 576px){.panel-grid .g-col-sm-1{grid-column:auto/span 1}.panel-grid .g-col-sm-2{grid-column:auto/span 2}.panel-grid .g-col-sm-3{grid-column:auto/span 3}.panel-grid .g-col-sm-4{grid-column:auto/span 4}.panel-grid .g-col-sm-5{grid-column:auto/span 5}.panel-grid .g-col-sm-6{grid-column:auto/span 6}.panel-grid .g-col-sm-7{grid-column:auto/span 7}.panel-grid .g-col-sm-8{grid-column:auto/span 8}.panel-grid .g-col-sm-9{grid-column:auto/span 9}.panel-grid .g-col-sm-10{grid-column:auto/span 10}.panel-grid .g-col-sm-11{grid-column:auto/span 11}.panel-grid .g-col-sm-12{grid-column:auto/span 12}.panel-grid .g-col-sm-13{grid-column:auto/span 13}.panel-grid .g-col-sm-14{grid-column:auto/span 14}.panel-grid .g-col-sm-15{grid-column:auto/span 15}.panel-grid .g-col-sm-16{grid-column:auto/span 16}.panel-grid .g-col-sm-17{grid-column:auto/span 17}.panel-grid .g-col-sm-18{grid-column:auto/span 18}.panel-grid .g-col-sm-19{grid-column:auto/span 19}.panel-grid .g-col-sm-20{grid-column:auto/span 20}.panel-grid .g-col-sm-21{grid-column:auto/span 21}.panel-grid .g-col-sm-22{grid-column:auto/span 22}.panel-grid .g-col-sm-23{grid-column:auto/span 23}.panel-grid .g-col-sm-24{grid-column:auto/span 24}.panel-grid .g-start-sm-1{grid-column-start:1}.panel-grid .g-start-sm-2{grid-column-start:2}.panel-grid .g-start-sm-3{grid-column-start:3}.panel-grid .g-start-sm-4{grid-column-start:4}.panel-grid .g-start-sm-5{grid-column-start:5}.panel-grid .g-start-sm-6{grid-column-start:6}.panel-grid .g-start-sm-7{grid-column-start:7}.panel-grid .g-start-sm-8{grid-column-start:8}.panel-grid .g-start-sm-9{grid-column-start:9}.panel-grid .g-start-sm-10{grid-column-start:10}.panel-grid .g-start-sm-11{grid-column-start:11}.panel-grid .g-start-sm-12{grid-column-start:12}.panel-grid .g-start-sm-13{grid-column-start:13}.panel-grid .g-start-sm-14{grid-column-start:14}.panel-grid .g-start-sm-15{grid-column-start:15}.panel-grid .g-start-sm-16{grid-column-start:16}.panel-grid .g-start-sm-17{grid-column-start:17}.panel-grid .g-start-sm-18{grid-column-start:18}.panel-grid .g-start-sm-19{grid-column-start:19}.panel-grid .g-start-sm-20{grid-column-start:20}.panel-grid .g-start-sm-21{grid-column-start:21}.panel-grid .g-start-sm-22{grid-column-start:22}.panel-grid .g-start-sm-23{grid-column-start:23}}@media(min-width: 768px){.panel-grid .g-col-md-1{grid-column:auto/span 1}.panel-grid .g-col-md-2{grid-column:auto/span 2}.panel-grid .g-col-md-3{grid-column:auto/span 3}.panel-grid .g-col-md-4{grid-column:auto/span 4}.panel-grid .g-col-md-5{grid-column:auto/span 5}.panel-grid .g-col-md-6{grid-column:auto/span 6}.panel-grid .g-col-md-7{grid-column:auto/span 7}.panel-grid .g-col-md-8{grid-column:auto/span 8}.panel-grid .g-col-md-9{grid-column:auto/span 9}.panel-grid .g-col-md-10{grid-column:auto/span 10}.panel-grid .g-col-md-11{grid-column:auto/span 11}.panel-grid .g-col-md-12{grid-column:auto/span 12}.panel-grid .g-col-md-13{grid-column:auto/span 13}.panel-grid .g-col-md-14{grid-column:auto/span 14}.panel-grid .g-col-md-15{grid-column:auto/span 15}.panel-grid .g-col-md-16{grid-column:auto/span 16}.panel-grid .g-col-md-17{grid-column:auto/span 17}.panel-grid .g-col-md-18{grid-column:auto/span 18}.panel-grid .g-col-md-19{grid-column:auto/span 19}.panel-grid .g-col-md-20{grid-column:auto/span 20}.panel-grid .g-col-md-21{grid-column:auto/span 21}.panel-grid .g-col-md-22{grid-column:auto/span 22}.panel-grid .g-col-md-23{grid-column:auto/span 23}.panel-grid .g-col-md-24{grid-column:auto/span 24}.panel-grid .g-start-md-1{grid-column-start:1}.panel-grid .g-start-md-2{grid-column-start:2}.panel-grid .g-start-md-3{grid-column-start:3}.panel-grid .g-start-md-4{grid-column-start:4}.panel-grid .g-start-md-5{grid-column-start:5}.panel-grid .g-start-md-6{grid-column-start:6}.panel-grid .g-start-md-7{grid-column-start:7}.panel-grid .g-start-md-8{grid-column-start:8}.panel-grid .g-start-md-9{grid-column-start:9}.panel-grid .g-start-md-10{grid-column-start:10}.panel-grid .g-start-md-11{grid-column-start:11}.panel-grid .g-start-md-12{grid-column-start:12}.panel-grid .g-start-md-13{grid-column-start:13}.panel-grid .g-start-md-14{grid-column-start:14}.panel-grid .g-start-md-15{grid-column-start:15}.panel-grid .g-start-md-16{grid-column-start:16}.panel-grid .g-start-md-17{grid-column-start:17}.panel-grid .g-start-md-18{grid-column-start:18}.panel-grid .g-start-md-19{grid-column-start:19}.panel-grid .g-start-md-20{grid-column-start:20}.panel-grid .g-start-md-21{grid-column-start:21}.panel-grid .g-start-md-22{grid-column-start:22}.panel-grid .g-start-md-23{grid-column-start:23}}@media(min-width: 992px){.panel-grid .g-col-lg-1{grid-column:auto/span 1}.panel-grid .g-col-lg-2{grid-column:auto/span 2}.panel-grid .g-col-lg-3{grid-column:auto/span 3}.panel-grid .g-col-lg-4{grid-column:auto/span 4}.panel-grid .g-col-lg-5{grid-column:auto/span 5}.panel-grid .g-col-lg-6{grid-column:auto/span 6}.panel-grid .g-col-lg-7{grid-column:auto/span 7}.panel-grid .g-col-lg-8{grid-column:auto/span 8}.panel-grid .g-col-lg-9{grid-column:auto/span 9}.panel-grid .g-col-lg-10{grid-column:auto/span 10}.panel-grid .g-col-lg-11{grid-column:auto/span 11}.panel-grid .g-col-lg-12{grid-column:auto/span 12}.panel-grid .g-col-lg-13{grid-column:auto/span 13}.panel-grid .g-col-lg-14{grid-column:auto/span 14}.panel-grid .g-col-lg-15{grid-column:auto/span 15}.panel-grid .g-col-lg-16{grid-column:auto/span 16}.panel-grid .g-col-lg-17{grid-column:auto/span 17}.panel-grid .g-col-lg-18{grid-column:auto/span 18}.panel-grid .g-col-lg-19{grid-column:auto/span 19}.panel-grid .g-col-lg-20{grid-column:auto/span 20}.panel-grid .g-col-lg-21{grid-column:auto/span 21}.panel-grid .g-col-lg-22{grid-column:auto/span 22}.panel-grid .g-col-lg-23{grid-column:auto/span 23}.panel-grid .g-col-lg-24{grid-column:auto/span 24}.panel-grid .g-start-lg-1{grid-column-start:1}.panel-grid .g-start-lg-2{grid-column-start:2}.panel-grid .g-start-lg-3{grid-column-start:3}.panel-grid .g-start-lg-4{grid-column-start:4}.panel-grid .g-start-lg-5{grid-column-start:5}.panel-grid .g-start-lg-6{grid-column-start:6}.panel-grid .g-start-lg-7{grid-column-start:7}.panel-grid .g-start-lg-8{grid-column-start:8}.panel-grid .g-start-lg-9{grid-column-start:9}.panel-grid .g-start-lg-10{grid-column-start:10}.panel-grid .g-start-lg-11{grid-column-start:11}.panel-grid .g-start-lg-12{grid-column-start:12}.panel-grid .g-start-lg-13{grid-column-start:13}.panel-grid .g-start-lg-14{grid-column-start:14}.panel-grid .g-start-lg-15{grid-column-start:15}.panel-grid .g-start-lg-16{grid-column-start:16}.panel-grid .g-start-lg-17{grid-column-start:17}.panel-grid .g-start-lg-18{grid-column-start:18}.panel-grid .g-start-lg-19{grid-column-start:19}.panel-grid .g-start-lg-20{grid-column-start:20}.panel-grid .g-start-lg-21{grid-column-start:21}.panel-grid .g-start-lg-22{grid-column-start:22}.panel-grid .g-start-lg-23{grid-column-start:23}}@media(min-width: 1200px){.panel-grid .g-col-xl-1{grid-column:auto/span 1}.panel-grid .g-col-xl-2{grid-column:auto/span 2}.panel-grid .g-col-xl-3{grid-column:auto/span 3}.panel-grid .g-col-xl-4{grid-column:auto/span 4}.panel-grid .g-col-xl-5{grid-column:auto/span 5}.panel-grid .g-col-xl-6{grid-column:auto/span 6}.panel-grid .g-col-xl-7{grid-column:auto/span 7}.panel-grid .g-col-xl-8{grid-column:auto/span 8}.panel-grid .g-col-xl-9{grid-column:auto/span 9}.panel-grid .g-col-xl-10{grid-column:auto/span 10}.panel-grid .g-col-xl-11{grid-column:auto/span 11}.panel-grid .g-col-xl-12{grid-column:auto/span 12}.panel-grid .g-col-xl-13{grid-column:auto/span 13}.panel-grid .g-col-xl-14{grid-column:auto/span 14}.panel-grid .g-col-xl-15{grid-column:auto/span 15}.panel-grid .g-col-xl-16{grid-column:auto/span 16}.panel-grid .g-col-xl-17{grid-column:auto/span 17}.panel-grid .g-col-xl-18{grid-column:auto/span 18}.panel-grid .g-col-xl-19{grid-column:auto/span 19}.panel-grid .g-col-xl-20{grid-column:auto/span 20}.panel-grid .g-col-xl-21{grid-column:auto/span 21}.panel-grid .g-col-xl-22{grid-column:auto/span 22}.panel-grid .g-col-xl-23{grid-column:auto/span 23}.panel-grid .g-col-xl-24{grid-column:auto/span 24}.panel-grid .g-start-xl-1{grid-column-start:1}.panel-grid .g-start-xl-2{grid-column-start:2}.panel-grid .g-start-xl-3{grid-column-start:3}.panel-grid .g-start-xl-4{grid-column-start:4}.panel-grid .g-start-xl-5{grid-column-start:5}.panel-grid .g-start-xl-6{grid-column-start:6}.panel-grid .g-start-xl-7{grid-column-start:7}.panel-grid .g-start-xl-8{grid-column-start:8}.panel-grid .g-start-xl-9{grid-column-start:9}.panel-grid .g-start-xl-10{grid-column-start:10}.panel-grid .g-start-xl-11{grid-column-start:11}.panel-grid .g-start-xl-12{grid-column-start:12}.panel-grid .g-start-xl-13{grid-column-start:13}.panel-grid .g-start-xl-14{grid-column-start:14}.panel-grid .g-start-xl-15{grid-column-start:15}.panel-grid .g-start-xl-16{grid-column-start:16}.panel-grid .g-start-xl-17{grid-column-start:17}.panel-grid .g-start-xl-18{grid-column-start:18}.panel-grid .g-start-xl-19{grid-column-start:19}.panel-grid .g-start-xl-20{grid-column-start:20}.panel-grid .g-start-xl-21{grid-column-start:21}.panel-grid .g-start-xl-22{grid-column-start:22}.panel-grid .g-start-xl-23{grid-column-start:23}}@media(min-width: 1400px){.panel-grid .g-col-xxl-1{grid-column:auto/span 1}.panel-grid .g-col-xxl-2{grid-column:auto/span 2}.panel-grid .g-col-xxl-3{grid-column:auto/span 3}.panel-grid .g-col-xxl-4{grid-column:auto/span 4}.panel-grid .g-col-xxl-5{grid-column:auto/span 5}.panel-grid .g-col-xxl-6{grid-column:auto/span 6}.panel-grid .g-col-xxl-7{grid-column:auto/span 7}.panel-grid .g-col-xxl-8{grid-column:auto/span 8}.panel-grid .g-col-xxl-9{grid-column:auto/span 9}.panel-grid .g-col-xxl-10{grid-column:auto/span 10}.panel-grid .g-col-xxl-11{grid-column:auto/span 11}.panel-grid .g-col-xxl-12{grid-column:auto/span 12}.panel-grid .g-col-xxl-13{grid-column:auto/span 13}.panel-grid .g-col-xxl-14{grid-column:auto/span 14}.panel-grid .g-col-xxl-15{grid-column:auto/span 15}.panel-grid .g-col-xxl-16{grid-column:auto/span 16}.panel-grid .g-col-xxl-17{grid-column:auto/span 17}.panel-grid .g-col-xxl-18{grid-column:auto/span 18}.panel-grid .g-col-xxl-19{grid-column:auto/span 19}.panel-grid .g-col-xxl-20{grid-column:auto/span 20}.panel-grid .g-col-xxl-21{grid-column:auto/span 21}.panel-grid .g-col-xxl-22{grid-column:auto/span 22}.panel-grid .g-col-xxl-23{grid-column:auto/span 23}.panel-grid .g-col-xxl-24{grid-column:auto/span 24}.panel-grid .g-start-xxl-1{grid-column-start:1}.panel-grid .g-start-xxl-2{grid-column-start:2}.panel-grid .g-start-xxl-3{grid-column-start:3}.panel-grid .g-start-xxl-4{grid-column-start:4}.panel-grid .g-start-xxl-5{grid-column-start:5}.panel-grid .g-start-xxl-6{grid-column-start:6}.panel-grid .g-start-xxl-7{grid-column-start:7}.panel-grid .g-start-xxl-8{grid-column-start:8}.panel-grid .g-start-xxl-9{grid-column-start:9}.panel-grid .g-start-xxl-10{grid-column-start:10}.panel-grid .g-start-xxl-11{grid-column-start:11}.panel-grid .g-start-xxl-12{grid-column-start:12}.panel-grid .g-start-xxl-13{grid-column-start:13}.panel-grid .g-start-xxl-14{grid-column-start:14}.panel-grid .g-start-xxl-15{grid-column-start:15}.panel-grid .g-start-xxl-16{grid-column-start:16}.panel-grid .g-start-xxl-17{grid-column-start:17}.panel-grid .g-start-xxl-18{grid-column-start:18}.panel-grid .g-start-xxl-19{grid-column-start:19}.panel-grid .g-start-xxl-20{grid-column-start:20}.panel-grid .g-start-xxl-21{grid-column-start:21}.panel-grid .g-start-xxl-22{grid-column-start:22}.panel-grid .g-start-xxl-23{grid-column-start:23}}main{margin-top:1em;margin-bottom:1em}h1,.h1,h2,.h2{color:inherit;margin-top:2rem;margin-bottom:1rem;font-weight:600}h1.title,.title.h1{margin-top:0}main.content>section:first-of-type>h2:first-child,main.content>section:first-of-type>.h2:first-child{margin-top:0}h2,.h2{border-bottom:1px solid #dee2e6;padding-bottom:.5rem}h3,.h3{font-weight:600}h3,.h3,h4,.h4{opacity:.9;margin-top:1.5rem}h5,.h5,h6,.h6{opacity:.9}.header-section-number{color:#404040}.nav-link.active .header-section-number{color:inherit}mark,.mark{padding:0em}.panel-caption,.figure-caption,.subfigure-caption,.table-caption,figcaption,caption{font-size:.9rem;color:#404040}.quarto-layout-cell[data-ref-parent] caption{color:#404040}.column-margin figcaption,.margin-caption,div.aside,aside,.column-margin{color:#404040;font-size:.825rem}.panel-caption.margin-caption{text-align:inherit}.column-margin.column-container p{margin-bottom:0}.column-margin.column-container>*:not(.collapse):first-child{padding-bottom:.5em;display:block}.column-margin.column-container>*:not(.collapse):not(:first-child){padding-top:.5em;padding-bottom:.5em;display:block}.column-margin.column-container>*.collapse:not(.show){display:none}@media(min-width: 768px){.column-margin.column-container .callout-margin-content:first-child{margin-top:4.5em}.column-margin.column-container .callout-margin-content-simple:first-child{margin-top:3.5em}}.margin-caption>*{padding-top:.5em;padding-bottom:.5em}@media(max-width: 767.98px){.quarto-layout-row{flex-direction:column}}.nav-tabs .nav-item{margin-top:1px;cursor:pointer}.tab-content{margin-top:0px;border-left:#dee2e6 1px solid;border-right:#dee2e6 1px solid;border-bottom:#dee2e6 1px solid;margin-left:0;padding:1em;margin-bottom:1em}@media(max-width: 767.98px){.layout-sidebar{margin-left:0;margin-right:0}}.panel-sidebar,.panel-sidebar .form-control,.panel-input,.panel-input .form-control,.selectize-dropdown{font-size:.9rem}.panel-sidebar .form-control,.panel-input .form-control{padding-top:.1rem}.tab-pane div.sourceCode{margin-top:0px}.tab-pane>p{padding-top:0}.tab-pane>p:nth-child(1){padding-top:0}.tab-pane>p:last-child{margin-bottom:0}.tab-pane>pre:last-child{margin-bottom:0}.tab-content>.tab-pane:not(.active){display:none !important}div.sourceCode{background-color:rgba(233,236,239,.65);border:1px solid rgba(233,236,239,.65)}pre.sourceCode{background-color:rgba(0,0,0,0)}pre.sourceCode{border:none;font-size:.875em;overflow:visible !important;padding:.4em}div.sourceCode{overflow-y:hidden}.callout div.sourceCode{margin-left:initial}.blockquote{font-size:inherit;padding-left:1rem;padding-right:1.5rem;color:#404040}.blockquote h1:first-child,.blockquote .h1:first-child,.blockquote h2:first-child,.blockquote .h2:first-child,.blockquote h3:first-child,.blockquote .h3:first-child,.blockquote h4:first-child,.blockquote .h4:first-child,.blockquote h5:first-child,.blockquote .h5:first-child{margin-top:0}pre{background-color:initial;padding:initial;border:initial}p pre code:not(.sourceCode),li pre code:not(.sourceCode),pre code:not(.sourceCode){background-color:initial}p code:not(.sourceCode),li code:not(.sourceCode),td code:not(.sourceCode){background-color:#f8f9fa;padding:.2em}nav p code:not(.sourceCode),nav li code:not(.sourceCode),nav td code:not(.sourceCode){background-color:rgba(0,0,0,0);padding:0}td code:not(.sourceCode){white-space:pre-wrap}#quarto-embedded-source-code-modal>.modal-dialog{max-width:1000px;padding-left:1.75rem;padding-right:1.75rem}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body{padding:0}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body div.sourceCode{margin:0;padding:.2rem .2rem;border-radius:0px;border:none}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-header{padding:.7rem}.code-tools-button{font-size:1rem;padding:.15rem .15rem;margin-left:5px;color:#6c757d;background-color:rgba(0,0,0,0);transition:initial;cursor:pointer}.code-tools-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}.code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}.sidebar{will-change:top;transition:top 200ms linear;position:sticky;overflow-y:auto;padding-top:1.2em;max-height:100vh}.sidebar.toc-left,.sidebar.margin-sidebar{top:0px;padding-top:1em}.sidebar.quarto-banner-title-block-sidebar>*{padding-top:1.65em}figure .quarto-notebook-link{margin-top:.5em}.quarto-notebook-link{font-size:.75em;color:#6c757d;margin-bottom:1em;text-decoration:none;display:block}.quarto-notebook-link:hover{text-decoration:underline;color:#4040bf}.quarto-notebook-link::before{display:inline-block;height:.75rem;width:.75rem;margin-bottom:0em;margin-right:.25em;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:.75rem .75rem}.toc-actions i.bi,.quarto-code-links i.bi,.quarto-other-links i.bi,.quarto-alternate-notebooks i.bi,.quarto-alternate-formats i.bi{margin-right:.4em;font-size:.8rem}.quarto-other-links-text-target .quarto-code-links i.bi,.quarto-other-links-text-target .quarto-other-links i.bi{margin-right:.2em}.quarto-other-formats-text-target .quarto-alternate-formats i.bi{margin-right:.1em}.toc-actions i.bi.empty,.quarto-code-links i.bi.empty,.quarto-other-links i.bi.empty,.quarto-alternate-notebooks i.bi.empty,.quarto-alternate-formats i.bi.empty{padding-left:1em}.quarto-notebook h2,.quarto-notebook .h2{border-bottom:none}.quarto-notebook .cell-container{display:flex}.quarto-notebook .cell-container .cell{flex-grow:4}.quarto-notebook .cell-container .cell-decorator{padding-top:1.5em;padding-right:1em;text-align:right}.quarto-notebook .cell-container.code-fold .cell-decorator{padding-top:3em}.quarto-notebook .cell-code code{white-space:pre-wrap}.quarto-notebook .cell .cell-output-stderr pre code,.quarto-notebook .cell .cell-output-stdout pre code{white-space:pre-wrap;overflow-wrap:anywhere}.toc-actions,.quarto-alternate-formats,.quarto-other-links,.quarto-code-links,.quarto-alternate-notebooks{padding-left:0em}.sidebar .toc-actions a,.sidebar .quarto-alternate-formats a,.sidebar .quarto-other-links a,.sidebar .quarto-code-links a,.sidebar .quarto-alternate-notebooks a,.sidebar nav[role=doc-toc] a{text-decoration:none}.sidebar .toc-actions a:hover,.sidebar .quarto-other-links a:hover,.sidebar .quarto-code-links a:hover,.sidebar .quarto-alternate-formats a:hover,.sidebar .quarto-alternate-notebooks a:hover{color:#4040bf}.sidebar .toc-actions h2,.sidebar .toc-actions .h2,.sidebar .quarto-code-links h2,.sidebar .quarto-code-links .h2,.sidebar .quarto-other-links h2,.sidebar .quarto-other-links .h2,.sidebar .quarto-alternate-notebooks h2,.sidebar .quarto-alternate-notebooks .h2,.sidebar .quarto-alternate-formats h2,.sidebar .quarto-alternate-formats .h2,.sidebar nav[role=doc-toc]>h2,.sidebar nav[role=doc-toc]>.h2{font-weight:500;margin-bottom:.2rem;margin-top:.3rem;font-family:inherit;border-bottom:0;padding-bottom:0;padding-top:0px}.sidebar .toc-actions>h2,.sidebar .toc-actions>.h2,.sidebar .quarto-code-links>h2,.sidebar .quarto-code-links>.h2,.sidebar .quarto-other-links>h2,.sidebar .quarto-other-links>.h2,.sidebar .quarto-alternate-notebooks>h2,.sidebar .quarto-alternate-notebooks>.h2,.sidebar .quarto-alternate-formats>h2,.sidebar .quarto-alternate-formats>.h2{font-size:.8rem}.sidebar nav[role=doc-toc]>h2,.sidebar nav[role=doc-toc]>.h2{font-size:.875rem}.sidebar nav[role=doc-toc]>ul a{border-left:1px solid #e9ecef;padding-left:.6rem}.sidebar .toc-actions h2>ul a,.sidebar .toc-actions .h2>ul a,.sidebar .quarto-code-links h2>ul a,.sidebar .quarto-code-links .h2>ul a,.sidebar .quarto-other-links h2>ul a,.sidebar .quarto-other-links .h2>ul a,.sidebar .quarto-alternate-notebooks h2>ul a,.sidebar .quarto-alternate-notebooks .h2>ul a,.sidebar .quarto-alternate-formats h2>ul a,.sidebar .quarto-alternate-formats .h2>ul a{border-left:none;padding-left:.6rem}.sidebar .toc-actions ul a:empty,.sidebar .quarto-code-links ul a:empty,.sidebar .quarto-other-links ul a:empty,.sidebar .quarto-alternate-notebooks ul a:empty,.sidebar .quarto-alternate-formats ul a:empty,.sidebar nav[role=doc-toc]>ul a:empty{display:none}.sidebar .toc-actions ul,.sidebar .quarto-code-links ul,.sidebar .quarto-other-links ul,.sidebar .quarto-alternate-notebooks ul,.sidebar .quarto-alternate-formats ul{padding-left:0;list-style:none}.sidebar nav[role=doc-toc] ul{list-style:none;padding-left:0;list-style:none}.sidebar nav[role=doc-toc]>ul{margin-left:.45em}.quarto-margin-sidebar nav[role=doc-toc]{padding-left:.5em}.sidebar .toc-actions>ul,.sidebar .quarto-code-links>ul,.sidebar .quarto-other-links>ul,.sidebar .quarto-alternate-notebooks>ul,.sidebar .quarto-alternate-formats>ul{font-size:.8rem}.sidebar nav[role=doc-toc]>ul{font-size:.875rem}.sidebar .toc-actions ul li a,.sidebar .quarto-code-links ul li a,.sidebar .quarto-other-links ul li a,.sidebar .quarto-alternate-notebooks ul li a,.sidebar .quarto-alternate-formats ul li a,.sidebar nav[role=doc-toc]>ul li a{line-height:1.1rem;padding-bottom:.2rem;padding-top:.2rem;color:inherit}.sidebar nav[role=doc-toc] ul>li>ul>li>a{padding-left:1.2em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>a{padding-left:2.4em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>a{padding-left:3.6em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:4.8em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:6em}.sidebar nav[role=doc-toc] ul>li>a.active,.sidebar nav[role=doc-toc] ul>li>ul>li>a.active{border-left:1px solid #4040bf;color:#4040bf !important}.sidebar nav[role=doc-toc] ul>li>a:hover,.sidebar nav[role=doc-toc] ul>li>ul>li>a:hover{color:#4040bf !important}kbd,.kbd{color:#000;background-color:#f8f9fa;border:1px solid;border-radius:5px;border-color:#dee2e6}.quarto-appendix-contents div.hanging-indent{margin-left:0em}.quarto-appendix-contents div.hanging-indent div.csl-entry{margin-left:1em;text-indent:-1em}.citation a,.footnote-ref{text-decoration:none}.footnotes ol{padding-left:1em}.tippy-content>*{margin-bottom:.7em}.tippy-content>*:last-child{margin-bottom:0}.callout{margin-top:1.25rem;margin-bottom:1.25rem;border-radius:.25rem;overflow-wrap:break-word}.callout .callout-title-container{overflow-wrap:anywhere}.callout.callout-style-simple{padding:.4em .7em;border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout.callout-style-default{border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout .callout-body-container{flex-grow:1}.callout.callout-style-simple .callout-body{font-size:.9rem;font-weight:400}.callout.callout-style-default .callout-body{font-size:.9rem;font-weight:400}.callout:not(.no-icon).callout-titled.callout-style-simple .callout-body{padding-left:1.6em}.callout.callout-titled>.callout-header{padding-top:.2em;margin-bottom:-0.2em}.callout.callout-style-simple>div.callout-header{border-bottom:none;font-size:.9rem;font-weight:600;opacity:75%}.callout.callout-style-default>div.callout-header{border-bottom:none;font-weight:600;opacity:85%;font-size:.9rem;padding-left:.5em;padding-right:.5em}.callout.callout-style-default .callout-body{padding-left:.5em;padding-right:.5em}.callout.callout-style-default .callout-body>:first-child{padding-top:.5rem;margin-top:0}.callout>div.callout-header[data-bs-toggle=collapse]{cursor:pointer}.callout.callout-style-default .callout-header[aria-expanded=false],.callout.callout-style-default .callout-header[aria-expanded=true]{padding-top:0px;margin-bottom:0px;align-items:center}.callout.callout-titled .callout-body>:last-child:not(.sourceCode),.callout.callout-titled .callout-body>div>:last-child:not(.sourceCode){padding-bottom:.5rem;margin-bottom:0}.callout:not(.callout-titled) .callout-body>:first-child,.callout:not(.callout-titled) .callout-body>div>:first-child{margin-top:.25rem}.callout:not(.callout-titled) .callout-body>:last-child,.callout:not(.callout-titled) .callout-body>div>:last-child{margin-bottom:.2rem}.callout.callout-style-simple .callout-icon::before,.callout.callout-style-simple .callout-toggle::before{height:1rem;width:1rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.callout.callout-style-default .callout-icon::before,.callout.callout-style-default .callout-toggle::before{height:.9rem;width:.9rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:.9rem .9rem}.callout.callout-style-default .callout-toggle::before{margin-top:5px}.callout .callout-btn-toggle .callout-toggle::before{transition:transform .2s linear}.callout .callout-header[aria-expanded=false] .callout-toggle::before{transform:rotate(-90deg)}.callout .callout-header[aria-expanded=true] .callout-toggle::before{transform:none}.callout.callout-style-simple:not(.no-icon) div.callout-icon-container{padding-top:.2em;padding-right:.55em}.callout.callout-style-default:not(.no-icon) div.callout-icon-container{padding-top:.1em;padding-right:.35em}.callout.callout-style-default:not(.no-icon) div.callout-title-container{margin-top:-1px}.callout.callout-style-default.callout-caution:not(.no-icon) div.callout-icon-container{padding-top:.3em;padding-right:.35em}.callout>.callout-body>.callout-icon-container>.no-icon,.callout>.callout-header>.callout-icon-container>.no-icon{display:none}div.callout.callout{border-left-color:#6c757d}div.callout.callout-style-default>.callout-header{background-color:#6c757d}div.callout-note.callout{border-left-color:#2780e3}div.callout-note.callout-style-default>.callout-header{background-color:#e9f2fc}div.callout-note:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-tip.callout{border-left-color:#3fb618}div.callout-tip.callout-style-default>.callout-header{background-color:#ecf8e8}div.callout-tip:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-warning.callout{border-left-color:#ff7518}div.callout-warning.callout-style-default>.callout-header{background-color:#fff1e8}div.callout-warning:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-caution.callout{border-left-color:#f0ad4e}div.callout-caution.callout-style-default>.callout-header{background-color:#fef7ed}div.callout-caution:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-important.callout{border-left-color:#ff0039}div.callout-important.callout-style-default>.callout-header{background-color:#ffe6eb}div.callout-important:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important .callout-toggle::before{background-image:url('data:image/svg+xml,')}.quarto-toggle-container{display:flex;align-items:center}.quarto-reader-toggle .bi::before,.quarto-color-scheme-toggle .bi::before{display:inline-block;height:1rem;width:1rem;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.sidebar-navigation{padding-left:20px}.navbar{background-color:#3cdd8c;color:#fff}.navbar .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.quarto-sidebar-toggle{border-color:#dee2e6;border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem;border-style:solid;border-width:1px;overflow:hidden;border-top-width:0px;padding-top:0px !important}.quarto-sidebar-toggle-title{cursor:pointer;padding-bottom:2px;margin-left:.25em;text-align:center;font-weight:400;font-size:.775em}#quarto-content .quarto-sidebar-toggle{background:#fafafa}#quarto-content .quarto-sidebar-toggle-title{color:#000}.quarto-sidebar-toggle-icon{color:#dee2e6;margin-right:.5em;float:right;transition:transform .2s ease}.quarto-sidebar-toggle-icon::before{padding-top:5px}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-icon{transform:rotate(-180deg)}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-title{border-bottom:solid #dee2e6 1px}.quarto-sidebar-toggle-contents{background-color:#fff;padding-right:10px;padding-left:10px;margin-top:0px !important;transition:max-height .5s ease}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-contents{padding-top:1em;padding-bottom:10px}@media(max-width: 767.98px){.sidebar-menu-container{padding-bottom:5em}}.quarto-sidebar-toggle:not(.expanded) .quarto-sidebar-toggle-contents{padding-top:0px !important;padding-bottom:0px}nav[role=doc-toc]{z-index:1020}#quarto-sidebar>*,nav[role=doc-toc]>*{transition:opacity .1s ease,border .1s ease}#quarto-sidebar.slow>*,nav[role=doc-toc].slow>*{transition:opacity .4s ease,border .4s ease}.quarto-color-scheme-toggle:not(.alternate).top-right .bi::before{background-image:url('data:image/svg+xml,')}.quarto-color-scheme-toggle.alternate.top-right .bi::before{background-image:url('data:image/svg+xml,')}#quarto-appendix.default{border-top:1px solid #dee2e6}#quarto-appendix.default{background-color:#fff;padding-top:1.5em;margin-top:2em;z-index:998}#quarto-appendix.default .quarto-appendix-heading{margin-top:0;line-height:1.4em;font-weight:600;opacity:.9;border-bottom:none;margin-bottom:0}#quarto-appendix.default .footnotes ol,#quarto-appendix.default .footnotes ol li>p:last-of-type,#quarto-appendix.default .quarto-appendix-contents>p:last-of-type{margin-bottom:0}#quarto-appendix.default .footnotes ol{margin-left:.5em}#quarto-appendix.default .quarto-appendix-secondary-label{margin-bottom:.4em}#quarto-appendix.default .quarto-appendix-bibtex{font-size:.7em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-bibtex code.sourceCode{white-space:pre-wrap}#quarto-appendix.default .quarto-appendix-citeas{font-size:.9em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-heading{font-size:1em !important}#quarto-appendix.default *[role=doc-endnotes]>ol,#quarto-appendix.default .quarto-appendix-contents>*:not(h2):not(.h2){font-size:.9em}#quarto-appendix.default section{padding-bottom:1.5em}#quarto-appendix.default section *[role=doc-endnotes],#quarto-appendix.default section>*:not(a){opacity:.9;word-wrap:break-word}.btn.btn-quarto,div.cell-output-display .btn-quarto{--bs-btn-color: #cacccd;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #cacccd;--bs-btn-hover-bg: #52585d;--bs-btn-hover-border-color: #484e53;--bs-btn-focus-shadow-rgb: 75, 80, 85;--bs-btn-active-color: #fff;--bs-btn-active-bg: #5d6166;--bs-btn-active-border-color: #484e53;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}nav.quarto-secondary-nav.color-navbar{background-color:#3cdd8c;color:#fff}nav.quarto-secondary-nav.color-navbar h1,nav.quarto-secondary-nav.color-navbar .h1,nav.quarto-secondary-nav.color-navbar .quarto-btn-toggle{color:#fff}@media(max-width: 991.98px){body.nav-sidebar .quarto-title-banner{margin-bottom:0;padding-bottom:1em}body.nav-sidebar #title-block-header{margin-block-end:0}}p.subtitle{margin-top:.25em;margin-bottom:.5em}code a:any-link{color:inherit;text-decoration-color:#6c757d}/*! light */div.observablehq table thead tr th{background-color:var(--bs-body-bg)}input,button,select,optgroup,textarea{background-color:var(--bs-body-bg)}.code-annotated .code-copy-button{margin-right:1.25em;margin-top:0;padding-bottom:0;padding-top:3px}.code-annotation-gutter-bg{background-color:#fff}.code-annotation-gutter{background-color:rgba(233,236,239,.65)}.code-annotation-gutter,.code-annotation-gutter-bg{height:100%;width:calc(20px + .5em);position:absolute;top:0;right:0}dl.code-annotation-container-grid dt{margin-right:1em;margin-top:.25rem}dl.code-annotation-container-grid dt{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;color:#1a1a1a;border:solid #1a1a1a 1px;border-radius:50%;height:22px;width:22px;line-height:22px;font-size:11px;text-align:center;vertical-align:middle;text-decoration:none}dl.code-annotation-container-grid dt[data-target-cell]{cursor:pointer}dl.code-annotation-container-grid dt[data-target-cell].code-annotation-active{color:#fff;border:solid #aaa 1px;background-color:#aaa}pre.code-annotation-code{padding-top:0;padding-bottom:0}pre.code-annotation-code code{z-index:3}#code-annotation-line-highlight-gutter{width:100%;border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}#code-annotation-line-highlight{margin-left:-4em;width:calc(100% + 4em);border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}code.sourceCode .code-annotation-anchor.code-annotation-active{background-color:var(--quarto-hl-normal-color, #aaaaaa);border:solid var(--quarto-hl-normal-color, #aaaaaa) 1px;color:#e9ecef;font-weight:bolder}code.sourceCode .code-annotation-anchor{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;color:var(--quarto-hl-co-color);border:solid var(--quarto-hl-co-color) 1px;border-radius:50%;height:18px;width:18px;font-size:9px;margin-top:2px}code.sourceCode button.code-annotation-anchor{padding:2px;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none}code.sourceCode a.code-annotation-anchor{line-height:18px;text-align:center;vertical-align:middle;cursor:default;text-decoration:none}@media print{.page-columns .column-screen-inset{grid-column:page-start-inset/page-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:page-start/page-end;z-index:998;opacity:.999}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:page-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/page-end;z-index:998;opacity:.999}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:page-start-inset/page-end-inset;padding:1em;background:#f8f9fa;z-index:998;opacity:.999;margin-bottom:1em}}.quarto-video{margin-bottom:1em}.table{border-top:1px solid #ccc;border-bottom:1px solid #ccc}.table>thead{border-top-width:0;border-bottom:1px solid gray}.table a{word-break:break-word}.table>:not(caption)>*>*{background-color:unset;color:unset}#quarto-document-content .crosstalk-input .checkbox input[type=checkbox],#quarto-document-content .crosstalk-input .checkbox-inline input[type=checkbox]{position:unset;margin-top:unset;margin-left:unset}#quarto-document-content .row{margin-left:unset;margin-right:unset}.quarto-xref{white-space:nowrap}#quarto-draft-alert{margin-top:0px;margin-bottom:0px;padding:.3em;text-align:center;font-size:.9em}#quarto-draft-alert i{margin-right:.3em}#quarto-back-to-top{z-index:1000}pre{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:0.875em;font-weight:400}pre code{font-family:inherit;font-size:inherit;font-weight:inherit}code{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:0.875em;font-weight:400}a{background-color:rgba(0,0,0,0);font-weight:400;text-decoration:underline}a.external:after{content:"";background-image:url('data:image/svg+xml,');background-size:contain;background-repeat:no-repeat;background-position:center center;margin-left:.2em;padding-right:.75em}div.sourceCode code a.external:after{content:none}a.external:after:hover{cursor:pointer}.quarto-ext-icon{display:inline-block;font-size:.75em;padding-left:.3em}.code-with-filename .code-with-filename-file{margin-bottom:0;padding-bottom:2px;padding-top:2px;padding-left:.7em;border:var(--quarto-border-width) solid var(--quarto-border-color);border-radius:var(--quarto-border-radius);border-bottom:0;border-bottom-left-radius:0%;border-bottom-right-radius:0%}.code-with-filename div.sourceCode,.reveal .code-with-filename div.sourceCode{margin-top:0;border-top-left-radius:0%;border-top-right-radius:0%}.code-with-filename .code-with-filename-file pre{margin-bottom:0}.code-with-filename .code-with-filename-file{background-color:rgba(219,219,219,.8)}.quarto-dark .code-with-filename .code-with-filename-file{background-color:#555}.code-with-filename .code-with-filename-file strong{font-weight:400}.quarto-title-banner{margin-bottom:1em;color:#fff;background:#3cdd8c}.quarto-title-banner a{color:#fff}.quarto-title-banner h1,.quarto-title-banner .h1,.quarto-title-banner h2,.quarto-title-banner .h2{color:#fff}.quarto-title-banner .code-tools-button{color:#fff}.quarto-title-banner .code-tools-button:hover{color:#fff}.quarto-title-banner .code-tools-button>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .quarto-title .title{font-weight:600}.quarto-title-banner .quarto-categories{margin-top:.75em}@media(min-width: 992px){.quarto-title-banner{padding-top:2.5em;padding-bottom:2.5em}}@media(max-width: 991.98px){.quarto-title-banner{padding-top:1em;padding-bottom:1em}}@media(max-width: 767.98px){body.hypothesis-enabled #title-block-header>*{padding-right:20px}}main.quarto-banner-title-block>section:first-child>h2,main.quarto-banner-title-block>section:first-child>.h2,main.quarto-banner-title-block>section:first-child>h3,main.quarto-banner-title-block>section:first-child>.h3,main.quarto-banner-title-block>section:first-child>h4,main.quarto-banner-title-block>section:first-child>.h4{margin-top:0}.quarto-title .quarto-categories{display:flex;flex-wrap:wrap;row-gap:.5em;column-gap:.4em;padding-bottom:.5em;margin-top:.75em}.quarto-title .quarto-categories .quarto-category{padding:.25em .75em;font-size:.65em;text-transform:uppercase;border:solid 1px;border-radius:.25rem;opacity:.6}.quarto-title .quarto-categories .quarto-category a{color:inherit}.quarto-title-meta-container{display:grid;grid-template-columns:1fr auto}.quarto-title-meta-column-end{display:flex;flex-direction:column;padding-left:1em}.quarto-title-meta-column-end a .bi{margin-right:.3em}#title-block-header.quarto-title-block.default .quarto-title-meta{display:grid;grid-template-columns:repeat(2, 1fr);grid-column-gap:1em}#title-block-header.quarto-title-block.default .quarto-title .title{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-author-orcid img{margin-top:-0.2em;height:.8em;width:.8em}#title-block-header.quarto-title-block.default .quarto-title-author-email{opacity:.7}#title-block-header.quarto-title-block.default .quarto-description p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p,#title-block-header.quarto-title-block.default .quarto-title-authors p,#title-block-header.quarto-title-block.default .quarto-title-affiliations p{margin-bottom:.1em}#title-block-header.quarto-title-block.default .quarto-title-meta-heading{text-transform:uppercase;margin-top:1em;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-contents{font-size:.9em}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p.affiliation:last-of-type{margin-bottom:.1em}#title-block-header.quarto-title-block.default p.affiliation{margin-bottom:.1em}#title-block-header.quarto-title-block.default .keywords,#title-block-header.quarto-title-block.default .description,#title-block-header.quarto-title-block.default .abstract{margin-top:0}#title-block-header.quarto-title-block.default .keywords>p,#title-block-header.quarto-title-block.default .description>p,#title-block-header.quarto-title-block.default .abstract>p{font-size:.9em}#title-block-header.quarto-title-block.default .keywords>p:last-of-type,#title-block-header.quarto-title-block.default .description>p:last-of-type,#title-block-header.quarto-title-block.default .abstract>p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .keywords .block-title,#title-block-header.quarto-title-block.default .description .block-title,#title-block-header.quarto-title-block.default .abstract .block-title{margin-top:1em;text-transform:uppercase;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-author{display:grid;grid-template-columns:minmax(max-content, 1fr) 1fr;grid-column-gap:1em}.quarto-title-tools-only{display:flex;justify-content:right}body{-webkit-font-smoothing:antialiased}.badge.bg-light{color:#343a40}.progress .progress-bar{font-size:8px;line-height:8px}:root{--quarto-scss-export-gray-300: #dee2e6;--quarto-scss-export-gray-500: #adb5bd;--quarto-scss-export-gray-600: #6c757d;--quarto-scss-export-gray-800: #343a40;--quarto-scss-export-card-cap-bg: rgba(52, 58, 64, 0.25);--quarto-scss-export-border-color: #dee2e6;--quarto-scss-export-text-muted: #6c757d;--quarto-scss-export-body-bg: #FFFFFF;--quarto-scss-export-body-color: #000000;--quarto-scss-export-link-color: #4040BF;--quarto-scss-export-primary: #E8E8FC;--quarto-scss-export-code-bg: #f8f9fa;--quarto-scss-export-code-color: #7d12ba;--quarto-scss-export-navbar-bg: #3CDD8C;--quarto-scss-export-navbar-fg: #FFFFFF;--quarto-scss-export-navbar-hl: #000000;--quarto-scss-export-white: #fff;--quarto-scss-export-gray-100: #f8f9fa;--quarto-scss-export-gray-200: #e9ecef;--quarto-scss-export-gray-400: #ced4da;--quarto-scss-export-gray-700: #495057;--quarto-scss-export-gray-900: #212529;--quarto-scss-export-black: #000;--quarto-scss-export-blue: #2780e3;--quarto-scss-export-indigo: #6610f2;--quarto-scss-export-purple: #613d7c;--quarto-scss-export-pink: #e83e8c;--quarto-scss-export-red: #ff0039;--quarto-scss-export-orange: #f0ad4e;--quarto-scss-export-yellow: #ff7518;--quarto-scss-export-green: #3fb618;--quarto-scss-export-teal: #20c997;--quarto-scss-export-cyan: #9954bb;--quarto-scss-export-secondary: #343a40;--quarto-scss-export-success: #3fb618;--quarto-scss-export-info: #9954bb;--quarto-scss-export-warning: #ff7518;--quarto-scss-export-danger: #ff0039;--quarto-scss-export-light: #f8f9fa;--quarto-scss-export-dark: #343a40;--quarto-scss-export-title-banner-color: ;--quarto-scss-export-title-banner-bg: ;--quarto-scss-export-btn-code-copy-color: #5E5E5E;--quarto-scss-export-btn-code-copy-color-active: #4758AB;--quarto-scss-export-sidebar-bg: #FFFFFF;--quarto-scss-export-link-color-bg: transparent;--quarto-scss-export-toc-color: #4040BF;--quarto-scss-export-toc-active-border: #4040BF;--quarto-scss-export-toc-inactive-border: #e9ecef;--quarto-scss-export-navbar-default: #E8E8FC;--quarto-scss-export-navbar-hl-override: #29297a;--quarto-scss-export-btn-bg: #343a40;--quarto-scss-export-btn-fg: #cacccd;--quarto-scss-export-body-contrast-bg: #FFFFFF;--quarto-scss-export-body-contrast-color: #000000;--quarto-scss-export-navbar-brand: #FFFFFF;--quarto-scss-export-navbar-brand-hl: #000000;--quarto-scss-export-navbar-toggler-border-color: rgba(255, 255, 255, 0);--quarto-scss-export-navbar-hover-color: rgba(0, 0, 0, 0.8);--quarto-scss-export-navbar-disabled-color: rgba(255, 255, 255, 0.75);--quarto-scss-export-sidebar-fg: #595959;--quarto-scss-export-title-block-color: #000000;--quarto-scss-export-title-block-contast-color: #FFFFFF;--quarto-scss-export-footer-bg: #FFFFFF;--quarto-scss-export-footer-fg: #757575;--quarto-scss-export-popover-bg: #FFFFFF;--quarto-scss-export-input-bg: #FFFFFF;--quarto-scss-export-input-border-color: #dee2e6;--quarto-scss-export-code-annotation-higlight-color: rgba(170, 170, 170, 0.2666666667);--quarto-scss-export-code-annotation-higlight-bg: rgba(170, 170, 170, 0.1333333333);--quarto-scss-export-table-group-separator-color: gray;--quarto-scss-export-table-group-separator-color-lighter: #cccccc;--quarto-scss-export-link-decoration: underline;--quarto-scss-export-table-border-color: #dee2e6;--quarto-scss-export-sidebar-glass-bg: rgba(102, 102, 102, 0.4);--quarto-scss-export-color-contrast-dark: #000;--quarto-scss-export-color-contrast-light: #fff;--quarto-scss-export-blue-100: #d4e6f9;--quarto-scss-export-blue-200: #a9ccf4;--quarto-scss-export-blue-300: #7db3ee;--quarto-scss-export-blue-400: #5299e9;--quarto-scss-export-blue-500: #2780e3;--quarto-scss-export-blue-600: #1f66b6;--quarto-scss-export-blue-700: #174d88;--quarto-scss-export-blue-800: #10335b;--quarto-scss-export-blue-900: #081a2d;--quarto-scss-export-indigo-100: #e0cffc;--quarto-scss-export-indigo-200: #c29ffa;--quarto-scss-export-indigo-300: #a370f7;--quarto-scss-export-indigo-400: #8540f5;--quarto-scss-export-indigo-500: #6610f2;--quarto-scss-export-indigo-600: #520dc2;--quarto-scss-export-indigo-700: #3d0a91;--quarto-scss-export-indigo-800: #290661;--quarto-scss-export-indigo-900: #140330;--quarto-scss-export-purple-100: #dfd8e5;--quarto-scss-export-purple-200: #c0b1cb;--quarto-scss-export-purple-300: #a08bb0;--quarto-scss-export-purple-400: #816496;--quarto-scss-export-purple-500: #613d7c;--quarto-scss-export-purple-600: #4e3163;--quarto-scss-export-purple-700: #3a254a;--quarto-scss-export-purple-800: #271832;--quarto-scss-export-purple-900: #130c19;--quarto-scss-export-pink-100: #fad8e8;--quarto-scss-export-pink-200: #f6b2d1;--quarto-scss-export-pink-300: #f18bba;--quarto-scss-export-pink-400: #ed65a3;--quarto-scss-export-pink-500: #e83e8c;--quarto-scss-export-pink-600: #ba3270;--quarto-scss-export-pink-700: #8b2554;--quarto-scss-export-pink-800: #5d1938;--quarto-scss-export-pink-900: #2e0c1c;--quarto-scss-export-red-100: #ffccd7;--quarto-scss-export-red-200: #ff99b0;--quarto-scss-export-red-300: #ff6688;--quarto-scss-export-red-400: #ff3361;--quarto-scss-export-red-500: #ff0039;--quarto-scss-export-red-600: #cc002e;--quarto-scss-export-red-700: #990022;--quarto-scss-export-red-800: #660017;--quarto-scss-export-red-900: #33000b;--quarto-scss-export-orange-100: #fcefdc;--quarto-scss-export-orange-200: #f9deb8;--quarto-scss-export-orange-300: #f6ce95;--quarto-scss-export-orange-400: #f3bd71;--quarto-scss-export-orange-500: #f0ad4e;--quarto-scss-export-orange-600: #c08a3e;--quarto-scss-export-orange-700: #90682f;--quarto-scss-export-orange-800: #60451f;--quarto-scss-export-orange-900: #302310;--quarto-scss-export-yellow-100: #ffe3d1;--quarto-scss-export-yellow-200: #ffc8a3;--quarto-scss-export-yellow-300: #ffac74;--quarto-scss-export-yellow-400: #ff9146;--quarto-scss-export-yellow-500: #ff7518;--quarto-scss-export-yellow-600: #cc5e13;--quarto-scss-export-yellow-700: #99460e;--quarto-scss-export-yellow-800: #662f0a;--quarto-scss-export-yellow-900: #331705;--quarto-scss-export-green-100: #d9f0d1;--quarto-scss-export-green-200: #b2e2a3;--quarto-scss-export-green-300: #8cd374;--quarto-scss-export-green-400: #65c546;--quarto-scss-export-green-500: #3fb618;--quarto-scss-export-green-600: #329213;--quarto-scss-export-green-700: #266d0e;--quarto-scss-export-green-800: #19490a;--quarto-scss-export-green-900: #0d2405;--quarto-scss-export-teal-100: #d2f4ea;--quarto-scss-export-teal-200: #a6e9d5;--quarto-scss-export-teal-300: #79dfc1;--quarto-scss-export-teal-400: #4dd4ac;--quarto-scss-export-teal-500: #20c997;--quarto-scss-export-teal-600: #1aa179;--quarto-scss-export-teal-700: #13795b;--quarto-scss-export-teal-800: #0d503c;--quarto-scss-export-teal-900: #06281e;--quarto-scss-export-cyan-100: #ebddf1;--quarto-scss-export-cyan-200: #d6bbe4;--quarto-scss-export-cyan-300: #c298d6;--quarto-scss-export-cyan-400: #ad76c9;--quarto-scss-export-cyan-500: #9954bb;--quarto-scss-export-cyan-600: #7a4396;--quarto-scss-export-cyan-700: #5c3270;--quarto-scss-export-cyan-800: #3d224b;--quarto-scss-export-cyan-900: #1f1125;--quarto-scss-export-default: #343a40;--quarto-scss-export-primary-text-emphasis: #5d5d65;--quarto-scss-export-secondary-text-emphasis: #15171a;--quarto-scss-export-success-text-emphasis: #19490a;--quarto-scss-export-info-text-emphasis: #3d224b;--quarto-scss-export-warning-text-emphasis: #662f0a;--quarto-scss-export-danger-text-emphasis: #660017;--quarto-scss-export-light-text-emphasis: #495057;--quarto-scss-export-dark-text-emphasis: #495057;--quarto-scss-export-primary-bg-subtle: #fafafe;--quarto-scss-export-secondary-bg-subtle: #d6d8d9;--quarto-scss-export-success-bg-subtle: #d9f0d1;--quarto-scss-export-info-bg-subtle: #ebddf1;--quarto-scss-export-warning-bg-subtle: #ffe3d1;--quarto-scss-export-danger-bg-subtle: #ffccd7;--quarto-scss-export-light-bg-subtle: #fcfcfd;--quarto-scss-export-dark-bg-subtle: #ced4da;--quarto-scss-export-primary-border-subtle: #f6f6fe;--quarto-scss-export-secondary-border-subtle: #aeb0b3;--quarto-scss-export-success-border-subtle: #b2e2a3;--quarto-scss-export-info-border-subtle: #d6bbe4;--quarto-scss-export-warning-border-subtle: #ffc8a3;--quarto-scss-export-danger-border-subtle: #ff99b0;--quarto-scss-export-light-border-subtle: #e9ecef;--quarto-scss-export-dark-border-subtle: #adb5bd;--quarto-scss-export-body-text-align: ;--quarto-scss-export-body-secondary-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-body-secondary-bg: #e9ecef;--quarto-scss-export-body-tertiary-color: rgba(0, 0, 0, 0.5);--quarto-scss-export-body-tertiary-bg: #f8f9fa;--quarto-scss-export-body-emphasis-color: #000;--quarto-scss-export-link-hover-color: #333399;--quarto-scss-export-link-hover-decoration: ;--quarto-scss-export-border-color-translucent: rgba(0, 0, 0, 0.175);--quarto-scss-export-component-active-bg: #E8E8FC;--quarto-scss-export-component-active-color: #000;--quarto-scss-export-focus-ring-color: rgba(232, 232, 252, 0.25);--quarto-scss-export-headings-font-family: ;--quarto-scss-export-headings-font-style: ;--quarto-scss-export-display-font-family: ;--quarto-scss-export-display-font-style: ;--quarto-scss-export-blockquote-footer-color: #6c757d;--quarto-scss-export-blockquote-border-color: #e9ecef;--quarto-scss-export-hr-bg-color: ;--quarto-scss-export-hr-height: ;--quarto-scss-export-hr-border-color: ;--quarto-scss-export-legend-font-weight: ;--quarto-scss-export-mark-bg: #ffe3d1;--quarto-scss-export-table-color: #000000;--quarto-scss-export-table-bg: #FFFFFF;--quarto-scss-export-table-accent-bg: transparent;--quarto-scss-export-table-th-font-weight: ;--quarto-scss-export-table-striped-color: #000000;--quarto-scss-export-table-striped-bg: rgba(0, 0, 0, 0.05);--quarto-scss-export-table-active-color: #000000;--quarto-scss-export-table-active-bg: rgba(0, 0, 0, 0.1);--quarto-scss-export-table-hover-color: #000000;--quarto-scss-export-table-hover-bg: rgba(0, 0, 0, 0.075);--quarto-scss-export-table-caption-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-input-btn-font-family: ;--quarto-scss-export-input-btn-focus-color: rgba(232, 232, 252, 0.25);--quarto-scss-export-btn-color: #000000;--quarto-scss-export-btn-font-family: ;--quarto-scss-export-btn-white-space: ;--quarto-scss-export-btn-link-color: #4040BF;--quarto-scss-export-btn-link-hover-color: #333399;--quarto-scss-export-btn-link-disabled-color: #6c757d;--quarto-scss-export-form-text-font-style: ;--quarto-scss-export-form-text-font-weight: ;--quarto-scss-export-form-text-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-form-label-font-size: ;--quarto-scss-export-form-label-font-style: ;--quarto-scss-export-form-label-font-weight: ;--quarto-scss-export-form-label-color: ;--quarto-scss-export-input-font-family: ;--quarto-scss-export-input-disabled-color: ;--quarto-scss-export-input-disabled-bg: #e9ecef;--quarto-scss-export-input-disabled-border-color: ;--quarto-scss-export-input-color: #000000;--quarto-scss-export-input-focus-bg: #FFFFFF;--quarto-scss-export-input-focus-border-color: #f4f4fe;--quarto-scss-export-input-focus-color: #000000;--quarto-scss-export-input-placeholder-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-input-plaintext-color: #000000;--quarto-scss-export-form-check-label-color: ;--quarto-scss-export-form-check-transition: ;--quarto-scss-export-form-check-input-bg: #FFFFFF;--quarto-scss-export-form-check-input-focus-border: #f4f4fe;--quarto-scss-export-form-check-input-checked-color: #000;--quarto-scss-export-form-check-input-checked-bg-color: #E8E8FC;--quarto-scss-export-form-check-input-checked-border-color: #E8E8FC;--quarto-scss-export-form-check-input-indeterminate-color: #000;--quarto-scss-export-form-check-input-indeterminate-bg-color: #E8E8FC;--quarto-scss-export-form-check-input-indeterminate-border-color: #E8E8FC;--quarto-scss-export-form-switch-color: rgba(0, 0, 0, 0.25);--quarto-scss-export-form-switch-focus-color: #f4f4fe;--quarto-scss-export-form-switch-checked-color: #000;--quarto-scss-export-input-group-addon-color: #000000;--quarto-scss-export-input-group-addon-bg: #f8f9fa;--quarto-scss-export-input-group-addon-border-color: #dee2e6;--quarto-scss-export-form-select-font-family: ;--quarto-scss-export-form-select-color: #000000;--quarto-scss-export-form-select-bg: #FFFFFF;--quarto-scss-export-form-select-disabled-color: ;--quarto-scss-export-form-select-disabled-bg: #e9ecef;--quarto-scss-export-form-select-disabled-border-color: ;--quarto-scss-export-form-select-indicator-color: #343a40;--quarto-scss-export-form-select-border-color: #dee2e6;--quarto-scss-export-form-select-focus-border-color: #f4f4fe;--quarto-scss-export-form-range-track-bg: #f8f9fa;--quarto-scss-export-form-range-thumb-bg: #E8E8FC;--quarto-scss-export-form-range-thumb-active-bg: #f8f8fe;--quarto-scss-export-form-range-thumb-disabled-bg: rgba(0, 0, 0, 0.75);--quarto-scss-export-form-file-button-color: #000000;--quarto-scss-export-form-file-button-bg: #f8f9fa;--quarto-scss-export-form-file-button-hover-bg: #e9ecef;--quarto-scss-export-form-floating-label-disabled-color: #6c757d;--quarto-scss-export-form-feedback-font-style: ;--quarto-scss-export-form-feedback-valid-color: #3fb618;--quarto-scss-export-form-feedback-invalid-color: #ff0039;--quarto-scss-export-form-feedback-icon-valid-color: #3fb618;--quarto-scss-export-form-feedback-icon-invalid-color: #ff0039;--quarto-scss-export-form-valid-color: #3fb618;--quarto-scss-export-form-valid-border-color: #3fb618;--quarto-scss-export-form-invalid-color: #ff0039;--quarto-scss-export-form-invalid-border-color: #ff0039;--quarto-scss-export-nav-link-font-size: ;--quarto-scss-export-nav-link-font-weight: ;--quarto-scss-export-nav-link-color: #4040BF;--quarto-scss-export-nav-link-hover-color: #333399;--quarto-scss-export-nav-link-disabled-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-nav-tabs-border-color: #dee2e6;--quarto-scss-export-nav-tabs-link-hover-border-color: #e9ecef #e9ecef #dee2e6;--quarto-scss-export-nav-tabs-link-active-color: #000;--quarto-scss-export-nav-tabs-link-active-bg: #FFFFFF;--quarto-scss-export-nav-pills-link-active-bg: #E8E8FC;--quarto-scss-export-nav-pills-link-active-color: #000;--quarto-scss-export-nav-underline-link-active-color: #000;--quarto-scss-export-navbar-padding-x: ;--quarto-scss-export-navbar-light-contrast: #000;--quarto-scss-export-navbar-dark-contrast: #000;--quarto-scss-export-navbar-light-icon-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-navbar-dark-icon-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-dropdown-color: #000000;--quarto-scss-export-dropdown-bg: #FFFFFF;--quarto-scss-export-dropdown-border-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-dropdown-divider-bg: rgba(0, 0, 0, 0.175);--quarto-scss-export-dropdown-link-color: #000000;--quarto-scss-export-dropdown-link-hover-color: #000000;--quarto-scss-export-dropdown-link-hover-bg: #f8f9fa;--quarto-scss-export-dropdown-link-active-bg: #E8E8FC;--quarto-scss-export-dropdown-link-active-color: #000;--quarto-scss-export-dropdown-link-disabled-color: rgba(0, 0, 0, 0.5);--quarto-scss-export-dropdown-header-color: #6c757d;--quarto-scss-export-dropdown-dark-color: #dee2e6;--quarto-scss-export-dropdown-dark-bg: #343a40;--quarto-scss-export-dropdown-dark-border-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-dropdown-dark-divider-bg: rgba(0, 0, 0, 0.175);--quarto-scss-export-dropdown-dark-box-shadow: ;--quarto-scss-export-dropdown-dark-link-color: #dee2e6;--quarto-scss-export-dropdown-dark-link-hover-color: #fff;--quarto-scss-export-dropdown-dark-link-hover-bg: rgba(255, 255, 255, 0.15);--quarto-scss-export-dropdown-dark-link-active-color: #000;--quarto-scss-export-dropdown-dark-link-active-bg: #E8E8FC;--quarto-scss-export-dropdown-dark-link-disabled-color: #adb5bd;--quarto-scss-export-dropdown-dark-header-color: #adb5bd;--quarto-scss-export-pagination-color: #4040BF;--quarto-scss-export-pagination-bg: #FFFFFF;--quarto-scss-export-pagination-border-color: #dee2e6;--quarto-scss-export-pagination-focus-color: #333399;--quarto-scss-export-pagination-focus-bg: #e9ecef;--quarto-scss-export-pagination-hover-color: #333399;--quarto-scss-export-pagination-hover-bg: #f8f9fa;--quarto-scss-export-pagination-hover-border-color: #dee2e6;--quarto-scss-export-pagination-active-color: #000;--quarto-scss-export-pagination-active-bg: #E8E8FC;--quarto-scss-export-pagination-active-border-color: #E8E8FC;--quarto-scss-export-pagination-disabled-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-pagination-disabled-bg: #e9ecef;--quarto-scss-export-pagination-disabled-border-color: #dee2e6;--quarto-scss-export-card-title-color: ;--quarto-scss-export-card-subtitle-color: ;--quarto-scss-export-card-border-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-card-box-shadow: ;--quarto-scss-export-card-cap-color: ;--quarto-scss-export-card-height: ;--quarto-scss-export-card-color: ;--quarto-scss-export-card-bg: #FFFFFF;--quarto-scss-export-accordion-color: #000000;--quarto-scss-export-accordion-bg: #FFFFFF;--quarto-scss-export-accordion-border-color: #dee2e6;--quarto-scss-export-accordion-button-color: #000000;--quarto-scss-export-accordion-button-bg: #FFFFFF;--quarto-scss-export-accordion-button-active-bg: #fafafe;--quarto-scss-export-accordion-button-active-color: #5d5d65;--quarto-scss-export-accordion-button-focus-border-color: #f4f4fe;--quarto-scss-export-accordion-icon-color: #000000;--quarto-scss-export-accordion-icon-active-color: #5d5d65;--quarto-scss-export-tooltip-color: #FFFFFF;--quarto-scss-export-tooltip-bg: #000;--quarto-scss-export-tooltip-margin: ;--quarto-scss-export-tooltip-arrow-color: ;--quarto-scss-export-form-feedback-tooltip-line-height: ;--quarto-scss-export-popover-border-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-popover-header-bg: #e9ecef;--quarto-scss-export-popover-body-color: #000000;--quarto-scss-export-popover-arrow-color: #FFFFFF;--quarto-scss-export-popover-arrow-outer-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-toast-color: ;--quarto-scss-export-toast-background-color: rgba(255, 255, 255, 0.85);--quarto-scss-export-toast-border-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-toast-header-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-toast-header-background-color: rgba(255, 255, 255, 0.85);--quarto-scss-export-toast-header-border-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-badge-color: #fff;--quarto-scss-export-modal-content-color: ;--quarto-scss-export-modal-content-bg: #FFFFFF;--quarto-scss-export-modal-content-border-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-modal-backdrop-bg: #000;--quarto-scss-export-modal-header-border-color: #dee2e6;--quarto-scss-export-modal-footer-bg: ;--quarto-scss-export-modal-footer-border-color: #dee2e6;--quarto-scss-export-progress-bg: #e9ecef;--quarto-scss-export-progress-bar-color: #fff;--quarto-scss-export-progress-bar-bg: #E8E8FC;--quarto-scss-export-list-group-color: #000000;--quarto-scss-export-list-group-bg: #FFFFFF;--quarto-scss-export-list-group-border-color: #dee2e6;--quarto-scss-export-list-group-hover-bg: #f8f9fa;--quarto-scss-export-list-group-active-bg: #E8E8FC;--quarto-scss-export-list-group-active-color: #000;--quarto-scss-export-list-group-active-border-color: #E8E8FC;--quarto-scss-export-list-group-disabled-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-list-group-disabled-bg: #FFFFFF;--quarto-scss-export-list-group-action-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-list-group-action-hover-color: #000;--quarto-scss-export-list-group-action-active-color: #000000;--quarto-scss-export-list-group-action-active-bg: #e9ecef;--quarto-scss-export-thumbnail-bg: #FFFFFF;--quarto-scss-export-thumbnail-border-color: #dee2e6;--quarto-scss-export-figure-caption-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-breadcrumb-font-size: ;--quarto-scss-export-breadcrumb-bg: ;--quarto-scss-export-breadcrumb-divider-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-breadcrumb-active-color: rgba(0, 0, 0, 0.75);--quarto-scss-export-breadcrumb-border-radius: ;--quarto-scss-export-carousel-control-color: #fff;--quarto-scss-export-carousel-indicator-active-bg: #fff;--quarto-scss-export-carousel-caption-color: #fff;--quarto-scss-export-carousel-dark-indicator-active-bg: #000;--quarto-scss-export-carousel-dark-caption-color: #000;--quarto-scss-export-btn-close-color: #000;--quarto-scss-export-offcanvas-border-color: rgba(0, 0, 0, 0.175);--quarto-scss-export-offcanvas-bg-color: #FFFFFF;--quarto-scss-export-offcanvas-color: #000000;--quarto-scss-export-offcanvas-backdrop-bg: #000;--quarto-scss-export-code-color-dark: white;--quarto-scss-export-kbd-color: #FFFFFF;--quarto-scss-export-kbd-bg: #000000;--quarto-scss-export-nested-kbd-font-weight: ;--quarto-scss-export-pre-bg: #f8f9fa;--quarto-scss-export-pre-color: #000;--quarto-scss-export-bslib-page-sidebar-title-bg: #3CDD8C;--quarto-scss-export-bslib-page-sidebar-title-color: #000;--quarto-scss-export-bslib-sidebar-bg: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);--quarto-scss-export-bslib-sidebar-toggle-bg: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.1);--quarto-scss-export-sidebar-color: #595959;--quarto-scss-export-sidebar-hover-color: rgba(64, 64, 191, 0.8);--quarto-scss-export-sidebar-disabled-color: rgba(89, 89, 89, 0.75);--quarto-scss-export-valuebox-bg-primary: #5397e9;--quarto-scss-export-valuebox-bg-secondary: #343a40;--quarto-scss-export-valuebox-bg-success: #3aa716;--quarto-scss-export-valuebox-bg-info: rgba(153, 84, 187, 0.7019607843);--quarto-scss-export-valuebox-bg-warning: #fa6400;--quarto-scss-export-valuebox-bg-danger: rgba(255, 0, 57, 0.7019607843);--quarto-scss-export-valuebox-bg-light: #f8f9fa;--quarto-scss-export-valuebox-bg-dark: #343a40;--quarto-scss-export-mermaid-bg-color: #FFFFFF;--quarto-scss-export-mermaid-edge-color: #343a40;--quarto-scss-export-mermaid-node-fg-color: #000000;--quarto-scss-export-mermaid-fg-color: #000000;--quarto-scss-export-mermaid-fg-color--lighter: #1a1a1a;--quarto-scss-export-mermaid-fg-color--lightest: #333333;--quarto-scss-export-mermaid-label-bg-color: #FFFFFF;--quarto-scss-export-mermaid-label-fg-color: #E8E8FC;--quarto-scss-export-mermaid-node-bg-color: rgba(232, 232, 252, 0.1);--quarto-scss-export-code-block-border-left-color: #dee2e6;--quarto-scss-export-callout-color-note: #2780e3;--quarto-scss-export-callout-color-tip: #3fb618;--quarto-scss-export-callout-color-important: #ff0039;--quarto-scss-export-callout-color-caution: #f0ad4e;--quarto-scss-export-callout-color-warning: #ff7518} \ No newline at end of file diff --git a/docs/site_libs/bootstrap/bootstrap-icons.css b/docs/site_libs/bootstrap/bootstrap-icons.css new file mode 100644 index 0000000000000000000000000000000000000000..285e4448fc8239bca4d8be301d80a6e777aa1d14 --- /dev/null +++ b/docs/site_libs/bootstrap/bootstrap-icons.css @@ -0,0 +1,2078 @@ +/*! + * Bootstrap Icons v1.11.1 (https://icons.getbootstrap.com/) + * Copyright 2019-2023 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) + */ + +@font-face { + font-display: block; + font-family: "bootstrap-icons"; + src: +url("./bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff"); +} + +.bi::before, +[class^="bi-"]::before, +[class*=" bi-"]::before { + display: inline-block; + font-family: bootstrap-icons !important; + font-style: normal; + font-weight: normal !important; + font-variant: normal; + text-transform: none; + line-height: 1; + vertical-align: -.125em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bi-123::before { content: "\f67f"; } +.bi-alarm-fill::before { content: "\f101"; } +.bi-alarm::before { content: "\f102"; } +.bi-align-bottom::before { content: "\f103"; } +.bi-align-center::before { content: "\f104"; } +.bi-align-end::before { content: "\f105"; } +.bi-align-middle::before { content: "\f106"; } +.bi-align-start::before { content: "\f107"; } +.bi-align-top::before { content: "\f108"; } +.bi-alt::before { content: "\f109"; } +.bi-app-indicator::before { content: "\f10a"; } +.bi-app::before { content: "\f10b"; } +.bi-archive-fill::before { content: "\f10c"; } +.bi-archive::before { content: "\f10d"; } +.bi-arrow-90deg-down::before { content: "\f10e"; } +.bi-arrow-90deg-left::before { content: "\f10f"; } +.bi-arrow-90deg-right::before { content: "\f110"; } +.bi-arrow-90deg-up::before { content: "\f111"; } +.bi-arrow-bar-down::before { content: "\f112"; } +.bi-arrow-bar-left::before { content: "\f113"; } +.bi-arrow-bar-right::before { content: "\f114"; } +.bi-arrow-bar-up::before { content: "\f115"; } +.bi-arrow-clockwise::before { content: "\f116"; } +.bi-arrow-counterclockwise::before { content: "\f117"; } +.bi-arrow-down-circle-fill::before { content: "\f118"; } +.bi-arrow-down-circle::before { content: "\f119"; } +.bi-arrow-down-left-circle-fill::before { content: "\f11a"; } +.bi-arrow-down-left-circle::before { content: "\f11b"; } +.bi-arrow-down-left-square-fill::before { content: "\f11c"; } +.bi-arrow-down-left-square::before { content: "\f11d"; } +.bi-arrow-down-left::before { content: "\f11e"; } +.bi-arrow-down-right-circle-fill::before { content: "\f11f"; } +.bi-arrow-down-right-circle::before { content: "\f120"; } +.bi-arrow-down-right-square-fill::before { content: "\f121"; } +.bi-arrow-down-right-square::before { content: "\f122"; } +.bi-arrow-down-right::before { content: "\f123"; } +.bi-arrow-down-short::before { content: "\f124"; } +.bi-arrow-down-square-fill::before { content: "\f125"; } +.bi-arrow-down-square::before { content: "\f126"; } +.bi-arrow-down-up::before { content: "\f127"; } +.bi-arrow-down::before { content: "\f128"; } +.bi-arrow-left-circle-fill::before { content: "\f129"; } +.bi-arrow-left-circle::before { content: "\f12a"; } +.bi-arrow-left-right::before { content: "\f12b"; } +.bi-arrow-left-short::before { content: "\f12c"; } +.bi-arrow-left-square-fill::before { content: "\f12d"; } +.bi-arrow-left-square::before { content: "\f12e"; } +.bi-arrow-left::before { content: "\f12f"; } +.bi-arrow-repeat::before { content: "\f130"; } +.bi-arrow-return-left::before { content: "\f131"; } +.bi-arrow-return-right::before { content: "\f132"; } +.bi-arrow-right-circle-fill::before { content: "\f133"; } +.bi-arrow-right-circle::before { content: "\f134"; } +.bi-arrow-right-short::before { content: "\f135"; } +.bi-arrow-right-square-fill::before { content: "\f136"; } +.bi-arrow-right-square::before { content: "\f137"; } +.bi-arrow-right::before { content: "\f138"; } +.bi-arrow-up-circle-fill::before { content: "\f139"; } +.bi-arrow-up-circle::before { content: "\f13a"; } +.bi-arrow-up-left-circle-fill::before { content: "\f13b"; } +.bi-arrow-up-left-circle::before { content: "\f13c"; } +.bi-arrow-up-left-square-fill::before { content: "\f13d"; } +.bi-arrow-up-left-square::before { content: "\f13e"; } +.bi-arrow-up-left::before { content: "\f13f"; } +.bi-arrow-up-right-circle-fill::before { content: "\f140"; } +.bi-arrow-up-right-circle::before { content: "\f141"; } +.bi-arrow-up-right-square-fill::before { content: "\f142"; } +.bi-arrow-up-right-square::before { content: "\f143"; } +.bi-arrow-up-right::before { content: "\f144"; } +.bi-arrow-up-short::before { content: "\f145"; } +.bi-arrow-up-square-fill::before { content: "\f146"; } +.bi-arrow-up-square::before { content: "\f147"; } +.bi-arrow-up::before { content: "\f148"; } +.bi-arrows-angle-contract::before { content: "\f149"; } +.bi-arrows-angle-expand::before { content: "\f14a"; } +.bi-arrows-collapse::before { content: "\f14b"; } +.bi-arrows-expand::before { content: "\f14c"; } +.bi-arrows-fullscreen::before { content: "\f14d"; } +.bi-arrows-move::before { content: "\f14e"; } +.bi-aspect-ratio-fill::before { content: "\f14f"; } +.bi-aspect-ratio::before { content: "\f150"; } +.bi-asterisk::before { content: "\f151"; } +.bi-at::before { content: "\f152"; } +.bi-award-fill::before { content: "\f153"; } +.bi-award::before { content: "\f154"; } +.bi-back::before { content: "\f155"; } +.bi-backspace-fill::before { content: "\f156"; } +.bi-backspace-reverse-fill::before { content: "\f157"; } +.bi-backspace-reverse::before { content: "\f158"; } +.bi-backspace::before { content: "\f159"; } +.bi-badge-3d-fill::before { content: "\f15a"; } +.bi-badge-3d::before { content: "\f15b"; } +.bi-badge-4k-fill::before { content: "\f15c"; } +.bi-badge-4k::before { content: "\f15d"; } +.bi-badge-8k-fill::before { content: "\f15e"; } +.bi-badge-8k::before { content: "\f15f"; } +.bi-badge-ad-fill::before { content: "\f160"; } +.bi-badge-ad::before { content: "\f161"; } +.bi-badge-ar-fill::before { content: "\f162"; } +.bi-badge-ar::before { content: "\f163"; } +.bi-badge-cc-fill::before { content: "\f164"; } +.bi-badge-cc::before { content: "\f165"; } +.bi-badge-hd-fill::before { content: "\f166"; } +.bi-badge-hd::before { content: "\f167"; } +.bi-badge-tm-fill::before { content: "\f168"; } +.bi-badge-tm::before { content: "\f169"; } +.bi-badge-vo-fill::before { content: "\f16a"; } +.bi-badge-vo::before { content: "\f16b"; } +.bi-badge-vr-fill::before { content: "\f16c"; } +.bi-badge-vr::before { content: "\f16d"; } +.bi-badge-wc-fill::before { content: "\f16e"; } +.bi-badge-wc::before { content: "\f16f"; } +.bi-bag-check-fill::before { content: "\f170"; } +.bi-bag-check::before { content: "\f171"; } +.bi-bag-dash-fill::before { content: "\f172"; } +.bi-bag-dash::before { content: "\f173"; } +.bi-bag-fill::before { content: "\f174"; } +.bi-bag-plus-fill::before { content: "\f175"; } +.bi-bag-plus::before { content: "\f176"; } +.bi-bag-x-fill::before { content: "\f177"; } +.bi-bag-x::before { content: "\f178"; } +.bi-bag::before { content: "\f179"; } +.bi-bar-chart-fill::before { content: "\f17a"; } +.bi-bar-chart-line-fill::before { content: "\f17b"; } +.bi-bar-chart-line::before { content: "\f17c"; } +.bi-bar-chart-steps::before { content: "\f17d"; } +.bi-bar-chart::before { content: "\f17e"; } +.bi-basket-fill::before { content: "\f17f"; } +.bi-basket::before { content: "\f180"; } +.bi-basket2-fill::before { content: "\f181"; } +.bi-basket2::before { content: "\f182"; } +.bi-basket3-fill::before { content: "\f183"; } +.bi-basket3::before { content: "\f184"; } +.bi-battery-charging::before { content: "\f185"; } +.bi-battery-full::before { content: "\f186"; } +.bi-battery-half::before { content: "\f187"; } +.bi-battery::before { content: "\f188"; } +.bi-bell-fill::before { content: "\f189"; } +.bi-bell::before { content: "\f18a"; } +.bi-bezier::before { content: "\f18b"; } +.bi-bezier2::before { content: "\f18c"; } +.bi-bicycle::before { content: "\f18d"; } +.bi-binoculars-fill::before { content: "\f18e"; } +.bi-binoculars::before { content: "\f18f"; } +.bi-blockquote-left::before { content: "\f190"; } +.bi-blockquote-right::before { content: "\f191"; } +.bi-book-fill::before { content: "\f192"; } +.bi-book-half::before { content: "\f193"; } +.bi-book::before { content: "\f194"; } +.bi-bookmark-check-fill::before { content: "\f195"; } +.bi-bookmark-check::before { content: "\f196"; } +.bi-bookmark-dash-fill::before { content: "\f197"; } +.bi-bookmark-dash::before { content: "\f198"; } +.bi-bookmark-fill::before { content: "\f199"; } +.bi-bookmark-heart-fill::before { content: "\f19a"; } +.bi-bookmark-heart::before { content: "\f19b"; } +.bi-bookmark-plus-fill::before { content: "\f19c"; } +.bi-bookmark-plus::before { content: "\f19d"; } +.bi-bookmark-star-fill::before { content: "\f19e"; } +.bi-bookmark-star::before { content: "\f19f"; } +.bi-bookmark-x-fill::before { content: "\f1a0"; } +.bi-bookmark-x::before { content: "\f1a1"; } +.bi-bookmark::before { content: "\f1a2"; } +.bi-bookmarks-fill::before { content: "\f1a3"; } +.bi-bookmarks::before { content: "\f1a4"; } +.bi-bookshelf::before { content: "\f1a5"; } +.bi-bootstrap-fill::before { content: "\f1a6"; } +.bi-bootstrap-reboot::before { content: "\f1a7"; } +.bi-bootstrap::before { content: "\f1a8"; } +.bi-border-all::before { content: "\f1a9"; } +.bi-border-bottom::before { content: "\f1aa"; } +.bi-border-center::before { content: "\f1ab"; } +.bi-border-inner::before { content: "\f1ac"; } +.bi-border-left::before { content: "\f1ad"; } +.bi-border-middle::before { content: "\f1ae"; } +.bi-border-outer::before { content: "\f1af"; } +.bi-border-right::before { content: "\f1b0"; } +.bi-border-style::before { content: "\f1b1"; } +.bi-border-top::before { content: "\f1b2"; } +.bi-border-width::before { content: "\f1b3"; } +.bi-border::before { content: "\f1b4"; } +.bi-bounding-box-circles::before { content: "\f1b5"; } +.bi-bounding-box::before { content: "\f1b6"; } +.bi-box-arrow-down-left::before { content: "\f1b7"; } +.bi-box-arrow-down-right::before { content: "\f1b8"; } +.bi-box-arrow-down::before { content: "\f1b9"; } +.bi-box-arrow-in-down-left::before { content: "\f1ba"; } +.bi-box-arrow-in-down-right::before { content: "\f1bb"; } +.bi-box-arrow-in-down::before { content: "\f1bc"; } +.bi-box-arrow-in-left::before { content: "\f1bd"; } +.bi-box-arrow-in-right::before { content: "\f1be"; } +.bi-box-arrow-in-up-left::before { content: "\f1bf"; } +.bi-box-arrow-in-up-right::before { content: "\f1c0"; } +.bi-box-arrow-in-up::before { content: "\f1c1"; } +.bi-box-arrow-left::before { content: "\f1c2"; } +.bi-box-arrow-right::before { content: "\f1c3"; } +.bi-box-arrow-up-left::before { content: "\f1c4"; } +.bi-box-arrow-up-right::before { content: "\f1c5"; } +.bi-box-arrow-up::before { content: "\f1c6"; } +.bi-box-seam::before { content: "\f1c7"; } +.bi-box::before { content: "\f1c8"; } +.bi-braces::before { content: "\f1c9"; } +.bi-bricks::before { content: "\f1ca"; } +.bi-briefcase-fill::before { content: "\f1cb"; } +.bi-briefcase::before { content: "\f1cc"; } +.bi-brightness-alt-high-fill::before { content: "\f1cd"; } +.bi-brightness-alt-high::before { content: "\f1ce"; } +.bi-brightness-alt-low-fill::before { content: "\f1cf"; } +.bi-brightness-alt-low::before { content: "\f1d0"; } +.bi-brightness-high-fill::before { content: "\f1d1"; } +.bi-brightness-high::before { content: "\f1d2"; } +.bi-brightness-low-fill::before { content: "\f1d3"; } +.bi-brightness-low::before { content: "\f1d4"; } +.bi-broadcast-pin::before { content: "\f1d5"; } +.bi-broadcast::before { content: "\f1d6"; } +.bi-brush-fill::before { content: "\f1d7"; } +.bi-brush::before { content: "\f1d8"; } +.bi-bucket-fill::before { content: "\f1d9"; } +.bi-bucket::before { content: "\f1da"; } +.bi-bug-fill::before { content: "\f1db"; } +.bi-bug::before { content: "\f1dc"; } +.bi-building::before { content: "\f1dd"; } +.bi-bullseye::before { content: "\f1de"; } +.bi-calculator-fill::before { content: "\f1df"; } +.bi-calculator::before { content: "\f1e0"; } +.bi-calendar-check-fill::before { content: "\f1e1"; } +.bi-calendar-check::before { content: "\f1e2"; } +.bi-calendar-date-fill::before { content: "\f1e3"; } +.bi-calendar-date::before { content: "\f1e4"; } +.bi-calendar-day-fill::before { content: "\f1e5"; } +.bi-calendar-day::before { content: "\f1e6"; } +.bi-calendar-event-fill::before { content: "\f1e7"; } +.bi-calendar-event::before { content: "\f1e8"; } +.bi-calendar-fill::before { content: "\f1e9"; } +.bi-calendar-minus-fill::before { content: "\f1ea"; } +.bi-calendar-minus::before { content: "\f1eb"; } +.bi-calendar-month-fill::before { content: "\f1ec"; } +.bi-calendar-month::before { content: "\f1ed"; } +.bi-calendar-plus-fill::before { content: "\f1ee"; } +.bi-calendar-plus::before { content: "\f1ef"; } +.bi-calendar-range-fill::before { content: "\f1f0"; } +.bi-calendar-range::before { content: "\f1f1"; } +.bi-calendar-week-fill::before { content: "\f1f2"; } +.bi-calendar-week::before { content: "\f1f3"; } +.bi-calendar-x-fill::before { content: "\f1f4"; } +.bi-calendar-x::before { content: "\f1f5"; } +.bi-calendar::before { content: "\f1f6"; } +.bi-calendar2-check-fill::before { content: "\f1f7"; } +.bi-calendar2-check::before { content: "\f1f8"; } +.bi-calendar2-date-fill::before { content: "\f1f9"; } +.bi-calendar2-date::before { content: "\f1fa"; } +.bi-calendar2-day-fill::before { content: "\f1fb"; } +.bi-calendar2-day::before { content: "\f1fc"; } +.bi-calendar2-event-fill::before { content: "\f1fd"; } +.bi-calendar2-event::before { content: "\f1fe"; } +.bi-calendar2-fill::before { content: "\f1ff"; } +.bi-calendar2-minus-fill::before { content: "\f200"; } +.bi-calendar2-minus::before { content: "\f201"; } +.bi-calendar2-month-fill::before { content: "\f202"; } +.bi-calendar2-month::before { content: "\f203"; } +.bi-calendar2-plus-fill::before { content: "\f204"; } +.bi-calendar2-plus::before { content: "\f205"; } +.bi-calendar2-range-fill::before { content: "\f206"; } +.bi-calendar2-range::before { content: "\f207"; } +.bi-calendar2-week-fill::before { content: "\f208"; } +.bi-calendar2-week::before { content: "\f209"; } +.bi-calendar2-x-fill::before { content: "\f20a"; } +.bi-calendar2-x::before { content: "\f20b"; } +.bi-calendar2::before { content: "\f20c"; } +.bi-calendar3-event-fill::before { content: "\f20d"; } +.bi-calendar3-event::before { content: "\f20e"; } +.bi-calendar3-fill::before { content: "\f20f"; } +.bi-calendar3-range-fill::before { content: "\f210"; } +.bi-calendar3-range::before { content: "\f211"; } +.bi-calendar3-week-fill::before { content: "\f212"; } +.bi-calendar3-week::before { content: "\f213"; } +.bi-calendar3::before { content: "\f214"; } +.bi-calendar4-event::before { content: "\f215"; } +.bi-calendar4-range::before { content: "\f216"; } +.bi-calendar4-week::before { content: "\f217"; } +.bi-calendar4::before { content: "\f218"; } +.bi-camera-fill::before { content: "\f219"; } +.bi-camera-reels-fill::before { content: "\f21a"; } +.bi-camera-reels::before { content: "\f21b"; } +.bi-camera-video-fill::before { content: "\f21c"; } +.bi-camera-video-off-fill::before { content: "\f21d"; } +.bi-camera-video-off::before { content: "\f21e"; } +.bi-camera-video::before { content: "\f21f"; } +.bi-camera::before { content: "\f220"; } +.bi-camera2::before { content: "\f221"; } +.bi-capslock-fill::before { content: "\f222"; } +.bi-capslock::before { content: "\f223"; } +.bi-card-checklist::before { content: "\f224"; } +.bi-card-heading::before { content: "\f225"; } +.bi-card-image::before { content: "\f226"; } +.bi-card-list::before { content: "\f227"; } +.bi-card-text::before { content: "\f228"; } +.bi-caret-down-fill::before { content: "\f229"; } +.bi-caret-down-square-fill::before { content: "\f22a"; } +.bi-caret-down-square::before { content: "\f22b"; } +.bi-caret-down::before { content: "\f22c"; } +.bi-caret-left-fill::before { content: "\f22d"; } +.bi-caret-left-square-fill::before { content: "\f22e"; } +.bi-caret-left-square::before { content: "\f22f"; } +.bi-caret-left::before { content: "\f230"; } +.bi-caret-right-fill::before { content: "\f231"; } +.bi-caret-right-square-fill::before { content: "\f232"; } +.bi-caret-right-square::before { content: "\f233"; } +.bi-caret-right::before { content: "\f234"; } +.bi-caret-up-fill::before { content: "\f235"; } +.bi-caret-up-square-fill::before { content: "\f236"; } +.bi-caret-up-square::before { content: "\f237"; } +.bi-caret-up::before { content: "\f238"; } +.bi-cart-check-fill::before { content: "\f239"; } +.bi-cart-check::before { content: "\f23a"; } +.bi-cart-dash-fill::before { content: "\f23b"; } +.bi-cart-dash::before { content: "\f23c"; } +.bi-cart-fill::before { content: "\f23d"; } +.bi-cart-plus-fill::before { content: "\f23e"; } +.bi-cart-plus::before { content: "\f23f"; } +.bi-cart-x-fill::before { content: "\f240"; } +.bi-cart-x::before { content: "\f241"; } +.bi-cart::before { content: "\f242"; } +.bi-cart2::before { content: "\f243"; } +.bi-cart3::before { content: "\f244"; } +.bi-cart4::before { content: "\f245"; } +.bi-cash-stack::before { content: "\f246"; } +.bi-cash::before { content: "\f247"; } +.bi-cast::before { content: "\f248"; } +.bi-chat-dots-fill::before { content: "\f249"; } +.bi-chat-dots::before { content: "\f24a"; } +.bi-chat-fill::before { content: "\f24b"; } +.bi-chat-left-dots-fill::before { content: "\f24c"; } +.bi-chat-left-dots::before { content: "\f24d"; } +.bi-chat-left-fill::before { content: "\f24e"; } +.bi-chat-left-quote-fill::before { content: "\f24f"; } +.bi-chat-left-quote::before { content: "\f250"; } +.bi-chat-left-text-fill::before { content: "\f251"; } +.bi-chat-left-text::before { content: "\f252"; } +.bi-chat-left::before { content: "\f253"; } +.bi-chat-quote-fill::before { content: "\f254"; } +.bi-chat-quote::before { content: "\f255"; } +.bi-chat-right-dots-fill::before { content: "\f256"; } +.bi-chat-right-dots::before { content: "\f257"; } +.bi-chat-right-fill::before { content: "\f258"; } +.bi-chat-right-quote-fill::before { content: "\f259"; } +.bi-chat-right-quote::before { content: "\f25a"; } +.bi-chat-right-text-fill::before { content: "\f25b"; } +.bi-chat-right-text::before { content: "\f25c"; } +.bi-chat-right::before { content: "\f25d"; } +.bi-chat-square-dots-fill::before { content: "\f25e"; } +.bi-chat-square-dots::before { content: "\f25f"; } +.bi-chat-square-fill::before { content: "\f260"; } +.bi-chat-square-quote-fill::before { content: "\f261"; } +.bi-chat-square-quote::before { content: "\f262"; } +.bi-chat-square-text-fill::before { content: "\f263"; } +.bi-chat-square-text::before { content: "\f264"; } +.bi-chat-square::before { content: "\f265"; } +.bi-chat-text-fill::before { content: "\f266"; } +.bi-chat-text::before { content: "\f267"; } +.bi-chat::before { content: "\f268"; } +.bi-check-all::before { content: "\f269"; } +.bi-check-circle-fill::before { content: "\f26a"; } +.bi-check-circle::before { content: "\f26b"; } +.bi-check-square-fill::before { content: "\f26c"; } +.bi-check-square::before { content: "\f26d"; } +.bi-check::before { content: "\f26e"; } +.bi-check2-all::before { content: "\f26f"; } +.bi-check2-circle::before { content: "\f270"; } +.bi-check2-square::before { content: "\f271"; } +.bi-check2::before { content: "\f272"; } +.bi-chevron-bar-contract::before { content: "\f273"; } +.bi-chevron-bar-down::before { content: "\f274"; } +.bi-chevron-bar-expand::before { content: "\f275"; } +.bi-chevron-bar-left::before { content: "\f276"; } +.bi-chevron-bar-right::before { content: "\f277"; } +.bi-chevron-bar-up::before { content: "\f278"; } +.bi-chevron-compact-down::before { content: "\f279"; } +.bi-chevron-compact-left::before { content: "\f27a"; } +.bi-chevron-compact-right::before { content: "\f27b"; } +.bi-chevron-compact-up::before { content: "\f27c"; } +.bi-chevron-contract::before { content: "\f27d"; } +.bi-chevron-double-down::before { content: "\f27e"; } +.bi-chevron-double-left::before { content: "\f27f"; } +.bi-chevron-double-right::before { content: "\f280"; } +.bi-chevron-double-up::before { content: "\f281"; } +.bi-chevron-down::before { content: "\f282"; } +.bi-chevron-expand::before { content: "\f283"; } +.bi-chevron-left::before { content: "\f284"; } +.bi-chevron-right::before { content: "\f285"; } +.bi-chevron-up::before { content: "\f286"; } +.bi-circle-fill::before { content: "\f287"; } +.bi-circle-half::before { content: "\f288"; } +.bi-circle-square::before { content: "\f289"; } +.bi-circle::before { content: "\f28a"; } +.bi-clipboard-check::before { content: "\f28b"; } +.bi-clipboard-data::before { content: "\f28c"; } +.bi-clipboard-minus::before { content: "\f28d"; } +.bi-clipboard-plus::before { content: "\f28e"; } +.bi-clipboard-x::before { content: "\f28f"; } +.bi-clipboard::before { content: "\f290"; } +.bi-clock-fill::before { content: "\f291"; } +.bi-clock-history::before { content: "\f292"; } +.bi-clock::before { content: "\f293"; } +.bi-cloud-arrow-down-fill::before { content: "\f294"; } +.bi-cloud-arrow-down::before { content: "\f295"; } +.bi-cloud-arrow-up-fill::before { content: "\f296"; } +.bi-cloud-arrow-up::before { content: "\f297"; } +.bi-cloud-check-fill::before { content: "\f298"; } +.bi-cloud-check::before { content: "\f299"; } +.bi-cloud-download-fill::before { content: "\f29a"; } +.bi-cloud-download::before { content: "\f29b"; } +.bi-cloud-drizzle-fill::before { content: "\f29c"; } +.bi-cloud-drizzle::before { content: "\f29d"; } +.bi-cloud-fill::before { content: "\f29e"; } +.bi-cloud-fog-fill::before { content: "\f29f"; } +.bi-cloud-fog::before { content: "\f2a0"; } +.bi-cloud-fog2-fill::before { content: "\f2a1"; } +.bi-cloud-fog2::before { content: "\f2a2"; } +.bi-cloud-hail-fill::before { content: "\f2a3"; } +.bi-cloud-hail::before { content: "\f2a4"; } +.bi-cloud-haze-fill::before { content: "\f2a6"; } +.bi-cloud-haze::before { content: "\f2a7"; } +.bi-cloud-haze2-fill::before { content: "\f2a8"; } +.bi-cloud-lightning-fill::before { content: "\f2a9"; } +.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } +.bi-cloud-lightning-rain::before { content: "\f2ab"; } +.bi-cloud-lightning::before { content: "\f2ac"; } +.bi-cloud-minus-fill::before { content: "\f2ad"; } +.bi-cloud-minus::before { content: "\f2ae"; } +.bi-cloud-moon-fill::before { content: "\f2af"; } +.bi-cloud-moon::before { content: "\f2b0"; } +.bi-cloud-plus-fill::before { content: "\f2b1"; } +.bi-cloud-plus::before { content: "\f2b2"; } +.bi-cloud-rain-fill::before { content: "\f2b3"; } +.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } +.bi-cloud-rain-heavy::before { content: "\f2b5"; } +.bi-cloud-rain::before { content: "\f2b6"; } +.bi-cloud-slash-fill::before { content: "\f2b7"; } +.bi-cloud-slash::before { content: "\f2b8"; } +.bi-cloud-sleet-fill::before { content: "\f2b9"; } +.bi-cloud-sleet::before { content: "\f2ba"; } +.bi-cloud-snow-fill::before { content: "\f2bb"; } +.bi-cloud-snow::before { content: "\f2bc"; } +.bi-cloud-sun-fill::before { content: "\f2bd"; } +.bi-cloud-sun::before { content: "\f2be"; } +.bi-cloud-upload-fill::before { content: "\f2bf"; } +.bi-cloud-upload::before { content: "\f2c0"; } +.bi-cloud::before { content: "\f2c1"; } +.bi-clouds-fill::before { content: "\f2c2"; } +.bi-clouds::before { content: "\f2c3"; } +.bi-cloudy-fill::before { content: "\f2c4"; } +.bi-cloudy::before { content: "\f2c5"; } +.bi-code-slash::before { content: "\f2c6"; } +.bi-code-square::before { content: "\f2c7"; } +.bi-code::before { content: "\f2c8"; } +.bi-collection-fill::before { content: "\f2c9"; } +.bi-collection-play-fill::before { content: "\f2ca"; } +.bi-collection-play::before { content: "\f2cb"; } +.bi-collection::before { content: "\f2cc"; } +.bi-columns-gap::before { content: "\f2cd"; } +.bi-columns::before { content: "\f2ce"; } +.bi-command::before { content: "\f2cf"; } +.bi-compass-fill::before { content: "\f2d0"; } +.bi-compass::before { content: "\f2d1"; } +.bi-cone-striped::before { content: "\f2d2"; } +.bi-cone::before { content: "\f2d3"; } +.bi-controller::before { content: "\f2d4"; } +.bi-cpu-fill::before { content: "\f2d5"; } +.bi-cpu::before { content: "\f2d6"; } +.bi-credit-card-2-back-fill::before { content: "\f2d7"; } +.bi-credit-card-2-back::before { content: "\f2d8"; } +.bi-credit-card-2-front-fill::before { content: "\f2d9"; } +.bi-credit-card-2-front::before { content: "\f2da"; } +.bi-credit-card-fill::before { content: "\f2db"; } +.bi-credit-card::before { content: "\f2dc"; } +.bi-crop::before { content: "\f2dd"; } +.bi-cup-fill::before { content: "\f2de"; } +.bi-cup-straw::before { content: "\f2df"; } +.bi-cup::before { content: "\f2e0"; } +.bi-cursor-fill::before { content: "\f2e1"; } +.bi-cursor-text::before { content: "\f2e2"; } +.bi-cursor::before { content: "\f2e3"; } +.bi-dash-circle-dotted::before { content: "\f2e4"; } +.bi-dash-circle-fill::before { content: "\f2e5"; } +.bi-dash-circle::before { content: "\f2e6"; } +.bi-dash-square-dotted::before { content: "\f2e7"; } +.bi-dash-square-fill::before { content: "\f2e8"; } +.bi-dash-square::before { content: "\f2e9"; } +.bi-dash::before { content: "\f2ea"; } +.bi-diagram-2-fill::before { content: "\f2eb"; } +.bi-diagram-2::before { content: "\f2ec"; } +.bi-diagram-3-fill::before { content: "\f2ed"; } +.bi-diagram-3::before { content: "\f2ee"; } +.bi-diamond-fill::before { content: "\f2ef"; } +.bi-diamond-half::before { content: "\f2f0"; } +.bi-diamond::before { content: "\f2f1"; } +.bi-dice-1-fill::before { content: "\f2f2"; } +.bi-dice-1::before { content: "\f2f3"; } +.bi-dice-2-fill::before { content: "\f2f4"; } +.bi-dice-2::before { content: "\f2f5"; } +.bi-dice-3-fill::before { content: "\f2f6"; } +.bi-dice-3::before { content: "\f2f7"; } +.bi-dice-4-fill::before { content: "\f2f8"; } +.bi-dice-4::before { content: "\f2f9"; } +.bi-dice-5-fill::before { content: "\f2fa"; } +.bi-dice-5::before { content: "\f2fb"; } +.bi-dice-6-fill::before { content: "\f2fc"; } +.bi-dice-6::before { content: "\f2fd"; } +.bi-disc-fill::before { content: "\f2fe"; } +.bi-disc::before { content: "\f2ff"; } +.bi-discord::before { content: "\f300"; } +.bi-display-fill::before { content: "\f301"; } +.bi-display::before { content: "\f302"; } +.bi-distribute-horizontal::before { content: "\f303"; } +.bi-distribute-vertical::before { content: "\f304"; } +.bi-door-closed-fill::before { content: "\f305"; } +.bi-door-closed::before { content: "\f306"; } +.bi-door-open-fill::before { content: "\f307"; } +.bi-door-open::before { content: "\f308"; } +.bi-dot::before { content: "\f309"; } +.bi-download::before { content: "\f30a"; } +.bi-droplet-fill::before { content: "\f30b"; } +.bi-droplet-half::before { content: "\f30c"; } +.bi-droplet::before { content: "\f30d"; } +.bi-earbuds::before { content: "\f30e"; } +.bi-easel-fill::before { content: "\f30f"; } +.bi-easel::before { content: "\f310"; } +.bi-egg-fill::before { content: "\f311"; } +.bi-egg-fried::before { content: "\f312"; } +.bi-egg::before { content: "\f313"; } +.bi-eject-fill::before { content: "\f314"; } +.bi-eject::before { content: "\f315"; } +.bi-emoji-angry-fill::before { content: "\f316"; } +.bi-emoji-angry::before { content: "\f317"; } +.bi-emoji-dizzy-fill::before { content: "\f318"; } +.bi-emoji-dizzy::before { content: "\f319"; } +.bi-emoji-expressionless-fill::before { content: "\f31a"; } +.bi-emoji-expressionless::before { content: "\f31b"; } +.bi-emoji-frown-fill::before { content: "\f31c"; } +.bi-emoji-frown::before { content: "\f31d"; } +.bi-emoji-heart-eyes-fill::before { content: "\f31e"; } +.bi-emoji-heart-eyes::before { content: "\f31f"; } +.bi-emoji-laughing-fill::before { content: "\f320"; } +.bi-emoji-laughing::before { content: "\f321"; } +.bi-emoji-neutral-fill::before { content: "\f322"; } +.bi-emoji-neutral::before { content: "\f323"; } +.bi-emoji-smile-fill::before { content: "\f324"; } +.bi-emoji-smile-upside-down-fill::before { content: "\f325"; } +.bi-emoji-smile-upside-down::before { content: "\f326"; } +.bi-emoji-smile::before { content: "\f327"; } +.bi-emoji-sunglasses-fill::before { content: "\f328"; } +.bi-emoji-sunglasses::before { content: "\f329"; } +.bi-emoji-wink-fill::before { content: "\f32a"; } +.bi-emoji-wink::before { content: "\f32b"; } +.bi-envelope-fill::before { content: "\f32c"; } +.bi-envelope-open-fill::before { content: "\f32d"; } +.bi-envelope-open::before { content: "\f32e"; } +.bi-envelope::before { content: "\f32f"; } +.bi-eraser-fill::before { content: "\f330"; } +.bi-eraser::before { content: "\f331"; } +.bi-exclamation-circle-fill::before { content: "\f332"; } +.bi-exclamation-circle::before { content: "\f333"; } +.bi-exclamation-diamond-fill::before { content: "\f334"; } +.bi-exclamation-diamond::before { content: "\f335"; } +.bi-exclamation-octagon-fill::before { content: "\f336"; } +.bi-exclamation-octagon::before { content: "\f337"; } +.bi-exclamation-square-fill::before { content: "\f338"; } +.bi-exclamation-square::before { content: "\f339"; } +.bi-exclamation-triangle-fill::before { content: "\f33a"; } +.bi-exclamation-triangle::before { content: "\f33b"; } +.bi-exclamation::before { content: "\f33c"; } +.bi-exclude::before { content: "\f33d"; } +.bi-eye-fill::before { content: "\f33e"; } +.bi-eye-slash-fill::before { content: "\f33f"; } +.bi-eye-slash::before { content: "\f340"; } +.bi-eye::before { content: "\f341"; } +.bi-eyedropper::before { content: "\f342"; } +.bi-eyeglasses::before { content: "\f343"; } +.bi-facebook::before { content: "\f344"; } +.bi-file-arrow-down-fill::before { content: "\f345"; } +.bi-file-arrow-down::before { content: "\f346"; } +.bi-file-arrow-up-fill::before { content: "\f347"; } +.bi-file-arrow-up::before { content: "\f348"; } +.bi-file-bar-graph-fill::before { content: "\f349"; } +.bi-file-bar-graph::before { content: "\f34a"; } +.bi-file-binary-fill::before { content: "\f34b"; } +.bi-file-binary::before { content: "\f34c"; } +.bi-file-break-fill::before { content: "\f34d"; } +.bi-file-break::before { content: "\f34e"; } +.bi-file-check-fill::before { content: "\f34f"; } +.bi-file-check::before { content: "\f350"; } +.bi-file-code-fill::before { content: "\f351"; } +.bi-file-code::before { content: "\f352"; } +.bi-file-diff-fill::before { content: "\f353"; } +.bi-file-diff::before { content: "\f354"; } +.bi-file-earmark-arrow-down-fill::before { content: "\f355"; } +.bi-file-earmark-arrow-down::before { content: "\f356"; } +.bi-file-earmark-arrow-up-fill::before { content: "\f357"; } +.bi-file-earmark-arrow-up::before { content: "\f358"; } +.bi-file-earmark-bar-graph-fill::before { content: "\f359"; } +.bi-file-earmark-bar-graph::before { content: "\f35a"; } +.bi-file-earmark-binary-fill::before { content: "\f35b"; } +.bi-file-earmark-binary::before { content: "\f35c"; } +.bi-file-earmark-break-fill::before { content: "\f35d"; } +.bi-file-earmark-break::before { content: "\f35e"; } +.bi-file-earmark-check-fill::before { content: "\f35f"; } +.bi-file-earmark-check::before { content: "\f360"; } +.bi-file-earmark-code-fill::before { content: "\f361"; } +.bi-file-earmark-code::before { content: "\f362"; } +.bi-file-earmark-diff-fill::before { content: "\f363"; } +.bi-file-earmark-diff::before { content: "\f364"; } +.bi-file-earmark-easel-fill::before { content: "\f365"; } +.bi-file-earmark-easel::before { content: "\f366"; } +.bi-file-earmark-excel-fill::before { content: "\f367"; } +.bi-file-earmark-excel::before { content: "\f368"; } +.bi-file-earmark-fill::before { content: "\f369"; } +.bi-file-earmark-font-fill::before { content: "\f36a"; } +.bi-file-earmark-font::before { content: "\f36b"; } +.bi-file-earmark-image-fill::before { content: "\f36c"; } +.bi-file-earmark-image::before { content: "\f36d"; } +.bi-file-earmark-lock-fill::before { content: "\f36e"; } +.bi-file-earmark-lock::before { content: "\f36f"; } +.bi-file-earmark-lock2-fill::before { content: "\f370"; } +.bi-file-earmark-lock2::before { content: "\f371"; } +.bi-file-earmark-medical-fill::before { content: "\f372"; } +.bi-file-earmark-medical::before { content: "\f373"; } +.bi-file-earmark-minus-fill::before { content: "\f374"; } +.bi-file-earmark-minus::before { content: "\f375"; } +.bi-file-earmark-music-fill::before { content: "\f376"; } +.bi-file-earmark-music::before { content: "\f377"; } +.bi-file-earmark-person-fill::before { content: "\f378"; } +.bi-file-earmark-person::before { content: "\f379"; } +.bi-file-earmark-play-fill::before { content: "\f37a"; } +.bi-file-earmark-play::before { content: "\f37b"; } +.bi-file-earmark-plus-fill::before { content: "\f37c"; } +.bi-file-earmark-plus::before { content: "\f37d"; } +.bi-file-earmark-post-fill::before { content: "\f37e"; } +.bi-file-earmark-post::before { content: "\f37f"; } +.bi-file-earmark-ppt-fill::before { content: "\f380"; } +.bi-file-earmark-ppt::before { content: "\f381"; } +.bi-file-earmark-richtext-fill::before { content: "\f382"; } +.bi-file-earmark-richtext::before { content: "\f383"; } +.bi-file-earmark-ruled-fill::before { content: "\f384"; } +.bi-file-earmark-ruled::before { content: "\f385"; } +.bi-file-earmark-slides-fill::before { content: "\f386"; } +.bi-file-earmark-slides::before { content: "\f387"; } +.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } +.bi-file-earmark-spreadsheet::before { content: "\f389"; } +.bi-file-earmark-text-fill::before { content: "\f38a"; } +.bi-file-earmark-text::before { content: "\f38b"; } +.bi-file-earmark-word-fill::before { content: "\f38c"; } +.bi-file-earmark-word::before { content: "\f38d"; } +.bi-file-earmark-x-fill::before { content: "\f38e"; } +.bi-file-earmark-x::before { content: "\f38f"; } +.bi-file-earmark-zip-fill::before { content: "\f390"; } +.bi-file-earmark-zip::before { content: "\f391"; } +.bi-file-earmark::before { content: "\f392"; } +.bi-file-easel-fill::before { content: "\f393"; } +.bi-file-easel::before { content: "\f394"; } +.bi-file-excel-fill::before { content: "\f395"; } +.bi-file-excel::before { content: "\f396"; } +.bi-file-fill::before { content: "\f397"; } +.bi-file-font-fill::before { content: "\f398"; } +.bi-file-font::before { content: "\f399"; } +.bi-file-image-fill::before { content: "\f39a"; } +.bi-file-image::before { content: "\f39b"; } +.bi-file-lock-fill::before { content: "\f39c"; } +.bi-file-lock::before { content: "\f39d"; } +.bi-file-lock2-fill::before { content: "\f39e"; } +.bi-file-lock2::before { content: "\f39f"; } +.bi-file-medical-fill::before { content: "\f3a0"; } +.bi-file-medical::before { content: "\f3a1"; } +.bi-file-minus-fill::before { content: "\f3a2"; } +.bi-file-minus::before { content: "\f3a3"; } +.bi-file-music-fill::before { content: "\f3a4"; } +.bi-file-music::before { content: "\f3a5"; } +.bi-file-person-fill::before { content: "\f3a6"; } +.bi-file-person::before { content: "\f3a7"; } +.bi-file-play-fill::before { content: "\f3a8"; } +.bi-file-play::before { content: "\f3a9"; } +.bi-file-plus-fill::before { content: "\f3aa"; } +.bi-file-plus::before { content: "\f3ab"; } +.bi-file-post-fill::before { content: "\f3ac"; } +.bi-file-post::before { content: "\f3ad"; } +.bi-file-ppt-fill::before { content: "\f3ae"; } +.bi-file-ppt::before { content: "\f3af"; } +.bi-file-richtext-fill::before { content: "\f3b0"; } +.bi-file-richtext::before { content: "\f3b1"; } +.bi-file-ruled-fill::before { content: "\f3b2"; } +.bi-file-ruled::before { content: "\f3b3"; } +.bi-file-slides-fill::before { content: "\f3b4"; } +.bi-file-slides::before { content: "\f3b5"; } +.bi-file-spreadsheet-fill::before { content: "\f3b6"; } +.bi-file-spreadsheet::before { content: "\f3b7"; } +.bi-file-text-fill::before { content: "\f3b8"; } +.bi-file-text::before { content: "\f3b9"; } +.bi-file-word-fill::before { content: "\f3ba"; } +.bi-file-word::before { content: "\f3bb"; } +.bi-file-x-fill::before { content: "\f3bc"; } +.bi-file-x::before { content: "\f3bd"; } +.bi-file-zip-fill::before { content: "\f3be"; } +.bi-file-zip::before { content: "\f3bf"; } +.bi-file::before { content: "\f3c0"; } +.bi-files-alt::before { content: "\f3c1"; } +.bi-files::before { content: "\f3c2"; } +.bi-film::before { content: "\f3c3"; } +.bi-filter-circle-fill::before { content: "\f3c4"; } +.bi-filter-circle::before { content: "\f3c5"; } +.bi-filter-left::before { content: "\f3c6"; } +.bi-filter-right::before { content: "\f3c7"; } +.bi-filter-square-fill::before { content: "\f3c8"; } +.bi-filter-square::before { content: "\f3c9"; } +.bi-filter::before { content: "\f3ca"; } +.bi-flag-fill::before { content: "\f3cb"; } +.bi-flag::before { content: "\f3cc"; } +.bi-flower1::before { content: "\f3cd"; } +.bi-flower2::before { content: "\f3ce"; } +.bi-flower3::before { content: "\f3cf"; } +.bi-folder-check::before { content: "\f3d0"; } +.bi-folder-fill::before { content: "\f3d1"; } +.bi-folder-minus::before { content: "\f3d2"; } +.bi-folder-plus::before { content: "\f3d3"; } +.bi-folder-symlink-fill::before { content: "\f3d4"; } +.bi-folder-symlink::before { content: "\f3d5"; } +.bi-folder-x::before { content: "\f3d6"; } +.bi-folder::before { content: "\f3d7"; } +.bi-folder2-open::before { content: "\f3d8"; } +.bi-folder2::before { content: "\f3d9"; } +.bi-fonts::before { content: "\f3da"; } +.bi-forward-fill::before { content: "\f3db"; } +.bi-forward::before { content: "\f3dc"; } +.bi-front::before { content: "\f3dd"; } +.bi-fullscreen-exit::before { content: "\f3de"; } +.bi-fullscreen::before { content: "\f3df"; } +.bi-funnel-fill::before { content: "\f3e0"; } +.bi-funnel::before { content: "\f3e1"; } +.bi-gear-fill::before { content: "\f3e2"; } +.bi-gear-wide-connected::before { content: "\f3e3"; } +.bi-gear-wide::before { content: "\f3e4"; } +.bi-gear::before { content: "\f3e5"; } +.bi-gem::before { content: "\f3e6"; } +.bi-geo-alt-fill::before { content: "\f3e7"; } +.bi-geo-alt::before { content: "\f3e8"; } +.bi-geo-fill::before { content: "\f3e9"; } +.bi-geo::before { content: "\f3ea"; } +.bi-gift-fill::before { content: "\f3eb"; } +.bi-gift::before { content: "\f3ec"; } +.bi-github::before { content: "\f3ed"; } +.bi-globe::before { content: "\f3ee"; } +.bi-globe2::before { content: "\f3ef"; } +.bi-google::before { content: "\f3f0"; } +.bi-graph-down::before { content: "\f3f1"; } +.bi-graph-up::before { content: "\f3f2"; } +.bi-grid-1x2-fill::before { content: "\f3f3"; } +.bi-grid-1x2::before { content: "\f3f4"; } +.bi-grid-3x2-gap-fill::before { content: "\f3f5"; } +.bi-grid-3x2-gap::before { content: "\f3f6"; } +.bi-grid-3x2::before { content: "\f3f7"; } +.bi-grid-3x3-gap-fill::before { content: "\f3f8"; } +.bi-grid-3x3-gap::before { content: "\f3f9"; } +.bi-grid-3x3::before { content: "\f3fa"; } +.bi-grid-fill::before { content: "\f3fb"; } +.bi-grid::before { content: "\f3fc"; } +.bi-grip-horizontal::before { content: "\f3fd"; } +.bi-grip-vertical::before { content: "\f3fe"; } +.bi-hammer::before { content: "\f3ff"; } +.bi-hand-index-fill::before { content: "\f400"; } +.bi-hand-index-thumb-fill::before { content: "\f401"; } +.bi-hand-index-thumb::before { content: "\f402"; } +.bi-hand-index::before { content: "\f403"; } +.bi-hand-thumbs-down-fill::before { content: "\f404"; } +.bi-hand-thumbs-down::before { content: "\f405"; } +.bi-hand-thumbs-up-fill::before { content: "\f406"; } +.bi-hand-thumbs-up::before { content: "\f407"; } +.bi-handbag-fill::before { content: "\f408"; } +.bi-handbag::before { content: "\f409"; } +.bi-hash::before { content: "\f40a"; } +.bi-hdd-fill::before { content: "\f40b"; } +.bi-hdd-network-fill::before { content: "\f40c"; } +.bi-hdd-network::before { content: "\f40d"; } +.bi-hdd-rack-fill::before { content: "\f40e"; } +.bi-hdd-rack::before { content: "\f40f"; } +.bi-hdd-stack-fill::before { content: "\f410"; } +.bi-hdd-stack::before { content: "\f411"; } +.bi-hdd::before { content: "\f412"; } +.bi-headphones::before { content: "\f413"; } +.bi-headset::before { content: "\f414"; } +.bi-heart-fill::before { content: "\f415"; } +.bi-heart-half::before { content: "\f416"; } +.bi-heart::before { content: "\f417"; } +.bi-heptagon-fill::before { content: "\f418"; } +.bi-heptagon-half::before { content: "\f419"; } +.bi-heptagon::before { content: "\f41a"; } +.bi-hexagon-fill::before { content: "\f41b"; } +.bi-hexagon-half::before { content: "\f41c"; } +.bi-hexagon::before { content: "\f41d"; } +.bi-hourglass-bottom::before { content: "\f41e"; } +.bi-hourglass-split::before { content: "\f41f"; } +.bi-hourglass-top::before { content: "\f420"; } +.bi-hourglass::before { content: "\f421"; } +.bi-house-door-fill::before { content: "\f422"; } +.bi-house-door::before { content: "\f423"; } +.bi-house-fill::before { content: "\f424"; } +.bi-house::before { content: "\f425"; } +.bi-hr::before { content: "\f426"; } +.bi-hurricane::before { content: "\f427"; } +.bi-image-alt::before { content: "\f428"; } +.bi-image-fill::before { content: "\f429"; } +.bi-image::before { content: "\f42a"; } +.bi-images::before { content: "\f42b"; } +.bi-inbox-fill::before { content: "\f42c"; } +.bi-inbox::before { content: "\f42d"; } +.bi-inboxes-fill::before { content: "\f42e"; } +.bi-inboxes::before { content: "\f42f"; } +.bi-info-circle-fill::before { content: "\f430"; } +.bi-info-circle::before { content: "\f431"; } +.bi-info-square-fill::before { content: "\f432"; } +.bi-info-square::before { content: "\f433"; } +.bi-info::before { content: "\f434"; } +.bi-input-cursor-text::before { content: "\f435"; } +.bi-input-cursor::before { content: "\f436"; } +.bi-instagram::before { content: "\f437"; } +.bi-intersect::before { content: "\f438"; } +.bi-journal-album::before { content: "\f439"; } +.bi-journal-arrow-down::before { content: "\f43a"; } +.bi-journal-arrow-up::before { content: "\f43b"; } +.bi-journal-bookmark-fill::before { content: "\f43c"; } +.bi-journal-bookmark::before { content: "\f43d"; } +.bi-journal-check::before { content: "\f43e"; } +.bi-journal-code::before { content: "\f43f"; } +.bi-journal-medical::before { content: "\f440"; } +.bi-journal-minus::before { content: "\f441"; } +.bi-journal-plus::before { content: "\f442"; } +.bi-journal-richtext::before { content: "\f443"; } +.bi-journal-text::before { content: "\f444"; } +.bi-journal-x::before { content: "\f445"; } +.bi-journal::before { content: "\f446"; } +.bi-journals::before { content: "\f447"; } +.bi-joystick::before { content: "\f448"; } +.bi-justify-left::before { content: "\f449"; } +.bi-justify-right::before { content: "\f44a"; } +.bi-justify::before { content: "\f44b"; } +.bi-kanban-fill::before { content: "\f44c"; } +.bi-kanban::before { content: "\f44d"; } +.bi-key-fill::before { content: "\f44e"; } +.bi-key::before { content: "\f44f"; } +.bi-keyboard-fill::before { content: "\f450"; } +.bi-keyboard::before { content: "\f451"; } +.bi-ladder::before { content: "\f452"; } +.bi-lamp-fill::before { content: "\f453"; } +.bi-lamp::before { content: "\f454"; } +.bi-laptop-fill::before { content: "\f455"; } +.bi-laptop::before { content: "\f456"; } +.bi-layer-backward::before { content: "\f457"; } +.bi-layer-forward::before { content: "\f458"; } +.bi-layers-fill::before { content: "\f459"; } +.bi-layers-half::before { content: "\f45a"; } +.bi-layers::before { content: "\f45b"; } +.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } +.bi-layout-sidebar-inset::before { content: "\f45d"; } +.bi-layout-sidebar-reverse::before { content: "\f45e"; } +.bi-layout-sidebar::before { content: "\f45f"; } +.bi-layout-split::before { content: "\f460"; } +.bi-layout-text-sidebar-reverse::before { content: "\f461"; } +.bi-layout-text-sidebar::before { content: "\f462"; } +.bi-layout-text-window-reverse::before { content: "\f463"; } +.bi-layout-text-window::before { content: "\f464"; } +.bi-layout-three-columns::before { content: "\f465"; } +.bi-layout-wtf::before { content: "\f466"; } +.bi-life-preserver::before { content: "\f467"; } +.bi-lightbulb-fill::before { content: "\f468"; } +.bi-lightbulb-off-fill::before { content: "\f469"; } +.bi-lightbulb-off::before { content: "\f46a"; } +.bi-lightbulb::before { content: "\f46b"; } +.bi-lightning-charge-fill::before { content: "\f46c"; } +.bi-lightning-charge::before { content: "\f46d"; } +.bi-lightning-fill::before { content: "\f46e"; } +.bi-lightning::before { content: "\f46f"; } +.bi-link-45deg::before { content: "\f470"; } +.bi-link::before { content: "\f471"; } +.bi-linkedin::before { content: "\f472"; } +.bi-list-check::before { content: "\f473"; } +.bi-list-nested::before { content: "\f474"; } +.bi-list-ol::before { content: "\f475"; } +.bi-list-stars::before { content: "\f476"; } +.bi-list-task::before { content: "\f477"; } +.bi-list-ul::before { content: "\f478"; } +.bi-list::before { content: "\f479"; } +.bi-lock-fill::before { content: "\f47a"; } +.bi-lock::before { content: "\f47b"; } +.bi-mailbox::before { content: "\f47c"; } +.bi-mailbox2::before { content: "\f47d"; } +.bi-map-fill::before { content: "\f47e"; } +.bi-map::before { content: "\f47f"; } +.bi-markdown-fill::before { content: "\f480"; } +.bi-markdown::before { content: "\f481"; } +.bi-mask::before { content: "\f482"; } +.bi-megaphone-fill::before { content: "\f483"; } +.bi-megaphone::before { content: "\f484"; } +.bi-menu-app-fill::before { content: "\f485"; } +.bi-menu-app::before { content: "\f486"; } +.bi-menu-button-fill::before { content: "\f487"; } +.bi-menu-button-wide-fill::before { content: "\f488"; } +.bi-menu-button-wide::before { content: "\f489"; } +.bi-menu-button::before { content: "\f48a"; } +.bi-menu-down::before { content: "\f48b"; } +.bi-menu-up::before { content: "\f48c"; } +.bi-mic-fill::before { content: "\f48d"; } +.bi-mic-mute-fill::before { content: "\f48e"; } +.bi-mic-mute::before { content: "\f48f"; } +.bi-mic::before { content: "\f490"; } +.bi-minecart-loaded::before { content: "\f491"; } +.bi-minecart::before { content: "\f492"; } +.bi-moisture::before { content: "\f493"; } +.bi-moon-fill::before { content: "\f494"; } +.bi-moon-stars-fill::before { content: "\f495"; } +.bi-moon-stars::before { content: "\f496"; } +.bi-moon::before { content: "\f497"; } +.bi-mouse-fill::before { content: "\f498"; } +.bi-mouse::before { content: "\f499"; } +.bi-mouse2-fill::before { content: "\f49a"; } +.bi-mouse2::before { content: "\f49b"; } +.bi-mouse3-fill::before { content: "\f49c"; } +.bi-mouse3::before { content: "\f49d"; } +.bi-music-note-beamed::before { content: "\f49e"; } +.bi-music-note-list::before { content: "\f49f"; } +.bi-music-note::before { content: "\f4a0"; } +.bi-music-player-fill::before { content: "\f4a1"; } +.bi-music-player::before { content: "\f4a2"; } +.bi-newspaper::before { content: "\f4a3"; } +.bi-node-minus-fill::before { content: "\f4a4"; } +.bi-node-minus::before { content: "\f4a5"; } +.bi-node-plus-fill::before { content: "\f4a6"; } +.bi-node-plus::before { content: "\f4a7"; } +.bi-nut-fill::before { content: "\f4a8"; } +.bi-nut::before { content: "\f4a9"; } +.bi-octagon-fill::before { content: "\f4aa"; } +.bi-octagon-half::before { content: "\f4ab"; } +.bi-octagon::before { content: "\f4ac"; } +.bi-option::before { content: "\f4ad"; } +.bi-outlet::before { content: "\f4ae"; } +.bi-paint-bucket::before { content: "\f4af"; } +.bi-palette-fill::before { content: "\f4b0"; } +.bi-palette::before { content: "\f4b1"; } +.bi-palette2::before { content: "\f4b2"; } +.bi-paperclip::before { content: "\f4b3"; } +.bi-paragraph::before { content: "\f4b4"; } +.bi-patch-check-fill::before { content: "\f4b5"; } +.bi-patch-check::before { content: "\f4b6"; } +.bi-patch-exclamation-fill::before { content: "\f4b7"; } +.bi-patch-exclamation::before { content: "\f4b8"; } +.bi-patch-minus-fill::before { content: "\f4b9"; } +.bi-patch-minus::before { content: "\f4ba"; } +.bi-patch-plus-fill::before { content: "\f4bb"; } +.bi-patch-plus::before { content: "\f4bc"; } +.bi-patch-question-fill::before { content: "\f4bd"; } +.bi-patch-question::before { content: "\f4be"; } +.bi-pause-btn-fill::before { content: "\f4bf"; } +.bi-pause-btn::before { content: "\f4c0"; } +.bi-pause-circle-fill::before { content: "\f4c1"; } +.bi-pause-circle::before { content: "\f4c2"; } +.bi-pause-fill::before { content: "\f4c3"; } +.bi-pause::before { content: "\f4c4"; } +.bi-peace-fill::before { content: "\f4c5"; } +.bi-peace::before { content: "\f4c6"; } +.bi-pen-fill::before { content: "\f4c7"; } +.bi-pen::before { content: "\f4c8"; } +.bi-pencil-fill::before { content: "\f4c9"; } +.bi-pencil-square::before { content: "\f4ca"; } +.bi-pencil::before { content: "\f4cb"; } +.bi-pentagon-fill::before { content: "\f4cc"; } +.bi-pentagon-half::before { content: "\f4cd"; } +.bi-pentagon::before { content: "\f4ce"; } +.bi-people-fill::before { content: "\f4cf"; } +.bi-people::before { content: "\f4d0"; } +.bi-percent::before { content: "\f4d1"; } +.bi-person-badge-fill::before { content: "\f4d2"; } +.bi-person-badge::before { content: "\f4d3"; } +.bi-person-bounding-box::before { content: "\f4d4"; } +.bi-person-check-fill::before { content: "\f4d5"; } +.bi-person-check::before { content: "\f4d6"; } +.bi-person-circle::before { content: "\f4d7"; } +.bi-person-dash-fill::before { content: "\f4d8"; } +.bi-person-dash::before { content: "\f4d9"; } +.bi-person-fill::before { content: "\f4da"; } +.bi-person-lines-fill::before { content: "\f4db"; } +.bi-person-plus-fill::before { content: "\f4dc"; } +.bi-person-plus::before { content: "\f4dd"; } +.bi-person-square::before { content: "\f4de"; } +.bi-person-x-fill::before { content: "\f4df"; } +.bi-person-x::before { content: "\f4e0"; } +.bi-person::before { content: "\f4e1"; } +.bi-phone-fill::before { content: "\f4e2"; } +.bi-phone-landscape-fill::before { content: "\f4e3"; } +.bi-phone-landscape::before { content: "\f4e4"; } +.bi-phone-vibrate-fill::before { content: "\f4e5"; } +.bi-phone-vibrate::before { content: "\f4e6"; } +.bi-phone::before { content: "\f4e7"; } +.bi-pie-chart-fill::before { content: "\f4e8"; } +.bi-pie-chart::before { content: "\f4e9"; } +.bi-pin-angle-fill::before { content: "\f4ea"; } +.bi-pin-angle::before { content: "\f4eb"; } +.bi-pin-fill::before { content: "\f4ec"; } +.bi-pin::before { content: "\f4ed"; } +.bi-pip-fill::before { content: "\f4ee"; } +.bi-pip::before { content: "\f4ef"; } +.bi-play-btn-fill::before { content: "\f4f0"; } +.bi-play-btn::before { content: "\f4f1"; } +.bi-play-circle-fill::before { content: "\f4f2"; } +.bi-play-circle::before { content: "\f4f3"; } +.bi-play-fill::before { content: "\f4f4"; } +.bi-play::before { content: "\f4f5"; } +.bi-plug-fill::before { content: "\f4f6"; } +.bi-plug::before { content: "\f4f7"; } +.bi-plus-circle-dotted::before { content: "\f4f8"; } +.bi-plus-circle-fill::before { content: "\f4f9"; } +.bi-plus-circle::before { content: "\f4fa"; } +.bi-plus-square-dotted::before { content: "\f4fb"; } +.bi-plus-square-fill::before { content: "\f4fc"; } +.bi-plus-square::before { content: "\f4fd"; } +.bi-plus::before { content: "\f4fe"; } +.bi-power::before { content: "\f4ff"; } +.bi-printer-fill::before { content: "\f500"; } +.bi-printer::before { content: "\f501"; } +.bi-puzzle-fill::before { content: "\f502"; } +.bi-puzzle::before { content: "\f503"; } +.bi-question-circle-fill::before { content: "\f504"; } +.bi-question-circle::before { content: "\f505"; } +.bi-question-diamond-fill::before { content: "\f506"; } +.bi-question-diamond::before { content: "\f507"; } +.bi-question-octagon-fill::before { content: "\f508"; } +.bi-question-octagon::before { content: "\f509"; } +.bi-question-square-fill::before { content: "\f50a"; } +.bi-question-square::before { content: "\f50b"; } +.bi-question::before { content: "\f50c"; } +.bi-rainbow::before { content: "\f50d"; } +.bi-receipt-cutoff::before { content: "\f50e"; } +.bi-receipt::before { content: "\f50f"; } +.bi-reception-0::before { content: "\f510"; } +.bi-reception-1::before { content: "\f511"; } +.bi-reception-2::before { content: "\f512"; } +.bi-reception-3::before { content: "\f513"; } +.bi-reception-4::before { content: "\f514"; } +.bi-record-btn-fill::before { content: "\f515"; } +.bi-record-btn::before { content: "\f516"; } +.bi-record-circle-fill::before { content: "\f517"; } +.bi-record-circle::before { content: "\f518"; } +.bi-record-fill::before { content: "\f519"; } +.bi-record::before { content: "\f51a"; } +.bi-record2-fill::before { content: "\f51b"; } +.bi-record2::before { content: "\f51c"; } +.bi-reply-all-fill::before { content: "\f51d"; } +.bi-reply-all::before { content: "\f51e"; } +.bi-reply-fill::before { content: "\f51f"; } +.bi-reply::before { content: "\f520"; } +.bi-rss-fill::before { content: "\f521"; } +.bi-rss::before { content: "\f522"; } +.bi-rulers::before { content: "\f523"; } +.bi-save-fill::before { content: "\f524"; } +.bi-save::before { content: "\f525"; } +.bi-save2-fill::before { content: "\f526"; } +.bi-save2::before { content: "\f527"; } +.bi-scissors::before { content: "\f528"; } +.bi-screwdriver::before { content: "\f529"; } +.bi-search::before { content: "\f52a"; } +.bi-segmented-nav::before { content: "\f52b"; } +.bi-server::before { content: "\f52c"; } +.bi-share-fill::before { content: "\f52d"; } +.bi-share::before { content: "\f52e"; } +.bi-shield-check::before { content: "\f52f"; } +.bi-shield-exclamation::before { content: "\f530"; } +.bi-shield-fill-check::before { content: "\f531"; } +.bi-shield-fill-exclamation::before { content: "\f532"; } +.bi-shield-fill-minus::before { content: "\f533"; } +.bi-shield-fill-plus::before { content: "\f534"; } +.bi-shield-fill-x::before { content: "\f535"; } +.bi-shield-fill::before { content: "\f536"; } +.bi-shield-lock-fill::before { content: "\f537"; } +.bi-shield-lock::before { content: "\f538"; } +.bi-shield-minus::before { content: "\f539"; } +.bi-shield-plus::before { content: "\f53a"; } +.bi-shield-shaded::before { content: "\f53b"; } +.bi-shield-slash-fill::before { content: "\f53c"; } +.bi-shield-slash::before { content: "\f53d"; } +.bi-shield-x::before { content: "\f53e"; } +.bi-shield::before { content: "\f53f"; } +.bi-shift-fill::before { content: "\f540"; } +.bi-shift::before { content: "\f541"; } +.bi-shop-window::before { content: "\f542"; } +.bi-shop::before { content: "\f543"; } +.bi-shuffle::before { content: "\f544"; } +.bi-signpost-2-fill::before { content: "\f545"; } +.bi-signpost-2::before { content: "\f546"; } +.bi-signpost-fill::before { content: "\f547"; } +.bi-signpost-split-fill::before { content: "\f548"; } +.bi-signpost-split::before { content: "\f549"; } +.bi-signpost::before { content: "\f54a"; } +.bi-sim-fill::before { content: "\f54b"; } +.bi-sim::before { content: "\f54c"; } +.bi-skip-backward-btn-fill::before { content: "\f54d"; } +.bi-skip-backward-btn::before { content: "\f54e"; } +.bi-skip-backward-circle-fill::before { content: "\f54f"; } +.bi-skip-backward-circle::before { content: "\f550"; } +.bi-skip-backward-fill::before { content: "\f551"; } +.bi-skip-backward::before { content: "\f552"; } +.bi-skip-end-btn-fill::before { content: "\f553"; } +.bi-skip-end-btn::before { content: "\f554"; } +.bi-skip-end-circle-fill::before { content: "\f555"; } +.bi-skip-end-circle::before { content: "\f556"; } +.bi-skip-end-fill::before { content: "\f557"; } +.bi-skip-end::before { content: "\f558"; } +.bi-skip-forward-btn-fill::before { content: "\f559"; } +.bi-skip-forward-btn::before { content: "\f55a"; } +.bi-skip-forward-circle-fill::before { content: "\f55b"; } +.bi-skip-forward-circle::before { content: "\f55c"; } +.bi-skip-forward-fill::before { content: "\f55d"; } +.bi-skip-forward::before { content: "\f55e"; } +.bi-skip-start-btn-fill::before { content: "\f55f"; } +.bi-skip-start-btn::before { content: "\f560"; } +.bi-skip-start-circle-fill::before { content: "\f561"; } +.bi-skip-start-circle::before { content: "\f562"; } +.bi-skip-start-fill::before { content: "\f563"; } +.bi-skip-start::before { content: "\f564"; } +.bi-slack::before { content: "\f565"; } +.bi-slash-circle-fill::before { content: "\f566"; } +.bi-slash-circle::before { content: "\f567"; } +.bi-slash-square-fill::before { content: "\f568"; } +.bi-slash-square::before { content: "\f569"; } +.bi-slash::before { content: "\f56a"; } +.bi-sliders::before { content: "\f56b"; } +.bi-smartwatch::before { content: "\f56c"; } +.bi-snow::before { content: "\f56d"; } +.bi-snow2::before { content: "\f56e"; } +.bi-snow3::before { content: "\f56f"; } +.bi-sort-alpha-down-alt::before { content: "\f570"; } +.bi-sort-alpha-down::before { content: "\f571"; } +.bi-sort-alpha-up-alt::before { content: "\f572"; } +.bi-sort-alpha-up::before { content: "\f573"; } +.bi-sort-down-alt::before { content: "\f574"; } +.bi-sort-down::before { content: "\f575"; } +.bi-sort-numeric-down-alt::before { content: "\f576"; } +.bi-sort-numeric-down::before { content: "\f577"; } +.bi-sort-numeric-up-alt::before { content: "\f578"; } +.bi-sort-numeric-up::before { content: "\f579"; } +.bi-sort-up-alt::before { content: "\f57a"; } +.bi-sort-up::before { content: "\f57b"; } +.bi-soundwave::before { content: "\f57c"; } +.bi-speaker-fill::before { content: "\f57d"; } +.bi-speaker::before { content: "\f57e"; } +.bi-speedometer::before { content: "\f57f"; } +.bi-speedometer2::before { content: "\f580"; } +.bi-spellcheck::before { content: "\f581"; } +.bi-square-fill::before { content: "\f582"; } +.bi-square-half::before { content: "\f583"; } +.bi-square::before { content: "\f584"; } +.bi-stack::before { content: "\f585"; } +.bi-star-fill::before { content: "\f586"; } +.bi-star-half::before { content: "\f587"; } +.bi-star::before { content: "\f588"; } +.bi-stars::before { content: "\f589"; } +.bi-stickies-fill::before { content: "\f58a"; } +.bi-stickies::before { content: "\f58b"; } +.bi-sticky-fill::before { content: "\f58c"; } +.bi-sticky::before { content: "\f58d"; } +.bi-stop-btn-fill::before { content: "\f58e"; } +.bi-stop-btn::before { content: "\f58f"; } +.bi-stop-circle-fill::before { content: "\f590"; } +.bi-stop-circle::before { content: "\f591"; } +.bi-stop-fill::before { content: "\f592"; } +.bi-stop::before { content: "\f593"; } +.bi-stoplights-fill::before { content: "\f594"; } +.bi-stoplights::before { content: "\f595"; } +.bi-stopwatch-fill::before { content: "\f596"; } +.bi-stopwatch::before { content: "\f597"; } +.bi-subtract::before { content: "\f598"; } +.bi-suit-club-fill::before { content: "\f599"; } +.bi-suit-club::before { content: "\f59a"; } +.bi-suit-diamond-fill::before { content: "\f59b"; } +.bi-suit-diamond::before { content: "\f59c"; } +.bi-suit-heart-fill::before { content: "\f59d"; } +.bi-suit-heart::before { content: "\f59e"; } +.bi-suit-spade-fill::before { content: "\f59f"; } +.bi-suit-spade::before { content: "\f5a0"; } +.bi-sun-fill::before { content: "\f5a1"; } +.bi-sun::before { content: "\f5a2"; } +.bi-sunglasses::before { content: "\f5a3"; } +.bi-sunrise-fill::before { content: "\f5a4"; } +.bi-sunrise::before { content: "\f5a5"; } +.bi-sunset-fill::before { content: "\f5a6"; } +.bi-sunset::before { content: "\f5a7"; } +.bi-symmetry-horizontal::before { content: "\f5a8"; } +.bi-symmetry-vertical::before { content: "\f5a9"; } +.bi-table::before { content: "\f5aa"; } +.bi-tablet-fill::before { content: "\f5ab"; } +.bi-tablet-landscape-fill::before { content: "\f5ac"; } +.bi-tablet-landscape::before { content: "\f5ad"; } +.bi-tablet::before { content: "\f5ae"; } +.bi-tag-fill::before { content: "\f5af"; } +.bi-tag::before { content: "\f5b0"; } +.bi-tags-fill::before { content: "\f5b1"; } +.bi-tags::before { content: "\f5b2"; } +.bi-telegram::before { content: "\f5b3"; } +.bi-telephone-fill::before { content: "\f5b4"; } +.bi-telephone-forward-fill::before { content: "\f5b5"; } +.bi-telephone-forward::before { content: "\f5b6"; } +.bi-telephone-inbound-fill::before { content: "\f5b7"; } +.bi-telephone-inbound::before { content: "\f5b8"; } +.bi-telephone-minus-fill::before { content: "\f5b9"; } +.bi-telephone-minus::before { content: "\f5ba"; } +.bi-telephone-outbound-fill::before { content: "\f5bb"; } +.bi-telephone-outbound::before { content: "\f5bc"; } +.bi-telephone-plus-fill::before { content: "\f5bd"; } +.bi-telephone-plus::before { content: "\f5be"; } +.bi-telephone-x-fill::before { content: "\f5bf"; } +.bi-telephone-x::before { content: "\f5c0"; } +.bi-telephone::before { content: "\f5c1"; } +.bi-terminal-fill::before { content: "\f5c2"; } +.bi-terminal::before { content: "\f5c3"; } +.bi-text-center::before { content: "\f5c4"; } +.bi-text-indent-left::before { content: "\f5c5"; } +.bi-text-indent-right::before { content: "\f5c6"; } +.bi-text-left::before { content: "\f5c7"; } +.bi-text-paragraph::before { content: "\f5c8"; } +.bi-text-right::before { content: "\f5c9"; } +.bi-textarea-resize::before { content: "\f5ca"; } +.bi-textarea-t::before { content: "\f5cb"; } +.bi-textarea::before { content: "\f5cc"; } +.bi-thermometer-half::before { content: "\f5cd"; } +.bi-thermometer-high::before { content: "\f5ce"; } +.bi-thermometer-low::before { content: "\f5cf"; } +.bi-thermometer-snow::before { content: "\f5d0"; } +.bi-thermometer-sun::before { content: "\f5d1"; } +.bi-thermometer::before { content: "\f5d2"; } +.bi-three-dots-vertical::before { content: "\f5d3"; } +.bi-three-dots::before { content: "\f5d4"; } +.bi-toggle-off::before { content: "\f5d5"; } +.bi-toggle-on::before { content: "\f5d6"; } +.bi-toggle2-off::before { content: "\f5d7"; } +.bi-toggle2-on::before { content: "\f5d8"; } +.bi-toggles::before { content: "\f5d9"; } +.bi-toggles2::before { content: "\f5da"; } +.bi-tools::before { content: "\f5db"; } +.bi-tornado::before { content: "\f5dc"; } +.bi-trash-fill::before { content: "\f5dd"; } +.bi-trash::before { content: "\f5de"; } +.bi-trash2-fill::before { content: "\f5df"; } +.bi-trash2::before { content: "\f5e0"; } +.bi-tree-fill::before { content: "\f5e1"; } +.bi-tree::before { content: "\f5e2"; } +.bi-triangle-fill::before { content: "\f5e3"; } +.bi-triangle-half::before { content: "\f5e4"; } +.bi-triangle::before { content: "\f5e5"; } +.bi-trophy-fill::before { content: "\f5e6"; } +.bi-trophy::before { content: "\f5e7"; } +.bi-tropical-storm::before { content: "\f5e8"; } +.bi-truck-flatbed::before { content: "\f5e9"; } +.bi-truck::before { content: "\f5ea"; } +.bi-tsunami::before { content: "\f5eb"; } +.bi-tv-fill::before { content: "\f5ec"; } +.bi-tv::before { content: "\f5ed"; } +.bi-twitch::before { content: "\f5ee"; } +.bi-twitter::before { content: "\f5ef"; } +.bi-type-bold::before { content: "\f5f0"; } +.bi-type-h1::before { content: "\f5f1"; } +.bi-type-h2::before { content: "\f5f2"; } +.bi-type-h3::before { content: "\f5f3"; } +.bi-type-italic::before { content: "\f5f4"; } +.bi-type-strikethrough::before { content: "\f5f5"; } +.bi-type-underline::before { content: "\f5f6"; } +.bi-type::before { content: "\f5f7"; } +.bi-ui-checks-grid::before { content: "\f5f8"; } +.bi-ui-checks::before { content: "\f5f9"; } +.bi-ui-radios-grid::before { content: "\f5fa"; } +.bi-ui-radios::before { content: "\f5fb"; } +.bi-umbrella-fill::before { content: "\f5fc"; } +.bi-umbrella::before { content: "\f5fd"; } +.bi-union::before { content: "\f5fe"; } +.bi-unlock-fill::before { content: "\f5ff"; } +.bi-unlock::before { content: "\f600"; } +.bi-upc-scan::before { content: "\f601"; } +.bi-upc::before { content: "\f602"; } +.bi-upload::before { content: "\f603"; } +.bi-vector-pen::before { content: "\f604"; } +.bi-view-list::before { content: "\f605"; } +.bi-view-stacked::before { content: "\f606"; } +.bi-vinyl-fill::before { content: "\f607"; } +.bi-vinyl::before { content: "\f608"; } +.bi-voicemail::before { content: "\f609"; } +.bi-volume-down-fill::before { content: "\f60a"; } +.bi-volume-down::before { content: "\f60b"; } +.bi-volume-mute-fill::before { content: "\f60c"; } +.bi-volume-mute::before { content: "\f60d"; } +.bi-volume-off-fill::before { content: "\f60e"; } +.bi-volume-off::before { content: "\f60f"; } +.bi-volume-up-fill::before { content: "\f610"; } +.bi-volume-up::before { content: "\f611"; } +.bi-vr::before { content: "\f612"; } +.bi-wallet-fill::before { content: "\f613"; } +.bi-wallet::before { content: "\f614"; } +.bi-wallet2::before { content: "\f615"; } +.bi-watch::before { content: "\f616"; } +.bi-water::before { content: "\f617"; } +.bi-whatsapp::before { content: "\f618"; } +.bi-wifi-1::before { content: "\f619"; } +.bi-wifi-2::before { content: "\f61a"; } +.bi-wifi-off::before { content: "\f61b"; } +.bi-wifi::before { content: "\f61c"; } +.bi-wind::before { content: "\f61d"; } +.bi-window-dock::before { content: "\f61e"; } +.bi-window-sidebar::before { content: "\f61f"; } +.bi-window::before { content: "\f620"; } +.bi-wrench::before { content: "\f621"; } +.bi-x-circle-fill::before { content: "\f622"; } +.bi-x-circle::before { content: "\f623"; } +.bi-x-diamond-fill::before { content: "\f624"; } +.bi-x-diamond::before { content: "\f625"; } +.bi-x-octagon-fill::before { content: "\f626"; } +.bi-x-octagon::before { content: "\f627"; } +.bi-x-square-fill::before { content: "\f628"; } +.bi-x-square::before { content: "\f629"; } +.bi-x::before { content: "\f62a"; } +.bi-youtube::before { content: "\f62b"; } +.bi-zoom-in::before { content: "\f62c"; } +.bi-zoom-out::before { content: "\f62d"; } +.bi-bank::before { content: "\f62e"; } +.bi-bank2::before { content: "\f62f"; } +.bi-bell-slash-fill::before { content: "\f630"; } +.bi-bell-slash::before { content: "\f631"; } +.bi-cash-coin::before { content: "\f632"; } +.bi-check-lg::before { content: "\f633"; } +.bi-coin::before { content: "\f634"; } +.bi-currency-bitcoin::before { content: "\f635"; } +.bi-currency-dollar::before { content: "\f636"; } +.bi-currency-euro::before { content: "\f637"; } +.bi-currency-exchange::before { content: "\f638"; } +.bi-currency-pound::before { content: "\f639"; } +.bi-currency-yen::before { content: "\f63a"; } +.bi-dash-lg::before { content: "\f63b"; } +.bi-exclamation-lg::before { content: "\f63c"; } +.bi-file-earmark-pdf-fill::before { content: "\f63d"; } +.bi-file-earmark-pdf::before { content: "\f63e"; } +.bi-file-pdf-fill::before { content: "\f63f"; } +.bi-file-pdf::before { content: "\f640"; } +.bi-gender-ambiguous::before { content: "\f641"; } +.bi-gender-female::before { content: "\f642"; } +.bi-gender-male::before { content: "\f643"; } +.bi-gender-trans::before { content: "\f644"; } +.bi-headset-vr::before { content: "\f645"; } +.bi-info-lg::before { content: "\f646"; } +.bi-mastodon::before { content: "\f647"; } +.bi-messenger::before { content: "\f648"; } +.bi-piggy-bank-fill::before { content: "\f649"; } +.bi-piggy-bank::before { content: "\f64a"; } +.bi-pin-map-fill::before { content: "\f64b"; } +.bi-pin-map::before { content: "\f64c"; } +.bi-plus-lg::before { content: "\f64d"; } +.bi-question-lg::before { content: "\f64e"; } +.bi-recycle::before { content: "\f64f"; } +.bi-reddit::before { content: "\f650"; } +.bi-safe-fill::before { content: "\f651"; } +.bi-safe2-fill::before { content: "\f652"; } +.bi-safe2::before { content: "\f653"; } +.bi-sd-card-fill::before { content: "\f654"; } +.bi-sd-card::before { content: "\f655"; } +.bi-skype::before { content: "\f656"; } +.bi-slash-lg::before { content: "\f657"; } +.bi-translate::before { content: "\f658"; } +.bi-x-lg::before { content: "\f659"; } +.bi-safe::before { content: "\f65a"; } +.bi-apple::before { content: "\f65b"; } +.bi-microsoft::before { content: "\f65d"; } +.bi-windows::before { content: "\f65e"; } +.bi-behance::before { content: "\f65c"; } +.bi-dribbble::before { content: "\f65f"; } +.bi-line::before { content: "\f660"; } +.bi-medium::before { content: "\f661"; } +.bi-paypal::before { content: "\f662"; } +.bi-pinterest::before { content: "\f663"; } +.bi-signal::before { content: "\f664"; } +.bi-snapchat::before { content: "\f665"; } +.bi-spotify::before { content: "\f666"; } +.bi-stack-overflow::before { content: "\f667"; } +.bi-strava::before { content: "\f668"; } +.bi-wordpress::before { content: "\f669"; } +.bi-vimeo::before { content: "\f66a"; } +.bi-activity::before { content: "\f66b"; } +.bi-easel2-fill::before { content: "\f66c"; } +.bi-easel2::before { content: "\f66d"; } +.bi-easel3-fill::before { content: "\f66e"; } +.bi-easel3::before { content: "\f66f"; } +.bi-fan::before { content: "\f670"; } +.bi-fingerprint::before { content: "\f671"; } +.bi-graph-down-arrow::before { content: "\f672"; } +.bi-graph-up-arrow::before { content: "\f673"; } +.bi-hypnotize::before { content: "\f674"; } +.bi-magic::before { content: "\f675"; } +.bi-person-rolodex::before { content: "\f676"; } +.bi-person-video::before { content: "\f677"; } +.bi-person-video2::before { content: "\f678"; } +.bi-person-video3::before { content: "\f679"; } +.bi-person-workspace::before { content: "\f67a"; } +.bi-radioactive::before { content: "\f67b"; } +.bi-webcam-fill::before { content: "\f67c"; } +.bi-webcam::before { content: "\f67d"; } +.bi-yin-yang::before { content: "\f67e"; } +.bi-bandaid-fill::before { content: "\f680"; } +.bi-bandaid::before { content: "\f681"; } +.bi-bluetooth::before { content: "\f682"; } +.bi-body-text::before { content: "\f683"; } +.bi-boombox::before { content: "\f684"; } +.bi-boxes::before { content: "\f685"; } +.bi-dpad-fill::before { content: "\f686"; } +.bi-dpad::before { content: "\f687"; } +.bi-ear-fill::before { content: "\f688"; } +.bi-ear::before { content: "\f689"; } +.bi-envelope-check-fill::before { content: "\f68b"; } +.bi-envelope-check::before { content: "\f68c"; } +.bi-envelope-dash-fill::before { content: "\f68e"; } +.bi-envelope-dash::before { content: "\f68f"; } +.bi-envelope-exclamation-fill::before { content: "\f691"; } +.bi-envelope-exclamation::before { content: "\f692"; } +.bi-envelope-plus-fill::before { content: "\f693"; } +.bi-envelope-plus::before { content: "\f694"; } +.bi-envelope-slash-fill::before { content: "\f696"; } +.bi-envelope-slash::before { content: "\f697"; } +.bi-envelope-x-fill::before { content: "\f699"; } +.bi-envelope-x::before { content: "\f69a"; } +.bi-explicit-fill::before { content: "\f69b"; } +.bi-explicit::before { content: "\f69c"; } +.bi-git::before { content: "\f69d"; } +.bi-infinity::before { content: "\f69e"; } +.bi-list-columns-reverse::before { content: "\f69f"; } +.bi-list-columns::before { content: "\f6a0"; } +.bi-meta::before { content: "\f6a1"; } +.bi-nintendo-switch::before { content: "\f6a4"; } +.bi-pc-display-horizontal::before { content: "\f6a5"; } +.bi-pc-display::before { content: "\f6a6"; } +.bi-pc-horizontal::before { content: "\f6a7"; } +.bi-pc::before { content: "\f6a8"; } +.bi-playstation::before { content: "\f6a9"; } +.bi-plus-slash-minus::before { content: "\f6aa"; } +.bi-projector-fill::before { content: "\f6ab"; } +.bi-projector::before { content: "\f6ac"; } +.bi-qr-code-scan::before { content: "\f6ad"; } +.bi-qr-code::before { content: "\f6ae"; } +.bi-quora::before { content: "\f6af"; } +.bi-quote::before { content: "\f6b0"; } +.bi-robot::before { content: "\f6b1"; } +.bi-send-check-fill::before { content: "\f6b2"; } +.bi-send-check::before { content: "\f6b3"; } +.bi-send-dash-fill::before { content: "\f6b4"; } +.bi-send-dash::before { content: "\f6b5"; } +.bi-send-exclamation-fill::before { content: "\f6b7"; } +.bi-send-exclamation::before { content: "\f6b8"; } +.bi-send-fill::before { content: "\f6b9"; } +.bi-send-plus-fill::before { content: "\f6ba"; } +.bi-send-plus::before { content: "\f6bb"; } +.bi-send-slash-fill::before { content: "\f6bc"; } +.bi-send-slash::before { content: "\f6bd"; } +.bi-send-x-fill::before { content: "\f6be"; } +.bi-send-x::before { content: "\f6bf"; } +.bi-send::before { content: "\f6c0"; } +.bi-steam::before { content: "\f6c1"; } +.bi-terminal-dash::before { content: "\f6c3"; } +.bi-terminal-plus::before { content: "\f6c4"; } +.bi-terminal-split::before { content: "\f6c5"; } +.bi-ticket-detailed-fill::before { content: "\f6c6"; } +.bi-ticket-detailed::before { content: "\f6c7"; } +.bi-ticket-fill::before { content: "\f6c8"; } +.bi-ticket-perforated-fill::before { content: "\f6c9"; } +.bi-ticket-perforated::before { content: "\f6ca"; } +.bi-ticket::before { content: "\f6cb"; } +.bi-tiktok::before { content: "\f6cc"; } +.bi-window-dash::before { content: "\f6cd"; } +.bi-window-desktop::before { content: "\f6ce"; } +.bi-window-fullscreen::before { content: "\f6cf"; } +.bi-window-plus::before { content: "\f6d0"; } +.bi-window-split::before { content: "\f6d1"; } +.bi-window-stack::before { content: "\f6d2"; } +.bi-window-x::before { content: "\f6d3"; } +.bi-xbox::before { content: "\f6d4"; } +.bi-ethernet::before { content: "\f6d5"; } +.bi-hdmi-fill::before { content: "\f6d6"; } +.bi-hdmi::before { content: "\f6d7"; } +.bi-usb-c-fill::before { content: "\f6d8"; } +.bi-usb-c::before { content: "\f6d9"; } +.bi-usb-fill::before { content: "\f6da"; } +.bi-usb-plug-fill::before { content: "\f6db"; } +.bi-usb-plug::before { content: "\f6dc"; } +.bi-usb-symbol::before { content: "\f6dd"; } +.bi-usb::before { content: "\f6de"; } +.bi-boombox-fill::before { content: "\f6df"; } +.bi-displayport::before { content: "\f6e1"; } +.bi-gpu-card::before { content: "\f6e2"; } +.bi-memory::before { content: "\f6e3"; } +.bi-modem-fill::before { content: "\f6e4"; } +.bi-modem::before { content: "\f6e5"; } +.bi-motherboard-fill::before { content: "\f6e6"; } +.bi-motherboard::before { content: "\f6e7"; } +.bi-optical-audio-fill::before { content: "\f6e8"; } +.bi-optical-audio::before { content: "\f6e9"; } +.bi-pci-card::before { content: "\f6ea"; } +.bi-router-fill::before { content: "\f6eb"; } +.bi-router::before { content: "\f6ec"; } +.bi-thunderbolt-fill::before { content: "\f6ef"; } +.bi-thunderbolt::before { content: "\f6f0"; } +.bi-usb-drive-fill::before { content: "\f6f1"; } +.bi-usb-drive::before { content: "\f6f2"; } +.bi-usb-micro-fill::before { content: "\f6f3"; } +.bi-usb-micro::before { content: "\f6f4"; } +.bi-usb-mini-fill::before { content: "\f6f5"; } +.bi-usb-mini::before { content: "\f6f6"; } +.bi-cloud-haze2::before { content: "\f6f7"; } +.bi-device-hdd-fill::before { content: "\f6f8"; } +.bi-device-hdd::before { content: "\f6f9"; } +.bi-device-ssd-fill::before { content: "\f6fa"; } +.bi-device-ssd::before { content: "\f6fb"; } +.bi-displayport-fill::before { content: "\f6fc"; } +.bi-mortarboard-fill::before { content: "\f6fd"; } +.bi-mortarboard::before { content: "\f6fe"; } +.bi-terminal-x::before { content: "\f6ff"; } +.bi-arrow-through-heart-fill::before { content: "\f700"; } +.bi-arrow-through-heart::before { content: "\f701"; } +.bi-badge-sd-fill::before { content: "\f702"; } +.bi-badge-sd::before { content: "\f703"; } +.bi-bag-heart-fill::before { content: "\f704"; } +.bi-bag-heart::before { content: "\f705"; } +.bi-balloon-fill::before { content: "\f706"; } +.bi-balloon-heart-fill::before { content: "\f707"; } +.bi-balloon-heart::before { content: "\f708"; } +.bi-balloon::before { content: "\f709"; } +.bi-box2-fill::before { content: "\f70a"; } +.bi-box2-heart-fill::before { content: "\f70b"; } +.bi-box2-heart::before { content: "\f70c"; } +.bi-box2::before { content: "\f70d"; } +.bi-braces-asterisk::before { content: "\f70e"; } +.bi-calendar-heart-fill::before { content: "\f70f"; } +.bi-calendar-heart::before { content: "\f710"; } +.bi-calendar2-heart-fill::before { content: "\f711"; } +.bi-calendar2-heart::before { content: "\f712"; } +.bi-chat-heart-fill::before { content: "\f713"; } +.bi-chat-heart::before { content: "\f714"; } +.bi-chat-left-heart-fill::before { content: "\f715"; } +.bi-chat-left-heart::before { content: "\f716"; } +.bi-chat-right-heart-fill::before { content: "\f717"; } +.bi-chat-right-heart::before { content: "\f718"; } +.bi-chat-square-heart-fill::before { content: "\f719"; } +.bi-chat-square-heart::before { content: "\f71a"; } +.bi-clipboard-check-fill::before { content: "\f71b"; } +.bi-clipboard-data-fill::before { content: "\f71c"; } +.bi-clipboard-fill::before { content: "\f71d"; } +.bi-clipboard-heart-fill::before { content: "\f71e"; } +.bi-clipboard-heart::before { content: "\f71f"; } +.bi-clipboard-minus-fill::before { content: "\f720"; } +.bi-clipboard-plus-fill::before { content: "\f721"; } +.bi-clipboard-pulse::before { content: "\f722"; } +.bi-clipboard-x-fill::before { content: "\f723"; } +.bi-clipboard2-check-fill::before { content: "\f724"; } +.bi-clipboard2-check::before { content: "\f725"; } +.bi-clipboard2-data-fill::before { content: "\f726"; } +.bi-clipboard2-data::before { content: "\f727"; } +.bi-clipboard2-fill::before { content: "\f728"; } +.bi-clipboard2-heart-fill::before { content: "\f729"; } +.bi-clipboard2-heart::before { content: "\f72a"; } +.bi-clipboard2-minus-fill::before { content: "\f72b"; } +.bi-clipboard2-minus::before { content: "\f72c"; } +.bi-clipboard2-plus-fill::before { content: "\f72d"; } +.bi-clipboard2-plus::before { content: "\f72e"; } +.bi-clipboard2-pulse-fill::before { content: "\f72f"; } +.bi-clipboard2-pulse::before { content: "\f730"; } +.bi-clipboard2-x-fill::before { content: "\f731"; } +.bi-clipboard2-x::before { content: "\f732"; } +.bi-clipboard2::before { content: "\f733"; } +.bi-emoji-kiss-fill::before { content: "\f734"; } +.bi-emoji-kiss::before { content: "\f735"; } +.bi-envelope-heart-fill::before { content: "\f736"; } +.bi-envelope-heart::before { content: "\f737"; } +.bi-envelope-open-heart-fill::before { content: "\f738"; } +.bi-envelope-open-heart::before { content: "\f739"; } +.bi-envelope-paper-fill::before { content: "\f73a"; } +.bi-envelope-paper-heart-fill::before { content: "\f73b"; } +.bi-envelope-paper-heart::before { content: "\f73c"; } +.bi-envelope-paper::before { content: "\f73d"; } +.bi-filetype-aac::before { content: "\f73e"; } +.bi-filetype-ai::before { content: "\f73f"; } +.bi-filetype-bmp::before { content: "\f740"; } +.bi-filetype-cs::before { content: "\f741"; } +.bi-filetype-css::before { content: "\f742"; } +.bi-filetype-csv::before { content: "\f743"; } +.bi-filetype-doc::before { content: "\f744"; } +.bi-filetype-docx::before { content: "\f745"; } +.bi-filetype-exe::before { content: "\f746"; } +.bi-filetype-gif::before { content: "\f747"; } +.bi-filetype-heic::before { content: "\f748"; } +.bi-filetype-html::before { content: "\f749"; } +.bi-filetype-java::before { content: "\f74a"; } +.bi-filetype-jpg::before { content: "\f74b"; } +.bi-filetype-js::before { content: "\f74c"; } +.bi-filetype-jsx::before { content: "\f74d"; } +.bi-filetype-key::before { content: "\f74e"; } +.bi-filetype-m4p::before { content: "\f74f"; } +.bi-filetype-md::before { content: "\f750"; } +.bi-filetype-mdx::before { content: "\f751"; } +.bi-filetype-mov::before { content: "\f752"; } +.bi-filetype-mp3::before { content: "\f753"; } +.bi-filetype-mp4::before { content: "\f754"; } +.bi-filetype-otf::before { content: "\f755"; } +.bi-filetype-pdf::before { content: "\f756"; } +.bi-filetype-php::before { content: "\f757"; } +.bi-filetype-png::before { content: "\f758"; } +.bi-filetype-ppt::before { content: "\f75a"; } +.bi-filetype-psd::before { content: "\f75b"; } +.bi-filetype-py::before { content: "\f75c"; } +.bi-filetype-raw::before { content: "\f75d"; } +.bi-filetype-rb::before { content: "\f75e"; } +.bi-filetype-sass::before { content: "\f75f"; } +.bi-filetype-scss::before { content: "\f760"; } +.bi-filetype-sh::before { content: "\f761"; } +.bi-filetype-svg::before { content: "\f762"; } +.bi-filetype-tiff::before { content: "\f763"; } +.bi-filetype-tsx::before { content: "\f764"; } +.bi-filetype-ttf::before { content: "\f765"; } +.bi-filetype-txt::before { content: "\f766"; } +.bi-filetype-wav::before { content: "\f767"; } +.bi-filetype-woff::before { content: "\f768"; } +.bi-filetype-xls::before { content: "\f76a"; } +.bi-filetype-xml::before { content: "\f76b"; } +.bi-filetype-yml::before { content: "\f76c"; } +.bi-heart-arrow::before { content: "\f76d"; } +.bi-heart-pulse-fill::before { content: "\f76e"; } +.bi-heart-pulse::before { content: "\f76f"; } +.bi-heartbreak-fill::before { content: "\f770"; } +.bi-heartbreak::before { content: "\f771"; } +.bi-hearts::before { content: "\f772"; } +.bi-hospital-fill::before { content: "\f773"; } +.bi-hospital::before { content: "\f774"; } +.bi-house-heart-fill::before { content: "\f775"; } +.bi-house-heart::before { content: "\f776"; } +.bi-incognito::before { content: "\f777"; } +.bi-magnet-fill::before { content: "\f778"; } +.bi-magnet::before { content: "\f779"; } +.bi-person-heart::before { content: "\f77a"; } +.bi-person-hearts::before { content: "\f77b"; } +.bi-phone-flip::before { content: "\f77c"; } +.bi-plugin::before { content: "\f77d"; } +.bi-postage-fill::before { content: "\f77e"; } +.bi-postage-heart-fill::before { content: "\f77f"; } +.bi-postage-heart::before { content: "\f780"; } +.bi-postage::before { content: "\f781"; } +.bi-postcard-fill::before { content: "\f782"; } +.bi-postcard-heart-fill::before { content: "\f783"; } +.bi-postcard-heart::before { content: "\f784"; } +.bi-postcard::before { content: "\f785"; } +.bi-search-heart-fill::before { content: "\f786"; } +.bi-search-heart::before { content: "\f787"; } +.bi-sliders2-vertical::before { content: "\f788"; } +.bi-sliders2::before { content: "\f789"; } +.bi-trash3-fill::before { content: "\f78a"; } +.bi-trash3::before { content: "\f78b"; } +.bi-valentine::before { content: "\f78c"; } +.bi-valentine2::before { content: "\f78d"; } +.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; } +.bi-wrench-adjustable-circle::before { content: "\f78f"; } +.bi-wrench-adjustable::before { content: "\f790"; } +.bi-filetype-json::before { content: "\f791"; } +.bi-filetype-pptx::before { content: "\f792"; } +.bi-filetype-xlsx::before { content: "\f793"; } +.bi-1-circle-fill::before { content: "\f796"; } +.bi-1-circle::before { content: "\f797"; } +.bi-1-square-fill::before { content: "\f798"; } +.bi-1-square::before { content: "\f799"; } +.bi-2-circle-fill::before { content: "\f79c"; } +.bi-2-circle::before { content: "\f79d"; } +.bi-2-square-fill::before { content: "\f79e"; } +.bi-2-square::before { content: "\f79f"; } +.bi-3-circle-fill::before { content: "\f7a2"; } +.bi-3-circle::before { content: "\f7a3"; } +.bi-3-square-fill::before { content: "\f7a4"; } +.bi-3-square::before { content: "\f7a5"; } +.bi-4-circle-fill::before { content: "\f7a8"; } +.bi-4-circle::before { content: "\f7a9"; } +.bi-4-square-fill::before { content: "\f7aa"; } +.bi-4-square::before { content: "\f7ab"; } +.bi-5-circle-fill::before { content: "\f7ae"; } +.bi-5-circle::before { content: "\f7af"; } +.bi-5-square-fill::before { content: "\f7b0"; } +.bi-5-square::before { content: "\f7b1"; } +.bi-6-circle-fill::before { content: "\f7b4"; } +.bi-6-circle::before { content: "\f7b5"; } +.bi-6-square-fill::before { content: "\f7b6"; } +.bi-6-square::before { content: "\f7b7"; } +.bi-7-circle-fill::before { content: "\f7ba"; } +.bi-7-circle::before { content: "\f7bb"; } +.bi-7-square-fill::before { content: "\f7bc"; } +.bi-7-square::before { content: "\f7bd"; } +.bi-8-circle-fill::before { content: "\f7c0"; } +.bi-8-circle::before { content: "\f7c1"; } +.bi-8-square-fill::before { content: "\f7c2"; } +.bi-8-square::before { content: "\f7c3"; } +.bi-9-circle-fill::before { content: "\f7c6"; } +.bi-9-circle::before { content: "\f7c7"; } +.bi-9-square-fill::before { content: "\f7c8"; } +.bi-9-square::before { content: "\f7c9"; } +.bi-airplane-engines-fill::before { content: "\f7ca"; } +.bi-airplane-engines::before { content: "\f7cb"; } +.bi-airplane-fill::before { content: "\f7cc"; } +.bi-airplane::before { content: "\f7cd"; } +.bi-alexa::before { content: "\f7ce"; } +.bi-alipay::before { content: "\f7cf"; } +.bi-android::before { content: "\f7d0"; } +.bi-android2::before { content: "\f7d1"; } +.bi-box-fill::before { content: "\f7d2"; } +.bi-box-seam-fill::before { content: "\f7d3"; } +.bi-browser-chrome::before { content: "\f7d4"; } +.bi-browser-edge::before { content: "\f7d5"; } +.bi-browser-firefox::before { content: "\f7d6"; } +.bi-browser-safari::before { content: "\f7d7"; } +.bi-c-circle-fill::before { content: "\f7da"; } +.bi-c-circle::before { content: "\f7db"; } +.bi-c-square-fill::before { content: "\f7dc"; } +.bi-c-square::before { content: "\f7dd"; } +.bi-capsule-pill::before { content: "\f7de"; } +.bi-capsule::before { content: "\f7df"; } +.bi-car-front-fill::before { content: "\f7e0"; } +.bi-car-front::before { content: "\f7e1"; } +.bi-cassette-fill::before { content: "\f7e2"; } +.bi-cassette::before { content: "\f7e3"; } +.bi-cc-circle-fill::before { content: "\f7e6"; } +.bi-cc-circle::before { content: "\f7e7"; } +.bi-cc-square-fill::before { content: "\f7e8"; } +.bi-cc-square::before { content: "\f7e9"; } +.bi-cup-hot-fill::before { content: "\f7ea"; } +.bi-cup-hot::before { content: "\f7eb"; } +.bi-currency-rupee::before { content: "\f7ec"; } +.bi-dropbox::before { content: "\f7ed"; } +.bi-escape::before { content: "\f7ee"; } +.bi-fast-forward-btn-fill::before { content: "\f7ef"; } +.bi-fast-forward-btn::before { content: "\f7f0"; } +.bi-fast-forward-circle-fill::before { content: "\f7f1"; } +.bi-fast-forward-circle::before { content: "\f7f2"; } +.bi-fast-forward-fill::before { content: "\f7f3"; } +.bi-fast-forward::before { content: "\f7f4"; } +.bi-filetype-sql::before { content: "\f7f5"; } +.bi-fire::before { content: "\f7f6"; } +.bi-google-play::before { content: "\f7f7"; } +.bi-h-circle-fill::before { content: "\f7fa"; } +.bi-h-circle::before { content: "\f7fb"; } +.bi-h-square-fill::before { content: "\f7fc"; } +.bi-h-square::before { content: "\f7fd"; } +.bi-indent::before { content: "\f7fe"; } +.bi-lungs-fill::before { content: "\f7ff"; } +.bi-lungs::before { content: "\f800"; } +.bi-microsoft-teams::before { content: "\f801"; } +.bi-p-circle-fill::before { content: "\f804"; } +.bi-p-circle::before { content: "\f805"; } +.bi-p-square-fill::before { content: "\f806"; } +.bi-p-square::before { content: "\f807"; } +.bi-pass-fill::before { content: "\f808"; } +.bi-pass::before { content: "\f809"; } +.bi-prescription::before { content: "\f80a"; } +.bi-prescription2::before { content: "\f80b"; } +.bi-r-circle-fill::before { content: "\f80e"; } +.bi-r-circle::before { content: "\f80f"; } +.bi-r-square-fill::before { content: "\f810"; } +.bi-r-square::before { content: "\f811"; } +.bi-repeat-1::before { content: "\f812"; } +.bi-repeat::before { content: "\f813"; } +.bi-rewind-btn-fill::before { content: "\f814"; } +.bi-rewind-btn::before { content: "\f815"; } +.bi-rewind-circle-fill::before { content: "\f816"; } +.bi-rewind-circle::before { content: "\f817"; } +.bi-rewind-fill::before { content: "\f818"; } +.bi-rewind::before { content: "\f819"; } +.bi-train-freight-front-fill::before { content: "\f81a"; } +.bi-train-freight-front::before { content: "\f81b"; } +.bi-train-front-fill::before { content: "\f81c"; } +.bi-train-front::before { content: "\f81d"; } +.bi-train-lightrail-front-fill::before { content: "\f81e"; } +.bi-train-lightrail-front::before { content: "\f81f"; } +.bi-truck-front-fill::before { content: "\f820"; } +.bi-truck-front::before { content: "\f821"; } +.bi-ubuntu::before { content: "\f822"; } +.bi-unindent::before { content: "\f823"; } +.bi-unity::before { content: "\f824"; } +.bi-universal-access-circle::before { content: "\f825"; } +.bi-universal-access::before { content: "\f826"; } +.bi-virus::before { content: "\f827"; } +.bi-virus2::before { content: "\f828"; } +.bi-wechat::before { content: "\f829"; } +.bi-yelp::before { content: "\f82a"; } +.bi-sign-stop-fill::before { content: "\f82b"; } +.bi-sign-stop-lights-fill::before { content: "\f82c"; } +.bi-sign-stop-lights::before { content: "\f82d"; } +.bi-sign-stop::before { content: "\f82e"; } +.bi-sign-turn-left-fill::before { content: "\f82f"; } +.bi-sign-turn-left::before { content: "\f830"; } +.bi-sign-turn-right-fill::before { content: "\f831"; } +.bi-sign-turn-right::before { content: "\f832"; } +.bi-sign-turn-slight-left-fill::before { content: "\f833"; } +.bi-sign-turn-slight-left::before { content: "\f834"; } +.bi-sign-turn-slight-right-fill::before { content: "\f835"; } +.bi-sign-turn-slight-right::before { content: "\f836"; } +.bi-sign-yield-fill::before { content: "\f837"; } +.bi-sign-yield::before { content: "\f838"; } +.bi-ev-station-fill::before { content: "\f839"; } +.bi-ev-station::before { content: "\f83a"; } +.bi-fuel-pump-diesel-fill::before { content: "\f83b"; } +.bi-fuel-pump-diesel::before { content: "\f83c"; } +.bi-fuel-pump-fill::before { content: "\f83d"; } +.bi-fuel-pump::before { content: "\f83e"; } +.bi-0-circle-fill::before { content: "\f83f"; } +.bi-0-circle::before { content: "\f840"; } +.bi-0-square-fill::before { content: "\f841"; } +.bi-0-square::before { content: "\f842"; } +.bi-rocket-fill::before { content: "\f843"; } +.bi-rocket-takeoff-fill::before { content: "\f844"; } +.bi-rocket-takeoff::before { content: "\f845"; } +.bi-rocket::before { content: "\f846"; } +.bi-stripe::before { content: "\f847"; } +.bi-subscript::before { content: "\f848"; } +.bi-superscript::before { content: "\f849"; } +.bi-trello::before { content: "\f84a"; } +.bi-envelope-at-fill::before { content: "\f84b"; } +.bi-envelope-at::before { content: "\f84c"; } +.bi-regex::before { content: "\f84d"; } +.bi-text-wrap::before { content: "\f84e"; } +.bi-sign-dead-end-fill::before { content: "\f84f"; } +.bi-sign-dead-end::before { content: "\f850"; } +.bi-sign-do-not-enter-fill::before { content: "\f851"; } +.bi-sign-do-not-enter::before { content: "\f852"; } +.bi-sign-intersection-fill::before { content: "\f853"; } +.bi-sign-intersection-side-fill::before { content: "\f854"; } +.bi-sign-intersection-side::before { content: "\f855"; } +.bi-sign-intersection-t-fill::before { content: "\f856"; } +.bi-sign-intersection-t::before { content: "\f857"; } +.bi-sign-intersection-y-fill::before { content: "\f858"; } +.bi-sign-intersection-y::before { content: "\f859"; } +.bi-sign-intersection::before { content: "\f85a"; } +.bi-sign-merge-left-fill::before { content: "\f85b"; } +.bi-sign-merge-left::before { content: "\f85c"; } +.bi-sign-merge-right-fill::before { content: "\f85d"; } +.bi-sign-merge-right::before { content: "\f85e"; } +.bi-sign-no-left-turn-fill::before { content: "\f85f"; } +.bi-sign-no-left-turn::before { content: "\f860"; } +.bi-sign-no-parking-fill::before { content: "\f861"; } +.bi-sign-no-parking::before { content: "\f862"; } +.bi-sign-no-right-turn-fill::before { content: "\f863"; } +.bi-sign-no-right-turn::before { content: "\f864"; } +.bi-sign-railroad-fill::before { content: "\f865"; } +.bi-sign-railroad::before { content: "\f866"; } +.bi-building-add::before { content: "\f867"; } +.bi-building-check::before { content: "\f868"; } +.bi-building-dash::before { content: "\f869"; } +.bi-building-down::before { content: "\f86a"; } +.bi-building-exclamation::before { content: "\f86b"; } +.bi-building-fill-add::before { content: "\f86c"; } +.bi-building-fill-check::before { content: "\f86d"; } +.bi-building-fill-dash::before { content: "\f86e"; } +.bi-building-fill-down::before { content: "\f86f"; } +.bi-building-fill-exclamation::before { content: "\f870"; } +.bi-building-fill-gear::before { content: "\f871"; } +.bi-building-fill-lock::before { content: "\f872"; } +.bi-building-fill-slash::before { content: "\f873"; } +.bi-building-fill-up::before { content: "\f874"; } +.bi-building-fill-x::before { content: "\f875"; } +.bi-building-fill::before { content: "\f876"; } +.bi-building-gear::before { content: "\f877"; } +.bi-building-lock::before { content: "\f878"; } +.bi-building-slash::before { content: "\f879"; } +.bi-building-up::before { content: "\f87a"; } +.bi-building-x::before { content: "\f87b"; } +.bi-buildings-fill::before { content: "\f87c"; } +.bi-buildings::before { content: "\f87d"; } +.bi-bus-front-fill::before { content: "\f87e"; } +.bi-bus-front::before { content: "\f87f"; } +.bi-ev-front-fill::before { content: "\f880"; } +.bi-ev-front::before { content: "\f881"; } +.bi-globe-americas::before { content: "\f882"; } +.bi-globe-asia-australia::before { content: "\f883"; } +.bi-globe-central-south-asia::before { content: "\f884"; } +.bi-globe-europe-africa::before { content: "\f885"; } +.bi-house-add-fill::before { content: "\f886"; } +.bi-house-add::before { content: "\f887"; } +.bi-house-check-fill::before { content: "\f888"; } +.bi-house-check::before { content: "\f889"; } +.bi-house-dash-fill::before { content: "\f88a"; } +.bi-house-dash::before { content: "\f88b"; } +.bi-house-down-fill::before { content: "\f88c"; } +.bi-house-down::before { content: "\f88d"; } +.bi-house-exclamation-fill::before { content: "\f88e"; } +.bi-house-exclamation::before { content: "\f88f"; } +.bi-house-gear-fill::before { content: "\f890"; } +.bi-house-gear::before { content: "\f891"; } +.bi-house-lock-fill::before { content: "\f892"; } +.bi-house-lock::before { content: "\f893"; } +.bi-house-slash-fill::before { content: "\f894"; } +.bi-house-slash::before { content: "\f895"; } +.bi-house-up-fill::before { content: "\f896"; } +.bi-house-up::before { content: "\f897"; } +.bi-house-x-fill::before { content: "\f898"; } +.bi-house-x::before { content: "\f899"; } +.bi-person-add::before { content: "\f89a"; } +.bi-person-down::before { content: "\f89b"; } +.bi-person-exclamation::before { content: "\f89c"; } +.bi-person-fill-add::before { content: "\f89d"; } +.bi-person-fill-check::before { content: "\f89e"; } +.bi-person-fill-dash::before { content: "\f89f"; } +.bi-person-fill-down::before { content: "\f8a0"; } +.bi-person-fill-exclamation::before { content: "\f8a1"; } +.bi-person-fill-gear::before { content: "\f8a2"; } +.bi-person-fill-lock::before { content: "\f8a3"; } +.bi-person-fill-slash::before { content: "\f8a4"; } +.bi-person-fill-up::before { content: "\f8a5"; } +.bi-person-fill-x::before { content: "\f8a6"; } +.bi-person-gear::before { content: "\f8a7"; } +.bi-person-lock::before { content: "\f8a8"; } +.bi-person-slash::before { content: "\f8a9"; } +.bi-person-up::before { content: "\f8aa"; } +.bi-scooter::before { content: "\f8ab"; } +.bi-taxi-front-fill::before { content: "\f8ac"; } +.bi-taxi-front::before { content: "\f8ad"; } +.bi-amd::before { content: "\f8ae"; } +.bi-database-add::before { content: "\f8af"; } +.bi-database-check::before { content: "\f8b0"; } +.bi-database-dash::before { content: "\f8b1"; } +.bi-database-down::before { content: "\f8b2"; } +.bi-database-exclamation::before { content: "\f8b3"; } +.bi-database-fill-add::before { content: "\f8b4"; } +.bi-database-fill-check::before { content: "\f8b5"; } +.bi-database-fill-dash::before { content: "\f8b6"; } +.bi-database-fill-down::before { content: "\f8b7"; } +.bi-database-fill-exclamation::before { content: "\f8b8"; } +.bi-database-fill-gear::before { content: "\f8b9"; } +.bi-database-fill-lock::before { content: "\f8ba"; } +.bi-database-fill-slash::before { content: "\f8bb"; } +.bi-database-fill-up::before { content: "\f8bc"; } +.bi-database-fill-x::before { content: "\f8bd"; } +.bi-database-fill::before { content: "\f8be"; } +.bi-database-gear::before { content: "\f8bf"; } +.bi-database-lock::before { content: "\f8c0"; } +.bi-database-slash::before { content: "\f8c1"; } +.bi-database-up::before { content: "\f8c2"; } +.bi-database-x::before { content: "\f8c3"; } +.bi-database::before { content: "\f8c4"; } +.bi-houses-fill::before { content: "\f8c5"; } +.bi-houses::before { content: "\f8c6"; } +.bi-nvidia::before { content: "\f8c7"; } +.bi-person-vcard-fill::before { content: "\f8c8"; } +.bi-person-vcard::before { content: "\f8c9"; } +.bi-sina-weibo::before { content: "\f8ca"; } +.bi-tencent-qq::before { content: "\f8cb"; } +.bi-wikipedia::before { content: "\f8cc"; } +.bi-alphabet-uppercase::before { content: "\f2a5"; } +.bi-alphabet::before { content: "\f68a"; } +.bi-amazon::before { content: "\f68d"; } +.bi-arrows-collapse-vertical::before { content: "\f690"; } +.bi-arrows-expand-vertical::before { content: "\f695"; } +.bi-arrows-vertical::before { content: "\f698"; } +.bi-arrows::before { content: "\f6a2"; } +.bi-ban-fill::before { content: "\f6a3"; } +.bi-ban::before { content: "\f6b6"; } +.bi-bing::before { content: "\f6c2"; } +.bi-cake::before { content: "\f6e0"; } +.bi-cake2::before { content: "\f6ed"; } +.bi-cookie::before { content: "\f6ee"; } +.bi-copy::before { content: "\f759"; } +.bi-crosshair::before { content: "\f769"; } +.bi-crosshair2::before { content: "\f794"; } +.bi-emoji-astonished-fill::before { content: "\f795"; } +.bi-emoji-astonished::before { content: "\f79a"; } +.bi-emoji-grimace-fill::before { content: "\f79b"; } +.bi-emoji-grimace::before { content: "\f7a0"; } +.bi-emoji-grin-fill::before { content: "\f7a1"; } +.bi-emoji-grin::before { content: "\f7a6"; } +.bi-emoji-surprise-fill::before { content: "\f7a7"; } +.bi-emoji-surprise::before { content: "\f7ac"; } +.bi-emoji-tear-fill::before { content: "\f7ad"; } +.bi-emoji-tear::before { content: "\f7b2"; } +.bi-envelope-arrow-down-fill::before { content: "\f7b3"; } +.bi-envelope-arrow-down::before { content: "\f7b8"; } +.bi-envelope-arrow-up-fill::before { content: "\f7b9"; } +.bi-envelope-arrow-up::before { content: "\f7be"; } +.bi-feather::before { content: "\f7bf"; } +.bi-feather2::before { content: "\f7c4"; } +.bi-floppy-fill::before { content: "\f7c5"; } +.bi-floppy::before { content: "\f7d8"; } +.bi-floppy2-fill::before { content: "\f7d9"; } +.bi-floppy2::before { content: "\f7e4"; } +.bi-gitlab::before { content: "\f7e5"; } +.bi-highlighter::before { content: "\f7f8"; } +.bi-marker-tip::before { content: "\f802"; } +.bi-nvme-fill::before { content: "\f803"; } +.bi-nvme::before { content: "\f80c"; } +.bi-opencollective::before { content: "\f80d"; } +.bi-pci-card-network::before { content: "\f8cd"; } +.bi-pci-card-sound::before { content: "\f8ce"; } +.bi-radar::before { content: "\f8cf"; } +.bi-send-arrow-down-fill::before { content: "\f8d0"; } +.bi-send-arrow-down::before { content: "\f8d1"; } +.bi-send-arrow-up-fill::before { content: "\f8d2"; } +.bi-send-arrow-up::before { content: "\f8d3"; } +.bi-sim-slash-fill::before { content: "\f8d4"; } +.bi-sim-slash::before { content: "\f8d5"; } +.bi-sourceforge::before { content: "\f8d6"; } +.bi-substack::before { content: "\f8d7"; } +.bi-threads-fill::before { content: "\f8d8"; } +.bi-threads::before { content: "\f8d9"; } +.bi-transparency::before { content: "\f8da"; } +.bi-twitter-x::before { content: "\f8db"; } +.bi-type-h4::before { content: "\f8dc"; } +.bi-type-h5::before { content: "\f8dd"; } +.bi-type-h6::before { content: "\f8de"; } +.bi-backpack-fill::before { content: "\f8df"; } +.bi-backpack::before { content: "\f8e0"; } +.bi-backpack2-fill::before { content: "\f8e1"; } +.bi-backpack2::before { content: "\f8e2"; } +.bi-backpack3-fill::before { content: "\f8e3"; } +.bi-backpack3::before { content: "\f8e4"; } +.bi-backpack4-fill::before { content: "\f8e5"; } +.bi-backpack4::before { content: "\f8e6"; } +.bi-brilliance::before { content: "\f8e7"; } +.bi-cake-fill::before { content: "\f8e8"; } +.bi-cake2-fill::before { content: "\f8e9"; } +.bi-duffle-fill::before { content: "\f8ea"; } +.bi-duffle::before { content: "\f8eb"; } +.bi-exposure::before { content: "\f8ec"; } +.bi-gender-neuter::before { content: "\f8ed"; } +.bi-highlights::before { content: "\f8ee"; } +.bi-luggage-fill::before { content: "\f8ef"; } +.bi-luggage::before { content: "\f8f0"; } +.bi-mailbox-flag::before { content: "\f8f1"; } +.bi-mailbox2-flag::before { content: "\f8f2"; } +.bi-noise-reduction::before { content: "\f8f3"; } +.bi-passport-fill::before { content: "\f8f4"; } +.bi-passport::before { content: "\f8f5"; } +.bi-person-arms-up::before { content: "\f8f6"; } +.bi-person-raised-hand::before { content: "\f8f7"; } +.bi-person-standing-dress::before { content: "\f8f8"; } +.bi-person-standing::before { content: "\f8f9"; } +.bi-person-walking::before { content: "\f8fa"; } +.bi-person-wheelchair::before { content: "\f8fb"; } +.bi-shadows::before { content: "\f8fc"; } +.bi-suitcase-fill::before { content: "\f8fd"; } +.bi-suitcase-lg-fill::before { content: "\f8fe"; } +.bi-suitcase-lg::before { content: "\f8ff"; } +.bi-suitcase::before { content: "\f900"; } +.bi-suitcase2-fill::before { content: "\f901"; } +.bi-suitcase2::before { content: "\f902"; } +.bi-vignette::before { content: "\f903"; } diff --git a/docs/site_libs/bootstrap/bootstrap-icons.woff b/docs/site_libs/bootstrap/bootstrap-icons.woff new file mode 100644 index 0000000000000000000000000000000000000000..72e4596f055c34d9a2060fb55f851fa726e3e0f7 --- /dev/null +++ b/docs/site_libs/bootstrap/bootstrap-icons.woff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d4572ef314e1b734cdd6485f913b0396d81bedf4d216a47cfde0cdf32a9316e +size 176200 diff --git a/docs/site_libs/bootstrap/bootstrap.min.js b/docs/site_libs/bootstrap/bootstrap.min.js new file mode 100644 index 0000000000000000000000000000000000000000..e8f21f703f7bb4e9ab84daca93c9ee1d5358a316 --- /dev/null +++ b/docs/site_libs/bootstrap/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.1 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function j(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${j(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${j(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${j(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.1"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return n(e)},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",Mt="collapsing",jt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(Mt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(Mt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(jt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Me(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const je={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Me(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:Me(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==P(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],M=f?-T[$]/2:0,j=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-M-q-z-O.mainAxis:j-q-z-O.mainAxis,K=v?-E[$]/2+M+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,Mn=`hide${xn}`,jn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,Mn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,jn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,jn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",Ms="Home",js="End",Fs="active",Hs="fade",Ws="show",Bs=":not(.dropdown-toggle)",zs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Rs=`.nav-link${Bs}, .list-group-item${Bs}, [role="tab"]${Bs}, ${zs}`,qs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Vs extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,Ms,js].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([Ms,js].includes(t.key))i=e[t.key===Ms?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Vs.getOrCreateInstance(i).show())}_getChildren(){return z.find(Rs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(Rs)?t:z.findOne(Rs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Vs.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,zs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Vs.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(qs))Vs.getOrCreateInstance(t)})),m(Vs);const Ks=".bs.toast",Qs=`mouseover${Ks}`,Xs=`mouseout${Ks}`,Ys=`focusin${Ks}`,Us=`focusout${Ks}`,Gs=`hide${Ks}`,Js=`hidden${Ks}`,Zs=`show${Ks}`,to=`shown${Ks}`,eo="hide",io="show",no="showing",so={animation:"boolean",autohide:"boolean",delay:"number"},oo={animation:!0,autohide:!0,delay:5e3};class ro extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return oo}static get DefaultType(){return so}static get NAME(){return"toast"}show(){N.trigger(this._element,Zs).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(eo),d(this._element),this._element.classList.add(io,no),this._queueCallback((()=>{this._element.classList.remove(no),N.trigger(this._element,to),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Gs).defaultPrevented||(this._element.classList.add(no),this._queueCallback((()=>{this._element.classList.add(eo),this._element.classList.remove(no,io),N.trigger(this._element,Js)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(io),super.dispose()}isShown(){return this._element.classList.contains(io)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Qs,(t=>this._onInteraction(t,!0))),N.on(this._element,Xs,(t=>this._onInteraction(t,!1))),N.on(this._element,Ys,(t=>this._onInteraction(t,!0))),N.on(this._element,Us,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ro.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ro),m(ro),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Vs,Toast:ro,Tooltip:cs}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/docs/site_libs/clipboard/clipboard.min.js b/docs/site_libs/clipboard/clipboard.min.js new file mode 100644 index 0000000000000000000000000000000000000000..1103f811ed80f17985ecf61e0d50e3359484244f --- /dev/null +++ b/docs/site_libs/clipboard/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1.anchorjs-link,.anchorjs-link:focus{opacity:1}",A.sheet.cssRules.length),A.sheet.insertRule("[data-anchorjs-icon]::after{content:attr(data-anchorjs-icon)}",A.sheet.cssRules.length),A.sheet.insertRule('@font-face{font-family:anchorjs-icons;src:url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype")}',A.sheet.cssRules.length)),h=document.querySelectorAll("[id]"),t=[].map.call(h,function(A){return A.id}),i=0;i\]./()*\\\n\t\b\v\u00A0]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),A=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||A||!1}}}); +// @license-end \ No newline at end of file diff --git a/docs/site_libs/quarto-html/popper.min.js b/docs/site_libs/quarto-html/popper.min.js new file mode 100644 index 0000000000000000000000000000000000000000..e3726d728b717eb344760b3db47f7e9b142ed0fb --- /dev/null +++ b/docs/site_libs/quarto-html/popper.min.js @@ -0,0 +1,6 @@ +/** + * @popperjs/core v2.11.7 - MIT License + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Popper={})}(this,(function(e){"use strict";function t(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function n(e){return e instanceof t(e).Element||e instanceof Element}function r(e){return e instanceof t(e).HTMLElement||e instanceof HTMLElement}function o(e){return"undefined"!=typeof ShadowRoot&&(e instanceof t(e).ShadowRoot||e instanceof ShadowRoot)}var i=Math.max,a=Math.min,s=Math.round;function f(){var e=navigator.userAgentData;return null!=e&&e.brands&&Array.isArray(e.brands)?e.brands.map((function(e){return e.brand+"/"+e.version})).join(" "):navigator.userAgent}function c(){return!/^((?!chrome|android).)*safari/i.test(f())}function p(e,o,i){void 0===o&&(o=!1),void 0===i&&(i=!1);var a=e.getBoundingClientRect(),f=1,p=1;o&&r(e)&&(f=e.offsetWidth>0&&s(a.width)/e.offsetWidth||1,p=e.offsetHeight>0&&s(a.height)/e.offsetHeight||1);var u=(n(e)?t(e):window).visualViewport,l=!c()&&i,d=(a.left+(l&&u?u.offsetLeft:0))/f,h=(a.top+(l&&u?u.offsetTop:0))/p,m=a.width/f,v=a.height/p;return{width:m,height:v,top:h,right:d+m,bottom:h+v,left:d,x:d,y:h}}function u(e){var n=t(e);return{scrollLeft:n.pageXOffset,scrollTop:n.pageYOffset}}function l(e){return e?(e.nodeName||"").toLowerCase():null}function d(e){return((n(e)?e.ownerDocument:e.document)||window.document).documentElement}function h(e){return p(d(e)).left+u(e).scrollLeft}function m(e){return t(e).getComputedStyle(e)}function v(e){var t=m(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function y(e,n,o){void 0===o&&(o=!1);var i,a,f=r(n),c=r(n)&&function(e){var t=e.getBoundingClientRect(),n=s(t.width)/e.offsetWidth||1,r=s(t.height)/e.offsetHeight||1;return 1!==n||1!==r}(n),m=d(n),y=p(e,c,o),g={scrollLeft:0,scrollTop:0},b={x:0,y:0};return(f||!f&&!o)&&(("body"!==l(n)||v(m))&&(g=(i=n)!==t(i)&&r(i)?{scrollLeft:(a=i).scrollLeft,scrollTop:a.scrollTop}:u(i)),r(n)?((b=p(n,!0)).x+=n.clientLeft,b.y+=n.clientTop):m&&(b.x=h(m))),{x:y.left+g.scrollLeft-b.x,y:y.top+g.scrollTop-b.y,width:y.width,height:y.height}}function g(e){var t=p(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function b(e){return"html"===l(e)?e:e.assignedSlot||e.parentNode||(o(e)?e.host:null)||d(e)}function x(e){return["html","body","#document"].indexOf(l(e))>=0?e.ownerDocument.body:r(e)&&v(e)?e:x(b(e))}function w(e,n){var r;void 0===n&&(n=[]);var o=x(e),i=o===(null==(r=e.ownerDocument)?void 0:r.body),a=t(o),s=i?[a].concat(a.visualViewport||[],v(o)?o:[]):o,f=n.concat(s);return i?f:f.concat(w(b(s)))}function O(e){return["table","td","th"].indexOf(l(e))>=0}function j(e){return r(e)&&"fixed"!==m(e).position?e.offsetParent:null}function E(e){for(var n=t(e),i=j(e);i&&O(i)&&"static"===m(i).position;)i=j(i);return i&&("html"===l(i)||"body"===l(i)&&"static"===m(i).position)?n:i||function(e){var t=/firefox/i.test(f());if(/Trident/i.test(f())&&r(e)&&"fixed"===m(e).position)return null;var n=b(e);for(o(n)&&(n=n.host);r(n)&&["html","body"].indexOf(l(n))<0;){var i=m(n);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return n;n=n.parentNode}return null}(e)||n}var D="top",A="bottom",L="right",P="left",M="auto",k=[D,A,L,P],W="start",B="end",H="viewport",T="popper",R=k.reduce((function(e,t){return e.concat([t+"-"+W,t+"-"+B])}),[]),S=[].concat(k,[M]).reduce((function(e,t){return e.concat([t,t+"-"+W,t+"-"+B])}),[]),V=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function q(e){var t=new Map,n=new Set,r=[];function o(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!n.has(e)){var r=t.get(e);r&&o(r)}})),r.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||o(e)})),r}function C(e){return e.split("-")[0]}function N(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&o(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function I(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function _(e,r,o){return r===H?I(function(e,n){var r=t(e),o=d(e),i=r.visualViewport,a=o.clientWidth,s=o.clientHeight,f=0,p=0;if(i){a=i.width,s=i.height;var u=c();(u||!u&&"fixed"===n)&&(f=i.offsetLeft,p=i.offsetTop)}return{width:a,height:s,x:f+h(e),y:p}}(e,o)):n(r)?function(e,t){var n=p(e,!1,"fixed"===t);return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}(r,o):I(function(e){var t,n=d(e),r=u(e),o=null==(t=e.ownerDocument)?void 0:t.body,a=i(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=i(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),f=-r.scrollLeft+h(e),c=-r.scrollTop;return"rtl"===m(o||n).direction&&(f+=i(n.clientWidth,o?o.clientWidth:0)-a),{width:a,height:s,x:f,y:c}}(d(e)))}function F(e,t,o,s){var f="clippingParents"===t?function(e){var t=w(b(e)),o=["absolute","fixed"].indexOf(m(e).position)>=0&&r(e)?E(e):e;return n(o)?t.filter((function(e){return n(e)&&N(e,o)&&"body"!==l(e)})):[]}(e):[].concat(t),c=[].concat(f,[o]),p=c[0],u=c.reduce((function(t,n){var r=_(e,n,s);return t.top=i(r.top,t.top),t.right=a(r.right,t.right),t.bottom=a(r.bottom,t.bottom),t.left=i(r.left,t.left),t}),_(e,p,s));return u.width=u.right-u.left,u.height=u.bottom-u.top,u.x=u.left,u.y=u.top,u}function U(e){return e.split("-")[1]}function z(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function X(e){var t,n=e.reference,r=e.element,o=e.placement,i=o?C(o):null,a=o?U(o):null,s=n.x+n.width/2-r.width/2,f=n.y+n.height/2-r.height/2;switch(i){case D:t={x:s,y:n.y-r.height};break;case A:t={x:s,y:n.y+n.height};break;case L:t={x:n.x+n.width,y:f};break;case P:t={x:n.x-r.width,y:f};break;default:t={x:n.x,y:n.y}}var c=i?z(i):null;if(null!=c){var p="y"===c?"height":"width";switch(a){case W:t[c]=t[c]-(n[p]/2-r[p]/2);break;case B:t[c]=t[c]+(n[p]/2-r[p]/2)}}return t}function Y(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function G(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function J(e,t){void 0===t&&(t={});var r=t,o=r.placement,i=void 0===o?e.placement:o,a=r.strategy,s=void 0===a?e.strategy:a,f=r.boundary,c=void 0===f?"clippingParents":f,u=r.rootBoundary,l=void 0===u?H:u,h=r.elementContext,m=void 0===h?T:h,v=r.altBoundary,y=void 0!==v&&v,g=r.padding,b=void 0===g?0:g,x=Y("number"!=typeof b?b:G(b,k)),w=m===T?"reference":T,O=e.rects.popper,j=e.elements[y?w:m],E=F(n(j)?j:j.contextElement||d(e.elements.popper),c,l,s),P=p(e.elements.reference),M=X({reference:P,element:O,strategy:"absolute",placement:i}),W=I(Object.assign({},O,M)),B=m===T?W:P,R={top:E.top-B.top+x.top,bottom:B.bottom-E.bottom+x.bottom,left:E.left-B.left+x.left,right:B.right-E.right+x.right},S=e.modifiersData.offset;if(m===T&&S){var V=S[i];Object.keys(R).forEach((function(e){var t=[L,A].indexOf(e)>=0?1:-1,n=[D,A].indexOf(e)>=0?"y":"x";R[e]+=V[n]*t}))}return R}var K={placement:"bottom",modifiers:[],strategy:"absolute"};function Q(){for(var e=arguments.length,t=new Array(e),n=0;n=0?-1:1,i="function"==typeof n?n(Object.assign({},t,{placement:e})):n,a=i[0],s=i[1];return a=a||0,s=(s||0)*o,[P,L].indexOf(r)>=0?{x:s,y:a}:{x:a,y:s}}(n,t.rects,i),e}),{}),s=a[t.placement],f=s.x,c=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=f,t.modifiersData.popperOffsets.y+=c),t.modifiersData[r]=a}},se={left:"right",right:"left",bottom:"top",top:"bottom"};function fe(e){return e.replace(/left|right|bottom|top/g,(function(e){return se[e]}))}var ce={start:"end",end:"start"};function pe(e){return e.replace(/start|end/g,(function(e){return ce[e]}))}function ue(e,t){void 0===t&&(t={});var n=t,r=n.placement,o=n.boundary,i=n.rootBoundary,a=n.padding,s=n.flipVariations,f=n.allowedAutoPlacements,c=void 0===f?S:f,p=U(r),u=p?s?R:R.filter((function(e){return U(e)===p})):k,l=u.filter((function(e){return c.indexOf(e)>=0}));0===l.length&&(l=u);var d=l.reduce((function(t,n){return t[n]=J(e,{placement:n,boundary:o,rootBoundary:i,padding:a})[C(n)],t}),{});return Object.keys(d).sort((function(e,t){return d[e]-d[t]}))}var le={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var o=n.mainAxis,i=void 0===o||o,a=n.altAxis,s=void 0===a||a,f=n.fallbackPlacements,c=n.padding,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.flipVariations,h=void 0===d||d,m=n.allowedAutoPlacements,v=t.options.placement,y=C(v),g=f||(y===v||!h?[fe(v)]:function(e){if(C(e)===M)return[];var t=fe(e);return[pe(e),t,pe(t)]}(v)),b=[v].concat(g).reduce((function(e,n){return e.concat(C(n)===M?ue(t,{placement:n,boundary:p,rootBoundary:u,padding:c,flipVariations:h,allowedAutoPlacements:m}):n)}),[]),x=t.rects.reference,w=t.rects.popper,O=new Map,j=!0,E=b[0],k=0;k=0,S=R?"width":"height",V=J(t,{placement:B,boundary:p,rootBoundary:u,altBoundary:l,padding:c}),q=R?T?L:P:T?A:D;x[S]>w[S]&&(q=fe(q));var N=fe(q),I=[];if(i&&I.push(V[H]<=0),s&&I.push(V[q]<=0,V[N]<=0),I.every((function(e){return e}))){E=B,j=!1;break}O.set(B,I)}if(j)for(var _=function(e){var t=b.find((function(t){var n=O.get(t);if(n)return n.slice(0,e).every((function(e){return e}))}));if(t)return E=t,"break"},F=h?3:1;F>0;F--){if("break"===_(F))break}t.placement!==E&&(t.modifiersData[r]._skip=!0,t.placement=E,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function de(e,t,n){return i(e,a(t,n))}var he={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name,o=n.mainAxis,s=void 0===o||o,f=n.altAxis,c=void 0!==f&&f,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.padding,h=n.tether,m=void 0===h||h,v=n.tetherOffset,y=void 0===v?0:v,b=J(t,{boundary:p,rootBoundary:u,padding:d,altBoundary:l}),x=C(t.placement),w=U(t.placement),O=!w,j=z(x),M="x"===j?"y":"x",k=t.modifiersData.popperOffsets,B=t.rects.reference,H=t.rects.popper,T="function"==typeof y?y(Object.assign({},t.rects,{placement:t.placement})):y,R="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),S=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,V={x:0,y:0};if(k){if(s){var q,N="y"===j?D:P,I="y"===j?A:L,_="y"===j?"height":"width",F=k[j],X=F+b[N],Y=F-b[I],G=m?-H[_]/2:0,K=w===W?B[_]:H[_],Q=w===W?-H[_]:-B[_],Z=t.elements.arrow,$=m&&Z?g(Z):{width:0,height:0},ee=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},te=ee[N],ne=ee[I],re=de(0,B[_],$[_]),oe=O?B[_]/2-G-re-te-R.mainAxis:K-re-te-R.mainAxis,ie=O?-B[_]/2+G+re+ne+R.mainAxis:Q+re+ne+R.mainAxis,ae=t.elements.arrow&&E(t.elements.arrow),se=ae?"y"===j?ae.clientTop||0:ae.clientLeft||0:0,fe=null!=(q=null==S?void 0:S[j])?q:0,ce=F+ie-fe,pe=de(m?a(X,F+oe-fe-se):X,F,m?i(Y,ce):Y);k[j]=pe,V[j]=pe-F}if(c){var ue,le="x"===j?D:P,he="x"===j?A:L,me=k[M],ve="y"===M?"height":"width",ye=me+b[le],ge=me-b[he],be=-1!==[D,P].indexOf(x),xe=null!=(ue=null==S?void 0:S[M])?ue:0,we=be?ye:me-B[ve]-H[ve]-xe+R.altAxis,Oe=be?me+B[ve]+H[ve]-xe-R.altAxis:ge,je=m&&be?function(e,t,n){var r=de(e,t,n);return r>n?n:r}(we,me,Oe):de(m?we:ye,me,m?Oe:ge);k[M]=je,V[M]=je-me}t.modifiersData[r]=V}},requiresIfExists:["offset"]};var me={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state,r=e.name,o=e.options,i=n.elements.arrow,a=n.modifiersData.popperOffsets,s=C(n.placement),f=z(s),c=[P,L].indexOf(s)>=0?"height":"width";if(i&&a){var p=function(e,t){return Y("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:G(e,k))}(o.padding,n),u=g(i),l="y"===f?D:P,d="y"===f?A:L,h=n.rects.reference[c]+n.rects.reference[f]-a[f]-n.rects.popper[c],m=a[f]-n.rects.reference[f],v=E(i),y=v?"y"===f?v.clientHeight||0:v.clientWidth||0:0,b=h/2-m/2,x=p[l],w=y-u[c]-p[d],O=y/2-u[c]/2+b,j=de(x,O,w),M=f;n.modifiersData[r]=((t={})[M]=j,t.centerOffset=j-O,t)}},effect:function(e){var t=e.state,n=e.options.element,r=void 0===n?"[data-popper-arrow]":n;null!=r&&("string"!=typeof r||(r=t.elements.popper.querySelector(r)))&&N(t.elements.popper,r)&&(t.elements.arrow=r)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ve(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function ye(e){return[D,L,A,P].some((function(t){return e[t]>=0}))}var ge={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state,n=e.name,r=t.rects.reference,o=t.rects.popper,i=t.modifiersData.preventOverflow,a=J(t,{elementContext:"reference"}),s=J(t,{altBoundary:!0}),f=ve(a,r),c=ve(s,o,i),p=ye(f),u=ye(c);t.modifiersData[n]={referenceClippingOffsets:f,popperEscapeOffsets:c,isReferenceHidden:p,hasPopperEscaped:u},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":u})}},be=Z({defaultModifiers:[ee,te,oe,ie]}),xe=[ee,te,oe,ie,ae,le,he,me,ge],we=Z({defaultModifiers:xe});e.applyStyles=ie,e.arrow=me,e.computeStyles=oe,e.createPopper=we,e.createPopperLite=be,e.defaultModifiers=xe,e.detectOverflow=J,e.eventListeners=ee,e.flip=le,e.hide=ge,e.offset=ae,e.popperGenerator=Z,e.popperOffsets=te,e.preventOverflow=he,Object.defineProperty(e,"__esModule",{value:!0})})); + diff --git a/docs/site_libs/quarto-html/quarto-syntax-highlighting-549806ee2085284f45b00abea8c6df48.css b/docs/site_libs/quarto-html/quarto-syntax-highlighting-549806ee2085284f45b00abea8c6df48.css new file mode 100644 index 0000000000000000000000000000000000000000..80e34e41a5883b72c3c129cd2b218d7a0453433c --- /dev/null +++ b/docs/site_libs/quarto-html/quarto-syntax-highlighting-549806ee2085284f45b00abea8c6df48.css @@ -0,0 +1,205 @@ +/* quarto syntax highlight colors */ +:root { + --quarto-hl-ot-color: #003B4F; + --quarto-hl-at-color: #657422; + --quarto-hl-ss-color: #20794D; + --quarto-hl-an-color: #5E5E5E; + --quarto-hl-fu-color: #4758AB; + --quarto-hl-st-color: #20794D; + --quarto-hl-cf-color: #003B4F; + --quarto-hl-op-color: #5E5E5E; + --quarto-hl-er-color: #AD0000; + --quarto-hl-bn-color: #AD0000; + --quarto-hl-al-color: #AD0000; + --quarto-hl-va-color: #111111; + --quarto-hl-bu-color: inherit; + --quarto-hl-ex-color: inherit; + --quarto-hl-pp-color: #AD0000; + --quarto-hl-in-color: #5E5E5E; + --quarto-hl-vs-color: #20794D; + --quarto-hl-wa-color: #5E5E5E; + --quarto-hl-do-color: #5E5E5E; + --quarto-hl-im-color: #00769E; + --quarto-hl-ch-color: #20794D; + --quarto-hl-dt-color: #AD0000; + --quarto-hl-fl-color: #AD0000; + --quarto-hl-co-color: #5E5E5E; + --quarto-hl-cv-color: #5E5E5E; + --quarto-hl-cn-color: #8f5902; + --quarto-hl-sc-color: #5E5E5E; + --quarto-hl-dv-color: #AD0000; + --quarto-hl-kw-color: #003B4F; +} + +/* other quarto variables */ +:root { + --quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +pre > code.sourceCode > span { + color: #003B4F; +} + +code span { + color: #003B4F; +} + +code.sourceCode > span { + color: #003B4F; +} + +div.sourceCode, +div.sourceCode pre.sourceCode { + color: #003B4F; +} + +code span.ot { + color: #003B4F; + font-style: inherit; +} + +code span.at { + color: #657422; + font-style: inherit; +} + +code span.ss { + color: #20794D; + font-style: inherit; +} + +code span.an { + color: #5E5E5E; + font-style: inherit; +} + +code span.fu { + color: #4758AB; + font-style: inherit; +} + +code span.st { + color: #20794D; + font-style: inherit; +} + +code span.cf { + color: #003B4F; + font-weight: bold; + font-style: inherit; +} + +code span.op { + color: #5E5E5E; + font-style: inherit; +} + +code span.er { + color: #AD0000; + font-style: inherit; +} + +code span.bn { + color: #AD0000; + font-style: inherit; +} + +code span.al { + color: #AD0000; + font-style: inherit; +} + +code span.va { + color: #111111; + font-style: inherit; +} + +code span.bu { + font-style: inherit; +} + +code span.ex { + font-style: inherit; +} + +code span.pp { + color: #AD0000; + font-style: inherit; +} + +code span.in { + color: #5E5E5E; + font-style: inherit; +} + +code span.vs { + color: #20794D; + font-style: inherit; +} + +code span.wa { + color: #5E5E5E; + font-style: italic; +} + +code span.do { + color: #5E5E5E; + font-style: italic; +} + +code span.im { + color: #00769E; + font-style: inherit; +} + +code span.ch { + color: #20794D; + font-style: inherit; +} + +code span.dt { + color: #AD0000; + font-style: inherit; +} + +code span.fl { + color: #AD0000; + font-style: inherit; +} + +code span.co { + color: #5E5E5E; + font-style: inherit; +} + +code span.cv { + color: #5E5E5E; + font-style: italic; +} + +code span.cn { + color: #8f5902; + font-style: inherit; +} + +code span.sc { + color: #5E5E5E; + font-style: inherit; +} + +code span.dv { + color: #AD0000; + font-style: inherit; +} + +code span.kw { + color: #003B4F; + font-weight: bold; + font-style: inherit; +} + +.prevent-inlining { + content: " { + // Find any conflicting margin elements and add margins to the + // top to prevent overlap + const marginChildren = window.document.querySelectorAll( + ".column-margin.column-container > *, .margin-caption, .aside" + ); + + let lastBottom = 0; + for (const marginChild of marginChildren) { + if (marginChild.offsetParent !== null) { + // clear the top margin so we recompute it + marginChild.style.marginTop = null; + const top = marginChild.getBoundingClientRect().top + window.scrollY; + if (top < lastBottom) { + const marginChildStyle = window.getComputedStyle(marginChild); + const marginBottom = parseFloat(marginChildStyle["marginBottom"]); + const margin = lastBottom - top + marginBottom; + marginChild.style.marginTop = `${margin}px`; + } + const styles = window.getComputedStyle(marginChild); + const marginTop = parseFloat(styles["marginTop"]); + lastBottom = top + marginChild.getBoundingClientRect().height + marginTop; + } + } +}; + +window.document.addEventListener("DOMContentLoaded", function (_event) { + // Recompute the position of margin elements anytime the body size changes + if (window.ResizeObserver) { + const resizeObserver = new window.ResizeObserver( + throttle(() => { + layoutMarginEls(); + if ( + window.document.body.getBoundingClientRect().width < 990 && + isReaderMode() + ) { + quartoToggleReader(); + } + }, 50) + ); + resizeObserver.observe(window.document.body); + } + + const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]'); + const sidebarEl = window.document.getElementById("quarto-sidebar"); + const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left"); + const marginSidebarEl = window.document.getElementById( + "quarto-margin-sidebar" + ); + // function to determine whether the element has a previous sibling that is active + const prevSiblingIsActiveLink = (el) => { + const sibling = el.previousElementSibling; + if (sibling && sibling.tagName === "A") { + return sibling.classList.contains("active"); + } else { + return false; + } + }; + + // fire slideEnter for bootstrap tab activations (for htmlwidget resize behavior) + function fireSlideEnter(e) { + const event = window.document.createEvent("Event"); + event.initEvent("slideenter", true, true); + window.document.dispatchEvent(event); + } + const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]'); + tabs.forEach((tab) => { + tab.addEventListener("shown.bs.tab", fireSlideEnter); + }); + + // fire slideEnter for tabby tab activations (for htmlwidget resize behavior) + document.addEventListener("tabby", fireSlideEnter, false); + + // Track scrolling and mark TOC links as active + // get table of contents and sidebar (bail if we don't have at least one) + const tocLinks = tocEl + ? [...tocEl.querySelectorAll("a[data-scroll-target]")] + : []; + const makeActive = (link) => tocLinks[link].classList.add("active"); + const removeActive = (link) => tocLinks[link].classList.remove("active"); + const removeAllActive = () => + [...Array(tocLinks.length).keys()].forEach((link) => removeActive(link)); + + // activate the anchor for a section associated with this TOC entry + tocLinks.forEach((link) => { + link.addEventListener("click", () => { + if (link.href.indexOf("#") !== -1) { + const anchor = link.href.split("#")[1]; + const heading = window.document.querySelector( + `[data-anchor-id="${anchor}"]` + ); + if (heading) { + // Add the class + heading.classList.add("reveal-anchorjs-link"); + + // function to show the anchor + const handleMouseout = () => { + heading.classList.remove("reveal-anchorjs-link"); + heading.removeEventListener("mouseout", handleMouseout); + }; + + // add a function to clear the anchor when the user mouses out of it + heading.addEventListener("mouseout", handleMouseout); + } + } + }); + }); + + const sections = tocLinks.map((link) => { + const target = link.getAttribute("data-scroll-target"); + if (target.startsWith("#")) { + return window.document.getElementById(decodeURI(`${target.slice(1)}`)); + } else { + return window.document.querySelector(decodeURI(`${target}`)); + } + }); + + const sectionMargin = 200; + let currentActive = 0; + // track whether we've initialized state the first time + let init = false; + + const updateActiveLink = () => { + // The index from bottom to top (e.g. reversed list) + let sectionIndex = -1; + if ( + window.innerHeight + window.pageYOffset >= + window.document.body.offsetHeight + ) { + // This is the no-scroll case where last section should be the active one + sectionIndex = 0; + } else { + // This finds the last section visible on screen that should be made active + sectionIndex = [...sections].reverse().findIndex((section) => { + if (section) { + return window.pageYOffset >= section.offsetTop - sectionMargin; + } else { + return false; + } + }); + } + if (sectionIndex > -1) { + const current = sections.length - sectionIndex - 1; + if (current !== currentActive) { + removeAllActive(); + currentActive = current; + makeActive(current); + if (init) { + window.dispatchEvent(sectionChanged); + } + init = true; + } + } + }; + + const inHiddenRegion = (top, bottom, hiddenRegions) => { + for (const region of hiddenRegions) { + if (top <= region.bottom && bottom >= region.top) { + return true; + } + } + return false; + }; + + const categorySelector = "header.quarto-title-block .quarto-category"; + const activateCategories = (href) => { + // Find any categories + // Surround them with a link pointing back to: + // #category=Authoring + try { + const categoryEls = window.document.querySelectorAll(categorySelector); + for (const categoryEl of categoryEls) { + const categoryText = categoryEl.textContent; + if (categoryText) { + const link = `${href}#category=${encodeURIComponent(categoryText)}`; + const linkEl = window.document.createElement("a"); + linkEl.setAttribute("href", link); + for (const child of categoryEl.childNodes) { + linkEl.append(child); + } + categoryEl.appendChild(linkEl); + } + } + } catch { + // Ignore errors + } + }; + function hasTitleCategories() { + return window.document.querySelector(categorySelector) !== null; + } + + function offsetRelativeUrl(url) { + const offset = getMeta("quarto:offset"); + return offset ? offset + url : url; + } + + function offsetAbsoluteUrl(url) { + const offset = getMeta("quarto:offset"); + const baseUrl = new URL(offset, window.location); + + const projRelativeUrl = url.replace(baseUrl, ""); + if (projRelativeUrl.startsWith("/")) { + return projRelativeUrl; + } else { + return "/" + projRelativeUrl; + } + } + + // read a meta tag value + function getMeta(metaName) { + const metas = window.document.getElementsByTagName("meta"); + for (let i = 0; i < metas.length; i++) { + if (metas[i].getAttribute("name") === metaName) { + return metas[i].getAttribute("content"); + } + } + return ""; + } + + async function findAndActivateCategories() { + // Categories search with listing only use path without query + const currentPagePath = offsetAbsoluteUrl( + window.location.origin + window.location.pathname + ); + const response = await fetch(offsetRelativeUrl("listings.json")); + if (response.status == 200) { + return response.json().then(function (listingPaths) { + const listingHrefs = []; + for (const listingPath of listingPaths) { + const pathWithoutLeadingSlash = listingPath.listing.substring(1); + for (const item of listingPath.items) { + if ( + item === currentPagePath || + item === currentPagePath + "index.html" + ) { + // Resolve this path against the offset to be sure + // we already are using the correct path to the listing + // (this adjusts the listing urls to be rooted against + // whatever root the page is actually running against) + const relative = offsetRelativeUrl(pathWithoutLeadingSlash); + const baseUrl = window.location; + const resolvedPath = new URL(relative, baseUrl); + listingHrefs.push(resolvedPath.pathname); + break; + } + } + } + + // Look up the tree for a nearby linting and use that if we find one + const nearestListing = findNearestParentListing( + offsetAbsoluteUrl(window.location.pathname), + listingHrefs + ); + if (nearestListing) { + activateCategories(nearestListing); + } else { + // See if the referrer is a listing page for this item + const referredRelativePath = offsetAbsoluteUrl(document.referrer); + const referrerListing = listingHrefs.find((listingHref) => { + const isListingReferrer = + listingHref === referredRelativePath || + listingHref === referredRelativePath + "index.html"; + return isListingReferrer; + }); + + if (referrerListing) { + // Try to use the referrer if possible + activateCategories(referrerListing); + } else if (listingHrefs.length > 0) { + // Otherwise, just fall back to the first listing + activateCategories(listingHrefs[0]); + } + } + }); + } + } + if (hasTitleCategories()) { + findAndActivateCategories(); + } + + const findNearestParentListing = (href, listingHrefs) => { + if (!href || !listingHrefs) { + return undefined; + } + // Look up the tree for a nearby linting and use that if we find one + const relativeParts = href.substring(1).split("/"); + while (relativeParts.length > 0) { + const path = relativeParts.join("/"); + for (const listingHref of listingHrefs) { + if (listingHref.startsWith(path)) { + return listingHref; + } + } + relativeParts.pop(); + } + + return undefined; + }; + + const manageSidebarVisiblity = (el, placeholderDescriptor) => { + let isVisible = true; + let elRect; + + return (hiddenRegions) => { + if (el === null) { + return; + } + + // Find the last element of the TOC + const lastChildEl = el.lastElementChild; + + if (lastChildEl) { + // Converts the sidebar to a menu + const convertToMenu = () => { + for (const child of el.children) { + child.style.opacity = 0; + child.style.overflow = "hidden"; + child.style.pointerEvents = "none"; + } + + nexttick(() => { + const toggleContainer = window.document.createElement("div"); + toggleContainer.style.width = "100%"; + toggleContainer.classList.add("zindex-over-content"); + toggleContainer.classList.add("quarto-sidebar-toggle"); + toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom + toggleContainer.id = placeholderDescriptor.id; + toggleContainer.style.position = "fixed"; + + const toggleIcon = window.document.createElement("i"); + toggleIcon.classList.add("quarto-sidebar-toggle-icon"); + toggleIcon.classList.add("bi"); + toggleIcon.classList.add("bi-caret-down-fill"); + + const toggleTitle = window.document.createElement("div"); + const titleEl = window.document.body.querySelector( + placeholderDescriptor.titleSelector + ); + if (titleEl) { + toggleTitle.append( + titleEl.textContent || titleEl.innerText, + toggleIcon + ); + } + toggleTitle.classList.add("zindex-over-content"); + toggleTitle.classList.add("quarto-sidebar-toggle-title"); + toggleContainer.append(toggleTitle); + + const toggleContents = window.document.createElement("div"); + toggleContents.classList = el.classList; + toggleContents.classList.add("zindex-over-content"); + toggleContents.classList.add("quarto-sidebar-toggle-contents"); + for (const child of el.children) { + if (child.id === "toc-title") { + continue; + } + + const clone = child.cloneNode(true); + clone.style.opacity = 1; + clone.style.pointerEvents = null; + clone.style.display = null; + toggleContents.append(clone); + } + toggleContents.style.height = "0px"; + const positionToggle = () => { + // position the element (top left of parent, same width as parent) + if (!elRect) { + elRect = el.getBoundingClientRect(); + } + toggleContainer.style.left = `${elRect.left}px`; + toggleContainer.style.top = `${elRect.top}px`; + toggleContainer.style.width = `${elRect.width}px`; + }; + positionToggle(); + + toggleContainer.append(toggleContents); + el.parentElement.prepend(toggleContainer); + + // Process clicks + let tocShowing = false; + // Allow the caller to control whether this is dismissed + // when it is clicked (e.g. sidebar navigation supports + // opening and closing the nav tree, so don't dismiss on click) + const clickEl = placeholderDescriptor.dismissOnClick + ? toggleContainer + : toggleTitle; + + const closeToggle = () => { + if (tocShowing) { + toggleContainer.classList.remove("expanded"); + toggleContents.style.height = "0px"; + tocShowing = false; + } + }; + + // Get rid of any expanded toggle if the user scrolls + window.document.addEventListener( + "scroll", + throttle(() => { + closeToggle(); + }, 50) + ); + + // Handle positioning of the toggle + window.addEventListener( + "resize", + throttle(() => { + elRect = undefined; + positionToggle(); + }, 50) + ); + + window.addEventListener("quarto-hrChanged", () => { + elRect = undefined; + }); + + // Process the click + clickEl.onclick = () => { + if (!tocShowing) { + toggleContainer.classList.add("expanded"); + toggleContents.style.height = null; + tocShowing = true; + } else { + closeToggle(); + } + }; + }); + }; + + // Converts a sidebar from a menu back to a sidebar + const convertToSidebar = () => { + for (const child of el.children) { + child.style.opacity = 1; + child.style.overflow = null; + child.style.pointerEvents = null; + } + + const placeholderEl = window.document.getElementById( + placeholderDescriptor.id + ); + if (placeholderEl) { + placeholderEl.remove(); + } + + el.classList.remove("rollup"); + }; + + if (isReaderMode()) { + convertToMenu(); + isVisible = false; + } else { + // Find the top and bottom o the element that is being managed + const elTop = el.offsetTop; + const elBottom = + elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight; + + if (!isVisible) { + // If the element is current not visible reveal if there are + // no conflicts with overlay regions + if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) { + convertToSidebar(); + isVisible = true; + } + } else { + // If the element is visible, hide it if it conflicts with overlay regions + // and insert a placeholder toggle (or if we're in reader mode) + if (inHiddenRegion(elTop, elBottom, hiddenRegions)) { + convertToMenu(); + isVisible = false; + } + } + } + } + }; + }; + + const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]'); + for (const tabEl of tabEls) { + const id = tabEl.getAttribute("data-bs-target"); + if (id) { + const columnEl = document.querySelector( + `${id} .column-margin, .tabset-margin-content` + ); + if (columnEl) + tabEl.addEventListener("shown.bs.tab", function (event) { + const el = event.srcElement; + if (el) { + const visibleCls = `${el.id}-margin-content`; + // walk up until we find a parent tabset + let panelTabsetEl = el.parentElement; + while (panelTabsetEl) { + if (panelTabsetEl.classList.contains("panel-tabset")) { + break; + } + panelTabsetEl = panelTabsetEl.parentElement; + } + + if (panelTabsetEl) { + const prevSib = panelTabsetEl.previousElementSibling; + if ( + prevSib && + prevSib.classList.contains("tabset-margin-container") + ) { + const childNodes = prevSib.querySelectorAll( + ".tabset-margin-content" + ); + for (const childEl of childNodes) { + if (childEl.classList.contains(visibleCls)) { + childEl.classList.remove("collapse"); + } else { + childEl.classList.add("collapse"); + } + } + } + } + } + + layoutMarginEls(); + }); + } + } + + // Manage the visibility of the toc and the sidebar + const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, { + id: "quarto-toc-toggle", + titleSelector: "#toc-title", + dismissOnClick: true, + }); + const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, { + id: "quarto-sidebarnav-toggle", + titleSelector: ".title", + dismissOnClick: false, + }); + let tocLeftScrollVisibility; + if (leftTocEl) { + tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, { + id: "quarto-lefttoc-toggle", + titleSelector: "#toc-title", + dismissOnClick: true, + }); + } + + // Find the first element that uses formatting in special columns + const conflictingEls = window.document.body.querySelectorAll( + '[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]' + ); + + // Filter all the possibly conflicting elements into ones + // the do conflict on the left or ride side + const arrConflictingEls = Array.from(conflictingEls); + const leftSideConflictEls = arrConflictingEls.filter((el) => { + if (el.tagName === "ASIDE") { + return false; + } + return Array.from(el.classList).find((className) => { + return ( + className !== "column-body" && + className.startsWith("column-") && + !className.endsWith("right") && + !className.endsWith("container") && + className !== "column-margin" + ); + }); + }); + const rightSideConflictEls = arrConflictingEls.filter((el) => { + if (el.tagName === "ASIDE") { + return true; + } + + const hasMarginCaption = Array.from(el.classList).find((className) => { + return className == "margin-caption"; + }); + if (hasMarginCaption) { + return true; + } + + return Array.from(el.classList).find((className) => { + return ( + className !== "column-body" && + !className.endsWith("container") && + className.startsWith("column-") && + !className.endsWith("left") + ); + }); + }); + + const kOverlapPaddingSize = 10; + function toRegions(els) { + return els.map((el) => { + const boundRect = el.getBoundingClientRect(); + const top = + boundRect.top + + document.documentElement.scrollTop - + kOverlapPaddingSize; + return { + top, + bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize, + }; + }); + } + + let hasObserved = false; + const visibleItemObserver = (els) => { + let visibleElements = [...els]; + const intersectionObserver = new IntersectionObserver( + (entries, _observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (visibleElements.indexOf(entry.target) === -1) { + visibleElements.push(entry.target); + } + } else { + visibleElements = visibleElements.filter((visibleEntry) => { + return visibleEntry !== entry; + }); + } + }); + + if (!hasObserved) { + hideOverlappedSidebars(); + } + hasObserved = true; + }, + {} + ); + els.forEach((el) => { + intersectionObserver.observe(el); + }); + + return { + getVisibleEntries: () => { + return visibleElements; + }, + }; + }; + + const rightElementObserver = visibleItemObserver(rightSideConflictEls); + const leftElementObserver = visibleItemObserver(leftSideConflictEls); + + const hideOverlappedSidebars = () => { + marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries())); + sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries())); + if (tocLeftScrollVisibility) { + tocLeftScrollVisibility( + toRegions(leftElementObserver.getVisibleEntries()) + ); + } + }; + + window.quartoToggleReader = () => { + // Applies a slow class (or removes it) + // to update the transition speed + const slowTransition = (slow) => { + const manageTransition = (id, slow) => { + const el = document.getElementById(id); + if (el) { + if (slow) { + el.classList.add("slow"); + } else { + el.classList.remove("slow"); + } + } + }; + + manageTransition("TOC", slow); + manageTransition("quarto-sidebar", slow); + }; + const readerMode = !isReaderMode(); + setReaderModeValue(readerMode); + + // If we're entering reader mode, slow the transition + if (readerMode) { + slowTransition(readerMode); + } + highlightReaderToggle(readerMode); + hideOverlappedSidebars(); + + // If we're exiting reader mode, restore the non-slow transition + if (!readerMode) { + slowTransition(!readerMode); + } + }; + + const highlightReaderToggle = (readerMode) => { + const els = document.querySelectorAll(".quarto-reader-toggle"); + if (els) { + els.forEach((el) => { + if (readerMode) { + el.classList.add("reader"); + } else { + el.classList.remove("reader"); + } + }); + } + }; + + const setReaderModeValue = (val) => { + if (window.location.protocol !== "file:") { + window.localStorage.setItem("quarto-reader-mode", val); + } else { + localReaderMode = val; + } + }; + + const isReaderMode = () => { + if (window.location.protocol !== "file:") { + return window.localStorage.getItem("quarto-reader-mode") === "true"; + } else { + return localReaderMode; + } + }; + let localReaderMode = null; + + const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded"); + const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1; + + // Walk the TOC and collapse/expand nodes + // Nodes are expanded if: + // - they are top level + // - they have children that are 'active' links + // - they are directly below an link that is 'active' + const walk = (el, depth) => { + // Tick depth when we enter a UL + if (el.tagName === "UL") { + depth = depth + 1; + } + + // It this is active link + let isActiveNode = false; + if (el.tagName === "A" && el.classList.contains("active")) { + isActiveNode = true; + } + + // See if there is an active child to this element + let hasActiveChild = false; + for (child of el.children) { + hasActiveChild = walk(child, depth) || hasActiveChild; + } + + // Process the collapse state if this is an UL + if (el.tagName === "UL") { + if (tocOpenDepth === -1 && depth > 1) { + // toc-expand: false + el.classList.add("collapse"); + } else if ( + depth <= tocOpenDepth || + hasActiveChild || + prevSiblingIsActiveLink(el) + ) { + el.classList.remove("collapse"); + } else { + el.classList.add("collapse"); + } + + // untick depth when we leave a UL + depth = depth - 1; + } + return hasActiveChild || isActiveNode; + }; + + // walk the TOC and expand / collapse any items that should be shown + if (tocEl) { + updateActiveLink(); + walk(tocEl, 0); + } + + // Throttle the scroll event and walk peridiocally + window.document.addEventListener( + "scroll", + throttle(() => { + if (tocEl) { + updateActiveLink(); + walk(tocEl, 0); + } + if (!isReaderMode()) { + hideOverlappedSidebars(); + } + }, 5) + ); + window.addEventListener( + "resize", + throttle(() => { + if (tocEl) { + updateActiveLink(); + walk(tocEl, 0); + } + if (!isReaderMode()) { + hideOverlappedSidebars(); + } + }, 10) + ); + hideOverlappedSidebars(); + highlightReaderToggle(isReaderMode()); +}); + +// grouped tabsets +window.addEventListener("pageshow", (_event) => { + function getTabSettings() { + const data = localStorage.getItem("quarto-persistent-tabsets-data"); + if (!data) { + localStorage.setItem("quarto-persistent-tabsets-data", "{}"); + return {}; + } + if (data) { + return JSON.parse(data); + } + } + + function setTabSettings(data) { + localStorage.setItem( + "quarto-persistent-tabsets-data", + JSON.stringify(data) + ); + } + + function setTabState(groupName, groupValue) { + const data = getTabSettings(); + data[groupName] = groupValue; + setTabSettings(data); + } + + function toggleTab(tab, active) { + const tabPanelId = tab.getAttribute("aria-controls"); + const tabPanel = document.getElementById(tabPanelId); + if (active) { + tab.classList.add("active"); + tabPanel.classList.add("active"); + } else { + tab.classList.remove("active"); + tabPanel.classList.remove("active"); + } + } + + function toggleAll(selectedGroup, selectorsToSync) { + for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) { + const active = selectedGroup === thisGroup; + for (const tab of tabs) { + toggleTab(tab, active); + } + } + } + + function findSelectorsToSyncByLanguage() { + const result = {}; + const tabs = Array.from( + document.querySelectorAll(`div[data-group] a[id^='tabset-']`) + ); + for (const item of tabs) { + const div = item.parentElement.parentElement.parentElement; + const group = div.getAttribute("data-group"); + if (!result[group]) { + result[group] = {}; + } + const selectorsToSync = result[group]; + const value = item.innerHTML; + if (!selectorsToSync[value]) { + selectorsToSync[value] = []; + } + selectorsToSync[value].push(item); + } + return result; + } + + function setupSelectorSync() { + const selectorsToSync = findSelectorsToSyncByLanguage(); + Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => { + Object.entries(tabSetsByValue).forEach(([value, items]) => { + items.forEach((item) => { + item.addEventListener("click", (_event) => { + setTabState(group, value); + toggleAll(value, selectorsToSync[group]); + }); + }); + }); + }); + return selectorsToSync; + } + + const selectorsToSync = setupSelectorSync(); + for (const [group, selectedName] of Object.entries(getTabSettings())) { + const selectors = selectorsToSync[group]; + // it's possible that stale state gives us empty selections, so we explicitly check here. + if (selectors) { + toggleAll(selectedName, selectors); + } + } +}); + +function throttle(func, wait) { + let waiting = false; + return function () { + if (!waiting) { + func.apply(this, arguments); + waiting = true; + setTimeout(function () { + waiting = false; + }, wait); + } + }; +} + +function nexttick(func) { + return setTimeout(func, 0); +} diff --git a/docs/site_libs/quarto-html/tippy.css b/docs/site_libs/quarto-html/tippy.css new file mode 100644 index 0000000000000000000000000000000000000000..e6ae635cb1f82b176c18afa80dfa029c7a536e70 --- /dev/null +++ b/docs/site_libs/quarto-html/tippy.css @@ -0,0 +1 @@ +.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} \ No newline at end of file diff --git a/docs/site_libs/quarto-html/tippy.umd.min.js b/docs/site_libs/quarto-html/tippy.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..ca292be32b252f9a40e231f3a3e696b2506f7c96 --- /dev/null +++ b/docs/site_libs/quarto-html/tippy.umd.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],t):(e=e||self).tippy=t(e.Popper)}(this,(function(e){"use strict";var t={passive:!0,capture:!0},n=function(){return document.body};function r(e,t,n){if(Array.isArray(e)){var r=e[t];return null==r?Array.isArray(n)?n[t]:n:r}return e}function o(e,t){var n={}.toString.call(e);return 0===n.indexOf("[object")&&n.indexOf(t+"]")>-1}function i(e,t){return"function"==typeof e?e.apply(void 0,t):e}function a(e,t){return 0===t?e:function(r){clearTimeout(n),n=setTimeout((function(){e(r)}),t)};var n}function s(e,t){var n=Object.assign({},e);return t.forEach((function(e){delete n[e]})),n}function u(e){return[].concat(e)}function c(e,t){-1===e.indexOf(t)&&e.push(t)}function p(e){return e.split("-")[0]}function f(e){return[].slice.call(e)}function l(e){return Object.keys(e).reduce((function(t,n){return void 0!==e[n]&&(t[n]=e[n]),t}),{})}function d(){return document.createElement("div")}function v(e){return["Element","Fragment"].some((function(t){return o(e,t)}))}function m(e){return o(e,"MouseEvent")}function g(e){return!(!e||!e._tippy||e._tippy.reference!==e)}function h(e){return v(e)?[e]:function(e){return o(e,"NodeList")}(e)?f(e):Array.isArray(e)?e:f(document.querySelectorAll(e))}function b(e,t){e.forEach((function(e){e&&(e.style.transitionDuration=t+"ms")}))}function y(e,t){e.forEach((function(e){e&&e.setAttribute("data-state",t)}))}function w(e){var t,n=u(e)[0];return null!=n&&null!=(t=n.ownerDocument)&&t.body?n.ownerDocument:document}function E(e,t,n){var r=t+"EventListener";["transitionend","webkitTransitionEnd"].forEach((function(t){e[r](t,n)}))}function O(e,t){for(var n=t;n;){var r;if(e.contains(n))return!0;n=null==n.getRootNode||null==(r=n.getRootNode())?void 0:r.host}return!1}var x={isTouch:!1},C=0;function T(){x.isTouch||(x.isTouch=!0,window.performance&&document.addEventListener("mousemove",A))}function A(){var e=performance.now();e-C<20&&(x.isTouch=!1,document.removeEventListener("mousemove",A)),C=e}function L(){var e=document.activeElement;if(g(e)){var t=e._tippy;e.blur&&!t.state.isVisible&&e.blur()}}var D=!!("undefined"!=typeof window&&"undefined"!=typeof document)&&!!window.msCrypto,R=Object.assign({appendTo:n,aria:{content:"auto",expanded:"auto"},delay:0,duration:[300,250],getReferenceClientRect:null,hideOnClick:!0,ignoreAttributes:!1,interactive:!1,interactiveBorder:2,interactiveDebounce:0,moveTransition:"",offset:[0,10],onAfterUpdate:function(){},onBeforeUpdate:function(){},onCreate:function(){},onDestroy:function(){},onHidden:function(){},onHide:function(){},onMount:function(){},onShow:function(){},onShown:function(){},onTrigger:function(){},onUntrigger:function(){},onClickOutside:function(){},placement:"top",plugins:[],popperOptions:{},render:null,showOnCreate:!1,touch:!0,trigger:"mouseenter focus",triggerTarget:null},{animateFill:!1,followCursor:!1,inlinePositioning:!1,sticky:!1},{allowHTML:!1,animation:"fade",arrow:!0,content:"",inertia:!1,maxWidth:350,role:"tooltip",theme:"",zIndex:9999}),k=Object.keys(R);function P(e){var t=(e.plugins||[]).reduce((function(t,n){var r,o=n.name,i=n.defaultValue;o&&(t[o]=void 0!==e[o]?e[o]:null!=(r=R[o])?r:i);return t}),{});return Object.assign({},e,t)}function j(e,t){var n=Object.assign({},t,{content:i(t.content,[e])},t.ignoreAttributes?{}:function(e,t){return(t?Object.keys(P(Object.assign({},R,{plugins:t}))):k).reduce((function(t,n){var r=(e.getAttribute("data-tippy-"+n)||"").trim();if(!r)return t;if("content"===n)t[n]=r;else try{t[n]=JSON.parse(r)}catch(e){t[n]=r}return t}),{})}(e,t.plugins));return n.aria=Object.assign({},R.aria,n.aria),n.aria={expanded:"auto"===n.aria.expanded?t.interactive:n.aria.expanded,content:"auto"===n.aria.content?t.interactive?null:"describedby":n.aria.content},n}function M(e,t){e.innerHTML=t}function V(e){var t=d();return!0===e?t.className="tippy-arrow":(t.className="tippy-svg-arrow",v(e)?t.appendChild(e):M(t,e)),t}function I(e,t){v(t.content)?(M(e,""),e.appendChild(t.content)):"function"!=typeof t.content&&(t.allowHTML?M(e,t.content):e.textContent=t.content)}function S(e){var t=e.firstElementChild,n=f(t.children);return{box:t,content:n.find((function(e){return e.classList.contains("tippy-content")})),arrow:n.find((function(e){return e.classList.contains("tippy-arrow")||e.classList.contains("tippy-svg-arrow")})),backdrop:n.find((function(e){return e.classList.contains("tippy-backdrop")}))}}function N(e){var t=d(),n=d();n.className="tippy-box",n.setAttribute("data-state","hidden"),n.setAttribute("tabindex","-1");var r=d();function o(n,r){var o=S(t),i=o.box,a=o.content,s=o.arrow;r.theme?i.setAttribute("data-theme",r.theme):i.removeAttribute("data-theme"),"string"==typeof r.animation?i.setAttribute("data-animation",r.animation):i.removeAttribute("data-animation"),r.inertia?i.setAttribute("data-inertia",""):i.removeAttribute("data-inertia"),i.style.maxWidth="number"==typeof r.maxWidth?r.maxWidth+"px":r.maxWidth,r.role?i.setAttribute("role",r.role):i.removeAttribute("role"),n.content===r.content&&n.allowHTML===r.allowHTML||I(a,e.props),r.arrow?s?n.arrow!==r.arrow&&(i.removeChild(s),i.appendChild(V(r.arrow))):i.appendChild(V(r.arrow)):s&&i.removeChild(s)}return r.className="tippy-content",r.setAttribute("data-state","hidden"),I(r,e.props),t.appendChild(n),n.appendChild(r),o(e.props,e.props),{popper:t,onUpdate:o}}N.$$tippy=!0;var B=1,H=[],U=[];function _(o,s){var v,g,h,C,T,A,L,k,M=j(o,Object.assign({},R,P(l(s)))),V=!1,I=!1,N=!1,_=!1,F=[],W=a(we,M.interactiveDebounce),X=B++,Y=(k=M.plugins).filter((function(e,t){return k.indexOf(e)===t})),$={id:X,reference:o,popper:d(),popperInstance:null,props:M,state:{isEnabled:!0,isVisible:!1,isDestroyed:!1,isMounted:!1,isShown:!1},plugins:Y,clearDelayTimeouts:function(){clearTimeout(v),clearTimeout(g),cancelAnimationFrame(h)},setProps:function(e){if($.state.isDestroyed)return;ae("onBeforeUpdate",[$,e]),be();var t=$.props,n=j(o,Object.assign({},t,l(e),{ignoreAttributes:!0}));$.props=n,he(),t.interactiveDebounce!==n.interactiveDebounce&&(ce(),W=a(we,n.interactiveDebounce));t.triggerTarget&&!n.triggerTarget?u(t.triggerTarget).forEach((function(e){e.removeAttribute("aria-expanded")})):n.triggerTarget&&o.removeAttribute("aria-expanded");ue(),ie(),J&&J(t,n);$.popperInstance&&(Ce(),Ae().forEach((function(e){requestAnimationFrame(e._tippy.popperInstance.forceUpdate)})));ae("onAfterUpdate",[$,e])},setContent:function(e){$.setProps({content:e})},show:function(){var e=$.state.isVisible,t=$.state.isDestroyed,o=!$.state.isEnabled,a=x.isTouch&&!$.props.touch,s=r($.props.duration,0,R.duration);if(e||t||o||a)return;if(te().hasAttribute("disabled"))return;if(ae("onShow",[$],!1),!1===$.props.onShow($))return;$.state.isVisible=!0,ee()&&(z.style.visibility="visible");ie(),de(),$.state.isMounted||(z.style.transition="none");if(ee()){var u=re(),p=u.box,f=u.content;b([p,f],0)}A=function(){var e;if($.state.isVisible&&!_){if(_=!0,z.offsetHeight,z.style.transition=$.props.moveTransition,ee()&&$.props.animation){var t=re(),n=t.box,r=t.content;b([n,r],s),y([n,r],"visible")}se(),ue(),c(U,$),null==(e=$.popperInstance)||e.forceUpdate(),ae("onMount",[$]),$.props.animation&&ee()&&function(e,t){me(e,t)}(s,(function(){$.state.isShown=!0,ae("onShown",[$])}))}},function(){var e,t=$.props.appendTo,r=te();e=$.props.interactive&&t===n||"parent"===t?r.parentNode:i(t,[r]);e.contains(z)||e.appendChild(z);$.state.isMounted=!0,Ce()}()},hide:function(){var e=!$.state.isVisible,t=$.state.isDestroyed,n=!$.state.isEnabled,o=r($.props.duration,1,R.duration);if(e||t||n)return;if(ae("onHide",[$],!1),!1===$.props.onHide($))return;$.state.isVisible=!1,$.state.isShown=!1,_=!1,V=!1,ee()&&(z.style.visibility="hidden");if(ce(),ve(),ie(!0),ee()){var i=re(),a=i.box,s=i.content;$.props.animation&&(b([a,s],o),y([a,s],"hidden"))}se(),ue(),$.props.animation?ee()&&function(e,t){me(e,(function(){!$.state.isVisible&&z.parentNode&&z.parentNode.contains(z)&&t()}))}(o,$.unmount):$.unmount()},hideWithInteractivity:function(e){ne().addEventListener("mousemove",W),c(H,W),W(e)},enable:function(){$.state.isEnabled=!0},disable:function(){$.hide(),$.state.isEnabled=!1},unmount:function(){$.state.isVisible&&$.hide();if(!$.state.isMounted)return;Te(),Ae().forEach((function(e){e._tippy.unmount()})),z.parentNode&&z.parentNode.removeChild(z);U=U.filter((function(e){return e!==$})),$.state.isMounted=!1,ae("onHidden",[$])},destroy:function(){if($.state.isDestroyed)return;$.clearDelayTimeouts(),$.unmount(),be(),delete o._tippy,$.state.isDestroyed=!0,ae("onDestroy",[$])}};if(!M.render)return $;var q=M.render($),z=q.popper,J=q.onUpdate;z.setAttribute("data-tippy-root",""),z.id="tippy-"+$.id,$.popper=z,o._tippy=$,z._tippy=$;var G=Y.map((function(e){return e.fn($)})),K=o.hasAttribute("aria-expanded");return he(),ue(),ie(),ae("onCreate",[$]),M.showOnCreate&&Le(),z.addEventListener("mouseenter",(function(){$.props.interactive&&$.state.isVisible&&$.clearDelayTimeouts()})),z.addEventListener("mouseleave",(function(){$.props.interactive&&$.props.trigger.indexOf("mouseenter")>=0&&ne().addEventListener("mousemove",W)})),$;function Q(){var e=$.props.touch;return Array.isArray(e)?e:[e,0]}function Z(){return"hold"===Q()[0]}function ee(){var e;return!(null==(e=$.props.render)||!e.$$tippy)}function te(){return L||o}function ne(){var e=te().parentNode;return e?w(e):document}function re(){return S(z)}function oe(e){return $.state.isMounted&&!$.state.isVisible||x.isTouch||C&&"focus"===C.type?0:r($.props.delay,e?0:1,R.delay)}function ie(e){void 0===e&&(e=!1),z.style.pointerEvents=$.props.interactive&&!e?"":"none",z.style.zIndex=""+$.props.zIndex}function ae(e,t,n){var r;(void 0===n&&(n=!0),G.forEach((function(n){n[e]&&n[e].apply(n,t)})),n)&&(r=$.props)[e].apply(r,t)}function se(){var e=$.props.aria;if(e.content){var t="aria-"+e.content,n=z.id;u($.props.triggerTarget||o).forEach((function(e){var r=e.getAttribute(t);if($.state.isVisible)e.setAttribute(t,r?r+" "+n:n);else{var o=r&&r.replace(n,"").trim();o?e.setAttribute(t,o):e.removeAttribute(t)}}))}}function ue(){!K&&$.props.aria.expanded&&u($.props.triggerTarget||o).forEach((function(e){$.props.interactive?e.setAttribute("aria-expanded",$.state.isVisible&&e===te()?"true":"false"):e.removeAttribute("aria-expanded")}))}function ce(){ne().removeEventListener("mousemove",W),H=H.filter((function(e){return e!==W}))}function pe(e){if(!x.isTouch||!N&&"mousedown"!==e.type){var t=e.composedPath&&e.composedPath()[0]||e.target;if(!$.props.interactive||!O(z,t)){if(u($.props.triggerTarget||o).some((function(e){return O(e,t)}))){if(x.isTouch)return;if($.state.isVisible&&$.props.trigger.indexOf("click")>=0)return}else ae("onClickOutside",[$,e]);!0===$.props.hideOnClick&&($.clearDelayTimeouts(),$.hide(),I=!0,setTimeout((function(){I=!1})),$.state.isMounted||ve())}}}function fe(){N=!0}function le(){N=!1}function de(){var e=ne();e.addEventListener("mousedown",pe,!0),e.addEventListener("touchend",pe,t),e.addEventListener("touchstart",le,t),e.addEventListener("touchmove",fe,t)}function ve(){var e=ne();e.removeEventListener("mousedown",pe,!0),e.removeEventListener("touchend",pe,t),e.removeEventListener("touchstart",le,t),e.removeEventListener("touchmove",fe,t)}function me(e,t){var n=re().box;function r(e){e.target===n&&(E(n,"remove",r),t())}if(0===e)return t();E(n,"remove",T),E(n,"add",r),T=r}function ge(e,t,n){void 0===n&&(n=!1),u($.props.triggerTarget||o).forEach((function(r){r.addEventListener(e,t,n),F.push({node:r,eventType:e,handler:t,options:n})}))}function he(){var e;Z()&&(ge("touchstart",ye,{passive:!0}),ge("touchend",Ee,{passive:!0})),(e=$.props.trigger,e.split(/\s+/).filter(Boolean)).forEach((function(e){if("manual"!==e)switch(ge(e,ye),e){case"mouseenter":ge("mouseleave",Ee);break;case"focus":ge(D?"focusout":"blur",Oe);break;case"focusin":ge("focusout",Oe)}}))}function be(){F.forEach((function(e){var t=e.node,n=e.eventType,r=e.handler,o=e.options;t.removeEventListener(n,r,o)})),F=[]}function ye(e){var t,n=!1;if($.state.isEnabled&&!xe(e)&&!I){var r="focus"===(null==(t=C)?void 0:t.type);C=e,L=e.currentTarget,ue(),!$.state.isVisible&&m(e)&&H.forEach((function(t){return t(e)})),"click"===e.type&&($.props.trigger.indexOf("mouseenter")<0||V)&&!1!==$.props.hideOnClick&&$.state.isVisible?n=!0:Le(e),"click"===e.type&&(V=!n),n&&!r&&De(e)}}function we(e){var t=e.target,n=te().contains(t)||z.contains(t);"mousemove"===e.type&&n||function(e,t){var n=t.clientX,r=t.clientY;return e.every((function(e){var t=e.popperRect,o=e.popperState,i=e.props.interactiveBorder,a=p(o.placement),s=o.modifiersData.offset;if(!s)return!0;var u="bottom"===a?s.top.y:0,c="top"===a?s.bottom.y:0,f="right"===a?s.left.x:0,l="left"===a?s.right.x:0,d=t.top-r+u>i,v=r-t.bottom-c>i,m=t.left-n+f>i,g=n-t.right-l>i;return d||v||m||g}))}(Ae().concat(z).map((function(e){var t,n=null==(t=e._tippy.popperInstance)?void 0:t.state;return n?{popperRect:e.getBoundingClientRect(),popperState:n,props:M}:null})).filter(Boolean),e)&&(ce(),De(e))}function Ee(e){xe(e)||$.props.trigger.indexOf("click")>=0&&V||($.props.interactive?$.hideWithInteractivity(e):De(e))}function Oe(e){$.props.trigger.indexOf("focusin")<0&&e.target!==te()||$.props.interactive&&e.relatedTarget&&z.contains(e.relatedTarget)||De(e)}function xe(e){return!!x.isTouch&&Z()!==e.type.indexOf("touch")>=0}function Ce(){Te();var t=$.props,n=t.popperOptions,r=t.placement,i=t.offset,a=t.getReferenceClientRect,s=t.moveTransition,u=ee()?S(z).arrow:null,c=a?{getBoundingClientRect:a,contextElement:a.contextElement||te()}:o,p=[{name:"offset",options:{offset:i}},{name:"preventOverflow",options:{padding:{top:2,bottom:2,left:5,right:5}}},{name:"flip",options:{padding:5}},{name:"computeStyles",options:{adaptive:!s}},{name:"$$tippy",enabled:!0,phase:"beforeWrite",requires:["computeStyles"],fn:function(e){var t=e.state;if(ee()){var n=re().box;["placement","reference-hidden","escaped"].forEach((function(e){"placement"===e?n.setAttribute("data-placement",t.placement):t.attributes.popper["data-popper-"+e]?n.setAttribute("data-"+e,""):n.removeAttribute("data-"+e)})),t.attributes.popper={}}}}];ee()&&u&&p.push({name:"arrow",options:{element:u,padding:3}}),p.push.apply(p,(null==n?void 0:n.modifiers)||[]),$.popperInstance=e.createPopper(c,z,Object.assign({},n,{placement:r,onFirstUpdate:A,modifiers:p}))}function Te(){$.popperInstance&&($.popperInstance.destroy(),$.popperInstance=null)}function Ae(){return f(z.querySelectorAll("[data-tippy-root]"))}function Le(e){$.clearDelayTimeouts(),e&&ae("onTrigger",[$,e]),de();var t=oe(!0),n=Q(),r=n[0],o=n[1];x.isTouch&&"hold"===r&&o&&(t=o),t?v=setTimeout((function(){$.show()}),t):$.show()}function De(e){if($.clearDelayTimeouts(),ae("onUntrigger",[$,e]),$.state.isVisible){if(!($.props.trigger.indexOf("mouseenter")>=0&&$.props.trigger.indexOf("click")>=0&&["mouseleave","mousemove"].indexOf(e.type)>=0&&V)){var t=oe(!1);t?g=setTimeout((function(){$.state.isVisible&&$.hide()}),t):h=requestAnimationFrame((function(){$.hide()}))}}else ve()}}function F(e,n){void 0===n&&(n={});var r=R.plugins.concat(n.plugins||[]);document.addEventListener("touchstart",T,t),window.addEventListener("blur",L);var o=Object.assign({},n,{plugins:r}),i=h(e).reduce((function(e,t){var n=t&&_(t,o);return n&&e.push(n),e}),[]);return v(e)?i[0]:i}F.defaultProps=R,F.setDefaultProps=function(e){Object.keys(e).forEach((function(t){R[t]=e[t]}))},F.currentInput=x;var W=Object.assign({},e.applyStyles,{effect:function(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow)}}),X={mouseover:"mouseenter",focusin:"focus",click:"click"};var Y={name:"animateFill",defaultValue:!1,fn:function(e){var t;if(null==(t=e.props.render)||!t.$$tippy)return{};var n=S(e.popper),r=n.box,o=n.content,i=e.props.animateFill?function(){var e=d();return e.className="tippy-backdrop",y([e],"hidden"),e}():null;return{onCreate:function(){i&&(r.insertBefore(i,r.firstElementChild),r.setAttribute("data-animatefill",""),r.style.overflow="hidden",e.setProps({arrow:!1,animation:"shift-away"}))},onMount:function(){if(i){var e=r.style.transitionDuration,t=Number(e.replace("ms",""));o.style.transitionDelay=Math.round(t/10)+"ms",i.style.transitionDuration=e,y([i],"visible")}},onShow:function(){i&&(i.style.transitionDuration="0ms")},onHide:function(){i&&y([i],"hidden")}}}};var $={clientX:0,clientY:0},q=[];function z(e){var t=e.clientX,n=e.clientY;$={clientX:t,clientY:n}}var J={name:"followCursor",defaultValue:!1,fn:function(e){var t=e.reference,n=w(e.props.triggerTarget||t),r=!1,o=!1,i=!0,a=e.props;function s(){return"initial"===e.props.followCursor&&e.state.isVisible}function u(){n.addEventListener("mousemove",f)}function c(){n.removeEventListener("mousemove",f)}function p(){r=!0,e.setProps({getReferenceClientRect:null}),r=!1}function f(n){var r=!n.target||t.contains(n.target),o=e.props.followCursor,i=n.clientX,a=n.clientY,s=t.getBoundingClientRect(),u=i-s.left,c=a-s.top;!r&&e.props.interactive||e.setProps({getReferenceClientRect:function(){var e=t.getBoundingClientRect(),n=i,r=a;"initial"===o&&(n=e.left+u,r=e.top+c);var s="horizontal"===o?e.top:r,p="vertical"===o?e.right:n,f="horizontal"===o?e.bottom:r,l="vertical"===o?e.left:n;return{width:p-l,height:f-s,top:s,right:p,bottom:f,left:l}}})}function l(){e.props.followCursor&&(q.push({instance:e,doc:n}),function(e){e.addEventListener("mousemove",z)}(n))}function d(){0===(q=q.filter((function(t){return t.instance!==e}))).filter((function(e){return e.doc===n})).length&&function(e){e.removeEventListener("mousemove",z)}(n)}return{onCreate:l,onDestroy:d,onBeforeUpdate:function(){a=e.props},onAfterUpdate:function(t,n){var i=n.followCursor;r||void 0!==i&&a.followCursor!==i&&(d(),i?(l(),!e.state.isMounted||o||s()||u()):(c(),p()))},onMount:function(){e.props.followCursor&&!o&&(i&&(f($),i=!1),s()||u())},onTrigger:function(e,t){m(t)&&($={clientX:t.clientX,clientY:t.clientY}),o="focus"===t.type},onHidden:function(){e.props.followCursor&&(p(),c(),i=!0)}}}};var G={name:"inlinePositioning",defaultValue:!1,fn:function(e){var t,n=e.reference;var r=-1,o=!1,i=[],a={name:"tippyInlinePositioning",enabled:!0,phase:"afterWrite",fn:function(o){var a=o.state;e.props.inlinePositioning&&(-1!==i.indexOf(a.placement)&&(i=[]),t!==a.placement&&-1===i.indexOf(a.placement)&&(i.push(a.placement),e.setProps({getReferenceClientRect:function(){return function(e){return function(e,t,n,r){if(n.length<2||null===e)return t;if(2===n.length&&r>=0&&n[0].left>n[1].right)return n[r]||t;switch(e){case"top":case"bottom":var o=n[0],i=n[n.length-1],a="top"===e,s=o.top,u=i.bottom,c=a?o.left:i.left,p=a?o.right:i.right;return{top:s,bottom:u,left:c,right:p,width:p-c,height:u-s};case"left":case"right":var f=Math.min.apply(Math,n.map((function(e){return e.left}))),l=Math.max.apply(Math,n.map((function(e){return e.right}))),d=n.filter((function(t){return"left"===e?t.left===f:t.right===l})),v=d[0].top,m=d[d.length-1].bottom;return{top:v,bottom:m,left:f,right:l,width:l-f,height:m-v};default:return t}}(p(e),n.getBoundingClientRect(),f(n.getClientRects()),r)}(a.placement)}})),t=a.placement)}};function s(){var t;o||(t=function(e,t){var n;return{popperOptions:Object.assign({},e.popperOptions,{modifiers:[].concat(((null==(n=e.popperOptions)?void 0:n.modifiers)||[]).filter((function(e){return e.name!==t.name})),[t])})}}(e.props,a),o=!0,e.setProps(t),o=!1)}return{onCreate:s,onAfterUpdate:s,onTrigger:function(t,n){if(m(n)){var o=f(e.reference.getClientRects()),i=o.find((function(e){return e.left-2<=n.clientX&&e.right+2>=n.clientX&&e.top-2<=n.clientY&&e.bottom+2>=n.clientY})),a=o.indexOf(i);r=a>-1?a:r}},onHidden:function(){r=-1}}}};var K={name:"sticky",defaultValue:!1,fn:function(e){var t=e.reference,n=e.popper;function r(t){return!0===e.props.sticky||e.props.sticky===t}var o=null,i=null;function a(){var s=r("reference")?(e.popperInstance?e.popperInstance.state.elements.reference:t).getBoundingClientRect():null,u=r("popper")?n.getBoundingClientRect():null;(s&&Q(o,s)||u&&Q(i,u))&&e.popperInstance&&e.popperInstance.update(),o=s,i=u,e.state.isMounted&&requestAnimationFrame(a)}return{onMount:function(){e.props.sticky&&a()}}}};function Q(e,t){return!e||!t||(e.top!==t.top||e.right!==t.right||e.bottom!==t.bottom||e.left!==t.left)}return F.setDefaultProps({plugins:[Y,J,G,K],render:N}),F.createSingleton=function(e,t){var n;void 0===t&&(t={});var r,o=e,i=[],a=[],c=t.overrides,p=[],f=!1;function l(){a=o.map((function(e){return u(e.props.triggerTarget||e.reference)})).reduce((function(e,t){return e.concat(t)}),[])}function v(){i=o.map((function(e){return e.reference}))}function m(e){o.forEach((function(t){e?t.enable():t.disable()}))}function g(e){return o.map((function(t){var n=t.setProps;return t.setProps=function(o){n(o),t.reference===r&&e.setProps(o)},function(){t.setProps=n}}))}function h(e,t){var n=a.indexOf(t);if(t!==r){r=t;var s=(c||[]).concat("content").reduce((function(e,t){return e[t]=o[n].props[t],e}),{});e.setProps(Object.assign({},s,{getReferenceClientRect:"function"==typeof s.getReferenceClientRect?s.getReferenceClientRect:function(){var e;return null==(e=i[n])?void 0:e.getBoundingClientRect()}}))}}m(!1),v(),l();var b={fn:function(){return{onDestroy:function(){m(!0)},onHidden:function(){r=null},onClickOutside:function(e){e.props.showOnCreate&&!f&&(f=!0,r=null)},onShow:function(e){e.props.showOnCreate&&!f&&(f=!0,h(e,i[0]))},onTrigger:function(e,t){h(e,t.currentTarget)}}}},y=F(d(),Object.assign({},s(t,["overrides"]),{plugins:[b].concat(t.plugins||[]),triggerTarget:a,popperOptions:Object.assign({},t.popperOptions,{modifiers:[].concat((null==(n=t.popperOptions)?void 0:n.modifiers)||[],[W])})})),w=y.show;y.show=function(e){if(w(),!r&&null==e)return h(y,i[0]);if(!r||null!=e){if("number"==typeof e)return i[e]&&h(y,i[e]);if(o.indexOf(e)>=0){var t=e.reference;return h(y,t)}return i.indexOf(e)>=0?h(y,e):void 0}},y.showNext=function(){var e=i[0];if(!r)return y.show(0);var t=i.indexOf(r);y.show(i[t+1]||e)},y.showPrevious=function(){var e=i[i.length-1];if(!r)return y.show(e);var t=i.indexOf(r),n=i[t-1]||e;y.show(n)};var E=y.setProps;return y.setProps=function(e){c=e.overrides||c,E(e)},y.setInstances=function(e){m(!0),p.forEach((function(e){return e()})),o=e,m(!1),v(),l(),p=g(y),y.setProps({triggerTarget:a})},p=g(y),y},F.delegate=function(e,n){var r=[],o=[],i=!1,a=n.target,c=s(n,["target"]),p=Object.assign({},c,{trigger:"manual",touch:!1}),f=Object.assign({touch:R.touch},c,{showOnCreate:!0}),l=F(e,p);function d(e){if(e.target&&!i){var t=e.target.closest(a);if(t){var r=t.getAttribute("data-tippy-trigger")||n.trigger||R.trigger;if(!t._tippy&&!("touchstart"===e.type&&"boolean"==typeof f.touch||"touchstart"!==e.type&&r.indexOf(X[e.type])<0)){var s=F(t,f);s&&(o=o.concat(s))}}}}function v(e,t,n,o){void 0===o&&(o=!1),e.addEventListener(t,n,o),r.push({node:e,eventType:t,handler:n,options:o})}return u(l).forEach((function(e){var n=e.destroy,a=e.enable,s=e.disable;e.destroy=function(e){void 0===e&&(e=!0),e&&o.forEach((function(e){e.destroy()})),o=[],r.forEach((function(e){var t=e.node,n=e.eventType,r=e.handler,o=e.options;t.removeEventListener(n,r,o)})),r=[],n()},e.enable=function(){a(),o.forEach((function(e){return e.enable()})),i=!1},e.disable=function(){s(),o.forEach((function(e){return e.disable()})),i=!0},function(e){var n=e.reference;v(n,"touchstart",d,t),v(n,"mouseover",d),v(n,"focusin",d),v(n,"click",d)}(e)})),l},F.hideAll=function(e){var t=void 0===e?{}:e,n=t.exclude,r=t.duration;U.forEach((function(e){var t=!1;if(n&&(t=g(n)?e.reference===n:e.popper===n.popper),!t){var o=e.props.duration;e.setProps({duration:r}),e.hide(),e.state.isDestroyed||e.setProps({duration:o})}}))},F.roundArrow='',F})); + diff --git a/docs/site_libs/quarto-listing/list.min.js b/docs/site_libs/quarto-listing/list.min.js new file mode 100644 index 0000000000000000000000000000000000000000..43dfd15a15f562d5847649d79aadd40b89d82877 --- /dev/null +++ b/docs/site_libs/quarto-listing/list.min.js @@ -0,0 +1,2 @@ +var List;List=function(){var t={"./src/add-async.js":function(t){t.exports=function(t){return function e(r,n,s){var i=r.splice(0,50);s=(s=s||[]).concat(t.add(i)),r.length>0?setTimeout((function(){e(r,n,s)}),1):(t.update(),n(s))}}},"./src/filter.js":function(t){t.exports=function(t){return t.handlers.filterStart=t.handlers.filterStart||[],t.handlers.filterComplete=t.handlers.filterComplete||[],function(e){if(t.trigger("filterStart"),t.i=1,t.reset.filter(),void 0===e)t.filtered=!1;else{t.filtered=!0;for(var r=t.items,n=0,s=r.length;nv.page,a=new g(t[s],void 0,n),v.items.push(a),r.push(a)}return v.update(),r}m(t.slice(0),e)}},this.show=function(t,e){return this.i=t,this.page=e,v.update(),v},this.remove=function(t,e,r){for(var n=0,s=0,i=v.items.length;s-1&&r.splice(n,1),v},this.trigger=function(t){for(var e=v.handlers[t].length;e--;)v.handlers[t][e](v);return v},this.reset={filter:function(){for(var t=v.items,e=t.length;e--;)t[e].filtered=!1;return v},search:function(){for(var t=v.items,e=t.length;e--;)t[e].found=!1;return v}},this.update=function(){var t=v.items,e=t.length;v.visibleItems=[],v.matchingItems=[],v.templater.clear();for(var r=0;r=v.i&&v.visibleItems.lengthe},innerWindow:function(t,e,r){return t>=e-r&&t<=e+r},dotted:function(t,e,r,n,s,i,a){return this.dottedLeft(t,e,r,n,s,i)||this.dottedRight(t,e,r,n,s,i,a)},dottedLeft:function(t,e,r,n,s,i){return e==r+1&&!this.innerWindow(e,s,i)&&!this.right(e,n)},dottedRight:function(t,e,r,n,s,i,a){return!t.items[a-1].values().dotted&&(e==n&&!this.innerWindow(e,s,i)&&!this.right(e,n))}};return function(e){var n=new i(t.listContainer.id,{listClass:e.paginationClass||"pagination",item:e.item||"
  • ",valueNames:["page","dotted"],searchClass:"pagination-search-that-is-not-supposed-to-exist",sortClass:"pagination-sort-that-is-not-supposed-to-exist"});s.bind(n.listContainer,"click",(function(e){var r=e.target||e.srcElement,n=t.utils.getAttribute(r,"data-page"),s=t.utils.getAttribute(r,"data-i");s&&t.show((s-1)*n+1,n)})),t.on("updated",(function(){r(n,e)})),r(n,e)}}},"./src/parse.js":function(t,e,r){t.exports=function(t){var e=r("./src/item.js")(t),n=function(r,n){for(var s=0,i=r.length;s0?setTimeout((function(){e(r,s)}),1):(t.update(),t.trigger("parseComplete"))};return t.handlers.parseComplete=t.handlers.parseComplete||[],function(){var e=function(t){for(var e=t.childNodes,r=[],n=0,s=e.length;n]/g.exec(t)){var e=document.createElement("tbody");return e.innerHTML=t,e.firstElementChild}if(-1!==t.indexOf("<")){var r=document.createElement("div");return r.innerHTML=t,r.firstElementChild}}},a=function(e,r,n){var s=void 0,i=function(e){for(var r=0,n=t.valueNames.length;r=1;)t.list.removeChild(t.list.firstChild)},function(){var r;if("function"!=typeof t.item){if(!(r="string"==typeof t.item?-1===t.item.indexOf("<")?document.getElementById(t.item):i(t.item):s()))throw new Error("The list needs to have at least one item on init otherwise you'll have to add a template.");r=n(r,t.valueNames),e=function(){return r.cloneNode(!0)}}else e=function(e){var r=t.item(e);return i(r)}}()};t.exports=function(t){return new e(t)}},"./src/utils/classes.js":function(t,e,r){var n=r("./src/utils/index-of.js"),s=/\s+/;Object.prototype.toString;function i(t){if(!t||!t.nodeType)throw new Error("A DOM element reference is required");this.el=t,this.list=t.classList}t.exports=function(t){return new i(t)},i.prototype.add=function(t){if(this.list)return this.list.add(t),this;var e=this.array();return~n(e,t)||e.push(t),this.el.className=e.join(" "),this},i.prototype.remove=function(t){if(this.list)return this.list.remove(t),this;var e=this.array(),r=n(e,t);return~r&&e.splice(r,1),this.el.className=e.join(" "),this},i.prototype.toggle=function(t,e){return this.list?(void 0!==e?e!==this.list.toggle(t,e)&&this.list.toggle(t):this.list.toggle(t),this):(void 0!==e?e?this.add(t):this.remove(t):this.has(t)?this.remove(t):this.add(t),this)},i.prototype.array=function(){var t=(this.el.getAttribute("class")||"").replace(/^\s+|\s+$/g,"").split(s);return""===t[0]&&t.shift(),t},i.prototype.has=i.prototype.contains=function(t){return this.list?this.list.contains(t):!!~n(this.array(),t)}},"./src/utils/events.js":function(t,e,r){var n=window.addEventListener?"addEventListener":"attachEvent",s=window.removeEventListener?"removeEventListener":"detachEvent",i="addEventListener"!==n?"on":"",a=r("./src/utils/to-array.js");e.bind=function(t,e,r,s){for(var o=0,l=(t=a(t)).length;o32)return!1;var a=n,o=function(){var t,r={};for(t=0;t=p;b--){var j=o[t.charAt(b-1)];if(C[b]=0===m?(C[b+1]<<1|1)&j:(C[b+1]<<1|1)&j|(v[b+1]|v[b])<<1|1|v[b+1],C[b]&d){var x=l(m,b-1);if(x<=u){if(u=x,!((c=b-1)>a))break;p=Math.max(1,2*a-c)}}}if(l(m+1,a)>u)break;v=C}return!(c<0)}},"./src/utils/get-attribute.js":function(t){t.exports=function(t,e){var r=t.getAttribute&&t.getAttribute(e)||null;if(!r)for(var n=t.attributes,s=n.length,i=0;i=48&&t<=57}function i(t,e){for(var i=(t+="").length,a=(e+="").length,o=0,l=0;o=i&&l=a?-1:l>=a&&o=i?1:i-a}i.caseInsensitive=i.i=function(t,e){return i((""+t).toLowerCase(),(""+e).toLowerCase())},Object.defineProperties(i,{alphabet:{get:function(){return e},set:function(t){r=[];var s=0;if(e=t)for(;s { + // category is URI encoded in EJS template for UTF-8 support + category = decodeURIComponent(atob(category)); + if (categoriesLoaded) { + activateCategory(category); + setCategoryHash(category); + } +}; + +window["quarto-listing-loaded"] = () => { + // Process any existing hash + const hash = getHash(); + + if (hash) { + // If there is a category, switch to that + if (hash.category) { + // category hash are URI encoded so we need to decode it before processing + // so that we can match it with the category element processed in JS + activateCategory(decodeURIComponent(hash.category)); + } + // Paginate a specific listing + const listingIds = Object.keys(window["quarto-listings"]); + for (const listingId of listingIds) { + const page = hash[getListingPageKey(listingId)]; + if (page) { + showPage(listingId, page); + } + } + } + + const listingIds = Object.keys(window["quarto-listings"]); + for (const listingId of listingIds) { + // The actual list + const list = window["quarto-listings"][listingId]; + + // Update the handlers for pagination events + refreshPaginationHandlers(listingId); + + // Render any visible items that need it + renderVisibleProgressiveImages(list); + + // Whenever the list is updated, we also need to + // attach handlers to the new pagination elements + // and refresh any newly visible items. + list.on("updated", function () { + renderVisibleProgressiveImages(list); + setTimeout(() => refreshPaginationHandlers(listingId)); + + // Show or hide the no matching message + toggleNoMatchingMessage(list); + }); + } +}; + +window.document.addEventListener("DOMContentLoaded", function (_event) { + // Attach click handlers to categories + const categoryEls = window.document.querySelectorAll( + ".quarto-listing-category .category" + ); + + for (const categoryEl of categoryEls) { + // category needs to support non ASCII characters + const category = decodeURIComponent( + atob(categoryEl.getAttribute("data-category")) + ); + categoryEl.onclick = () => { + activateCategory(category); + setCategoryHash(category); + }; + } + + // Attach a click handler to the category title + // (there should be only one, but since it is a class name, handle N) + const categoryTitleEls = window.document.querySelectorAll( + ".quarto-listing-category-title" + ); + for (const categoryTitleEl of categoryTitleEls) { + categoryTitleEl.onclick = () => { + activateCategory(""); + setCategoryHash(""); + }; + } + + categoriesLoaded = true; +}); + +function toggleNoMatchingMessage(list) { + const selector = `#${list.listContainer.id} .listing-no-matching`; + const noMatchingEl = window.document.querySelector(selector); + if (noMatchingEl) { + if (list.visibleItems.length === 0) { + noMatchingEl.classList.remove("d-none"); + } else { + if (!noMatchingEl.classList.contains("d-none")) { + noMatchingEl.classList.add("d-none"); + } + } + } +} + +function setCategoryHash(category) { + setHash({ category }); +} + +function setPageHash(listingId, page) { + const currentHash = getHash() || {}; + currentHash[getListingPageKey(listingId)] = page; + setHash(currentHash); +} + +function getListingPageKey(listingId) { + return `${listingId}-page`; +} + +function refreshPaginationHandlers(listingId) { + const listingEl = window.document.getElementById(listingId); + const paginationEls = listingEl.querySelectorAll( + ".pagination li.page-item:not(.disabled) .page.page-link" + ); + for (const paginationEl of paginationEls) { + paginationEl.onclick = (sender) => { + setPageHash(listingId, sender.target.getAttribute("data-i")); + showPage(listingId, sender.target.getAttribute("data-i")); + return false; + }; + } +} + +function renderVisibleProgressiveImages(list) { + // Run through the visible items and render any progressive images + for (const item of list.visibleItems) { + const itemEl = item.elm; + if (itemEl) { + const progressiveImgs = itemEl.querySelectorAll( + `img[${kProgressiveAttr}]` + ); + for (const progressiveImg of progressiveImgs) { + const srcValue = progressiveImg.getAttribute(kProgressiveAttr); + if (srcValue) { + progressiveImg.setAttribute("src", srcValue); + } + progressiveImg.removeAttribute(kProgressiveAttr); + } + } + } +} + +function getHash() { + // Hashes are of the form + // #name:value|name1:value1|name2:value2 + const currentUrl = new URL(window.location); + const hashRaw = currentUrl.hash ? currentUrl.hash.slice(1) : undefined; + return parseHash(hashRaw); +} + +const kAnd = "&"; +const kEquals = "="; + +function parseHash(hash) { + if (!hash) { + return undefined; + } + const hasValuesStrs = hash.split(kAnd); + const hashValues = hasValuesStrs + .map((hashValueStr) => { + const vals = hashValueStr.split(kEquals); + if (vals.length === 2) { + return { name: vals[0], value: vals[1] }; + } else { + return undefined; + } + }) + .filter((value) => { + return value !== undefined; + }); + + const hashObj = {}; + hashValues.forEach((hashValue) => { + hashObj[hashValue.name] = decodeURIComponent(hashValue.value); + }); + return hashObj; +} + +function makeHash(obj) { + return Object.keys(obj) + .map((key) => { + return `${key}${kEquals}${obj[key]}`; + }) + .join(kAnd); +} + +function setHash(obj) { + const hash = makeHash(obj); + window.history.pushState(null, null, `#${hash}`); +} + +function showPage(listingId, page) { + const list = window["quarto-listings"][listingId]; + if (list) { + list.show((page - 1) * list.page + 1, list.page); + } +} + +function activateCategory(category) { + // Deactivate existing categories + const activeEls = window.document.querySelectorAll( + ".quarto-listing-category .category.active" + ); + for (const activeEl of activeEls) { + activeEl.classList.remove("active"); + } + + // Activate this category + const categoryEl = window.document.querySelector( + `.quarto-listing-category .category[data-category='${btoa( + encodeURIComponent(category) + )}']` + ); + if (categoryEl) { + categoryEl.classList.add("active"); + } + + // Filter the listings to this category + filterListingCategory(category); +} + +function filterListingCategory(category) { + const listingIds = Object.keys(window["quarto-listings"]); + for (const listingId of listingIds) { + const list = window["quarto-listings"][listingId]; + if (list) { + if (category === "") { + // resets the filter + list.filter(); + } else { + // filter to this category + list.filter(function (item) { + const itemValues = item.values(); + if (itemValues.categories !== null) { + const categories = decodeURIComponent( + atob(itemValues.categories) + ).split(","); + return categories.includes(category); + } else { + return false; + } + }); + } + } + } +} diff --git a/docs/site_libs/quarto-nav/headroom.min.js b/docs/site_libs/quarto-nav/headroom.min.js new file mode 100644 index 0000000000000000000000000000000000000000..b08f1dffbbdae0ace4eb2510b43666c8491546d3 --- /dev/null +++ b/docs/site_libs/quarto-nav/headroom.min.js @@ -0,0 +1,7 @@ +/*! + * headroom.js v0.12.0 - Give your page some headroom. Hide your header until you need it + * Copyright (c) 2020 Nick Williams - http://wicky.nillia.ms/headroom.js + * License: MIT + */ + +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self).Headroom=n()}(this,function(){"use strict";function t(){return"undefined"!=typeof window}function d(t){return function(t){return t&&t.document&&function(t){return 9===t.nodeType}(t.document)}(t)?function(t){var n=t.document,o=n.body,s=n.documentElement;return{scrollHeight:function(){return Math.max(o.scrollHeight,s.scrollHeight,o.offsetHeight,s.offsetHeight,o.clientHeight,s.clientHeight)},height:function(){return t.innerHeight||s.clientHeight||o.clientHeight},scrollY:function(){return void 0!==t.pageYOffset?t.pageYOffset:(s||o.parentNode||o).scrollTop}}}(t):function(t){return{scrollHeight:function(){return Math.max(t.scrollHeight,t.offsetHeight,t.clientHeight)},height:function(){return Math.max(t.offsetHeight,t.clientHeight)},scrollY:function(){return t.scrollTop}}}(t)}function n(t,s,e){var n,o=function(){var n=!1;try{var t={get passive(){n=!0}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){n=!1}return n}(),i=!1,r=d(t),l=r.scrollY(),a={};function c(){var t=Math.round(r.scrollY()),n=r.height(),o=r.scrollHeight();a.scrollY=t,a.lastScrollY=l,a.direction=ls.tolerance[a.direction],e(a),l=t,i=!1}function h(){i||(i=!0,n=requestAnimationFrame(c))}var u=!!o&&{passive:!0,capture:!1};return t.addEventListener("scroll",h,u),c(),{destroy:function(){cancelAnimationFrame(n),t.removeEventListener("scroll",h,u)}}}function o(t){return t===Object(t)?t:{down:t,up:t}}function s(t,n){n=n||{},Object.assign(this,s.options,n),this.classes=Object.assign({},s.options.classes,n.classes),this.elem=t,this.tolerance=o(this.tolerance),this.offset=o(this.offset),this.initialised=!1,this.frozen=!1}return s.prototype={constructor:s,init:function(){return s.cutsTheMustard&&!this.initialised&&(this.addClass("initial"),this.initialised=!0,setTimeout(function(t){t.scrollTracker=n(t.scroller,{offset:t.offset,tolerance:t.tolerance},t.update.bind(t))},100,this)),this},destroy:function(){this.initialised=!1,Object.keys(this.classes).forEach(this.removeClass,this),this.scrollTracker.destroy()},unpin:function(){!this.hasClass("pinned")&&this.hasClass("unpinned")||(this.addClass("unpinned"),this.removeClass("pinned"),this.onUnpin&&this.onUnpin.call(this))},pin:function(){this.hasClass("unpinned")&&(this.addClass("pinned"),this.removeClass("unpinned"),this.onPin&&this.onPin.call(this))},freeze:function(){this.frozen=!0,this.addClass("frozen")},unfreeze:function(){this.frozen=!1,this.removeClass("frozen")},top:function(){this.hasClass("top")||(this.addClass("top"),this.removeClass("notTop"),this.onTop&&this.onTop.call(this))},notTop:function(){this.hasClass("notTop")||(this.addClass("notTop"),this.removeClass("top"),this.onNotTop&&this.onNotTop.call(this))},bottom:function(){this.hasClass("bottom")||(this.addClass("bottom"),this.removeClass("notBottom"),this.onBottom&&this.onBottom.call(this))},notBottom:function(){this.hasClass("notBottom")||(this.addClass("notBottom"),this.removeClass("bottom"),this.onNotBottom&&this.onNotBottom.call(this))},shouldUnpin:function(t){return"down"===t.direction&&!t.top&&t.toleranceExceeded},shouldPin:function(t){return"up"===t.direction&&t.toleranceExceeded||t.top},addClass:function(t){this.elem.classList.add.apply(this.elem.classList,this.classes[t].split(" "))},removeClass:function(t){this.elem.classList.remove.apply(this.elem.classList,this.classes[t].split(" "))},hasClass:function(t){return this.classes[t].split(" ").every(function(t){return this.classList.contains(t)},this.elem)},update:function(t){t.isOutOfBounds||!0!==this.frozen&&(t.top?this.top():this.notTop(),t.bottom?this.bottom():this.notBottom(),this.shouldUnpin(t)?this.unpin():this.shouldPin(t)&&this.pin())}},s.options={tolerance:{up:0,down:0},offset:0,scroller:t()?window:null,classes:{frozen:"headroom--frozen",pinned:"headroom--pinned",unpinned:"headroom--unpinned",top:"headroom--top",notTop:"headroom--not-top",bottom:"headroom--bottom",notBottom:"headroom--not-bottom",initial:"headroom"}},s.cutsTheMustard=!!(t()&&function(){}.bind&&"classList"in document.documentElement&&Object.assign&&Object.keys&&requestAnimationFrame),s}); diff --git a/docs/site_libs/quarto-nav/quarto-nav.js b/docs/site_libs/quarto-nav/quarto-nav.js new file mode 100644 index 0000000000000000000000000000000000000000..38cc43057bdfaae23727b4b638cf62a7594712c2 --- /dev/null +++ b/docs/site_libs/quarto-nav/quarto-nav.js @@ -0,0 +1,325 @@ +const headroomChanged = new CustomEvent("quarto-hrChanged", { + detail: {}, + bubbles: true, + cancelable: false, + composed: false, +}); + +const announceDismiss = () => { + const annEl = window.document.getElementById("quarto-announcement"); + if (annEl) { + annEl.remove(); + + const annId = annEl.getAttribute("data-announcement-id"); + window.localStorage.setItem(`quarto-announce-${annId}`, "true"); + } +}; + +const announceRegister = () => { + const annEl = window.document.getElementById("quarto-announcement"); + if (annEl) { + const annId = annEl.getAttribute("data-announcement-id"); + const isDismissed = + window.localStorage.getItem(`quarto-announce-${annId}`) || false; + if (isDismissed) { + announceDismiss(); + return; + } else { + annEl.classList.remove("hidden"); + } + + const actionEl = annEl.querySelector(".quarto-announcement-action"); + if (actionEl) { + actionEl.addEventListener("click", function (e) { + e.preventDefault(); + // Hide the bar immediately + announceDismiss(); + }); + } + } +}; + +window.document.addEventListener("DOMContentLoaded", function () { + let init = false; + + announceRegister(); + + // Manage the back to top button, if one is present. + let lastScrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollDownBuffer = 5; + const scrollUpBuffer = 35; + const btn = document.getElementById("quarto-back-to-top"); + const hideBackToTop = () => { + btn.style.display = "none"; + }; + const showBackToTop = () => { + btn.style.display = "inline-block"; + }; + if (btn) { + window.document.addEventListener( + "scroll", + function () { + const currentScrollTop = + window.pageYOffset || document.documentElement.scrollTop; + + // Shows and hides the button 'intelligently' as the user scrolls + if (currentScrollTop - scrollDownBuffer > lastScrollTop) { + hideBackToTop(); + lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop; + } else if (currentScrollTop < lastScrollTop - scrollUpBuffer) { + showBackToTop(); + lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop; + } + + // Show the button at the bottom, hides it at the top + if (currentScrollTop <= 0) { + hideBackToTop(); + } else if ( + window.innerHeight + currentScrollTop >= + document.body.offsetHeight + ) { + showBackToTop(); + } + }, + false + ); + } + + function throttle(func, wait) { + var timeout; + return function () { + const context = this; + const args = arguments; + const later = function () { + clearTimeout(timeout); + timeout = null; + func.apply(context, args); + }; + + if (!timeout) { + timeout = setTimeout(later, wait); + } + }; + } + + function headerOffset() { + // Set an offset if there is are fixed top navbar + const headerEl = window.document.querySelector("header.fixed-top"); + if (headerEl) { + return headerEl.clientHeight; + } else { + return 0; + } + } + + function footerOffset() { + const footerEl = window.document.querySelector("footer.footer"); + if (footerEl) { + return footerEl.clientHeight; + } else { + return 0; + } + } + + function dashboardOffset() { + const dashboardNavEl = window.document.getElementById( + "quarto-dashboard-header" + ); + if (dashboardNavEl !== null) { + return dashboardNavEl.clientHeight; + } else { + return 0; + } + } + + function updateDocumentOffsetWithoutAnimation() { + updateDocumentOffset(false); + } + + function updateDocumentOffset(animated) { + // set body offset + const topOffset = headerOffset(); + const bodyOffset = topOffset + footerOffset() + dashboardOffset(); + const bodyEl = window.document.body; + bodyEl.setAttribute("data-bs-offset", topOffset); + bodyEl.style.paddingTop = topOffset + "px"; + + // deal with sidebar offsets + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + if (!animated) { + sidebar.classList.add("notransition"); + // Remove the no transition class after the animation has time to complete + setTimeout(function () { + sidebar.classList.remove("notransition"); + }, 201); + } + + if (window.Headroom && sidebar.classList.contains("sidebar-unpinned")) { + sidebar.style.top = "0"; + sidebar.style.maxHeight = "100vh"; + } else { + sidebar.style.top = topOffset + "px"; + sidebar.style.maxHeight = "calc(100vh - " + topOffset + "px)"; + } + }); + + // allow space for footer + const mainContainer = window.document.querySelector(".quarto-container"); + if (mainContainer) { + mainContainer.style.minHeight = "calc(100vh - " + bodyOffset + "px)"; + } + + // link offset + let linkStyle = window.document.querySelector("#quarto-target-style"); + if (!linkStyle) { + linkStyle = window.document.createElement("style"); + linkStyle.setAttribute("id", "quarto-target-style"); + window.document.head.appendChild(linkStyle); + } + while (linkStyle.firstChild) { + linkStyle.removeChild(linkStyle.firstChild); + } + if (topOffset > 0) { + linkStyle.appendChild( + window.document.createTextNode(` + section:target::before { + content: ""; + display: block; + height: ${topOffset}px; + margin: -${topOffset}px 0 0; + }`) + ); + } + if (init) { + window.dispatchEvent(headroomChanged); + } + init = true; + } + + // initialize headroom + var header = window.document.querySelector("#quarto-header"); + if (header && window.Headroom) { + const headroom = new window.Headroom(header, { + tolerance: 5, + onPin: function () { + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + sidebar.classList.remove("sidebar-unpinned"); + }); + updateDocumentOffset(); + }, + onUnpin: function () { + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + sidebar.classList.add("sidebar-unpinned"); + }); + updateDocumentOffset(); + }, + }); + headroom.init(); + + let frozen = false; + window.quartoToggleHeadroom = function () { + if (frozen) { + headroom.unfreeze(); + frozen = false; + } else { + headroom.freeze(); + frozen = true; + } + }; + } + + window.addEventListener( + "hashchange", + function (e) { + if ( + getComputedStyle(document.documentElement).scrollBehavior !== "smooth" + ) { + window.scrollTo(0, window.pageYOffset - headerOffset()); + } + }, + false + ); + + // Observe size changed for the header + const headerEl = window.document.querySelector("header.fixed-top"); + if (headerEl && window.ResizeObserver) { + const observer = new window.ResizeObserver(() => { + setTimeout(updateDocumentOffsetWithoutAnimation, 0); + }); + observer.observe(headerEl, { + attributes: true, + childList: true, + characterData: true, + }); + } else { + window.addEventListener( + "resize", + throttle(updateDocumentOffsetWithoutAnimation, 50) + ); + } + setTimeout(updateDocumentOffsetWithoutAnimation, 250); + + // fixup index.html links if we aren't on the filesystem + if (window.location.protocol !== "file:") { + const links = window.document.querySelectorAll("a"); + for (let i = 0; i < links.length; i++) { + if (links[i].href) { + links[i].dataset.originalHref = links[i].href; + links[i].href = links[i].href.replace(/\/index\.html/, "/"); + } + } + + // Fixup any sharing links that require urls + // Append url to any sharing urls + const sharingLinks = window.document.querySelectorAll( + "a.sidebar-tools-main-item, a.quarto-navigation-tool, a.quarto-navbar-tools, a.quarto-navbar-tools-item" + ); + for (let i = 0; i < sharingLinks.length; i++) { + const sharingLink = sharingLinks[i]; + const href = sharingLink.getAttribute("href"); + if (href) { + sharingLink.setAttribute( + "href", + href.replace("|url|", window.location.href) + ); + } + } + + // Scroll the active navigation item into view, if necessary + const navSidebar = window.document.querySelector("nav#quarto-sidebar"); + if (navSidebar) { + // Find the active item + const activeItem = navSidebar.querySelector("li.sidebar-item a.active"); + if (activeItem) { + // Wait for the scroll height and height to resolve by observing size changes on the + // nav element that is scrollable + const resizeObserver = new ResizeObserver((_entries) => { + // The bottom of the element + const elBottom = activeItem.offsetTop; + const viewBottom = navSidebar.scrollTop + navSidebar.clientHeight; + + // The element height and scroll height are the same, then we are still loading + if (viewBottom !== navSidebar.scrollHeight) { + // Determine if the item isn't visible and scroll to it + if (elBottom >= viewBottom) { + navSidebar.scrollTop = elBottom; + } + + // stop observing now since we've completed the scroll + resizeObserver.unobserve(navSidebar); + } + }); + resizeObserver.observe(navSidebar); + } + } + } +}); diff --git a/docs/site_libs/quarto-search/autocomplete.umd.js b/docs/site_libs/quarto-search/autocomplete.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..ae0063aa9ea872eb1f240ee64628a7ae0cd5b9de --- /dev/null +++ b/docs/site_libs/quarto-search/autocomplete.umd.js @@ -0,0 +1,3 @@ +/*! @algolia/autocomplete-js 1.11.1 | MIT License | © Algolia, Inc. and contributors | https://github.com/algolia/autocomplete */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self)["@algolia/autocomplete-js"]={})}(this,(function(e){"use strict";function t(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function n(e){for(var n=1;n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function a(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var r,o,i,u,a=[],l=!0,c=!1;try{if(i=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=i.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(e){c=!0,o=e}finally{try{if(!l&&null!=n.return&&(u=n.return(),Object(u)!==u))return}finally{if(c)throw o}}return a}}(e,t)||c(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(e){return function(e){if(Array.isArray(e))return s(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||c(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function c(e,t){if(e){if("string"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?s(e,t):void 0}}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function x(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function N(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:20,n=[],r=0;r=3||2===n&&r>=4||1===n&&r>=10);function i(t,n,r){if(o&&void 0!==r){var i=r[0].__autocomplete_algoliaCredentials,u={"X-Algolia-Application-Id":i.appId,"X-Algolia-API-Key":i.apiKey};e.apply(void 0,[t].concat(D(n),[{headers:u}]))}else e.apply(void 0,[t].concat(D(n)))}return{init:function(t,n){e("init",{appId:t,apiKey:n})},setUserToken:function(t){e("setUserToken",t)},clickedObjectIDsAfterSearch:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&i("clickedObjectIDsAfterSearch",B(t),t[0].items)},clickedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&i("clickedObjectIDs",B(t),t[0].items)},clickedFilters:function(){for(var t=arguments.length,n=new Array(t),r=0;r0&&e.apply(void 0,["clickedFilters"].concat(n))},convertedObjectIDsAfterSearch:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&i("convertedObjectIDsAfterSearch",B(t),t[0].items)},convertedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&i("convertedObjectIDs",B(t),t[0].items)},convertedFilters:function(){for(var t=arguments.length,n=new Array(t),r=0;r0&&e.apply(void 0,["convertedFilters"].concat(n))},viewedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&t.reduce((function(e,t){var n=t.items,r=k(t,A);return[].concat(D(e),D(q(N(N({},r),{},{objectIDs:(null==n?void 0:n.map((function(e){return e.objectID})))||r.objectIDs})).map((function(e){return{items:n,payload:e}}))))}),[]).forEach((function(e){var t=e.items;return i("viewedObjectIDs",[e.payload],t)}))},viewedFilters:function(){for(var t=arguments.length,n=new Array(t),r=0;r0&&e.apply(void 0,["viewedFilters"].concat(n))}}}function F(e){var t=e.items.reduce((function(e,t){var n;return e[t.__autocomplete_indexName]=(null!==(n=e[t.__autocomplete_indexName])&&void 0!==n?n:[]).concat(t),e}),{});return Object.keys(t).map((function(e){return{index:e,items:t[e],algoliaSource:["autocomplete"]}}))}function L(e){return e.objectID&&e.__autocomplete_indexName&&e.__autocomplete_queryID}function U(e){return U="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},U(e)}function M(e){return function(e){if(Array.isArray(e))return H(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return H(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return H(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function H(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&z({onItemsChange:r,items:n,insights:a,state:t}))}}),0);return{name:"aa.algoliaInsightsPlugin",subscribe:function(e){var t=e.setContext,n=e.onSelect,r=e.onActive;function l(e){t({algoliaInsightsPlugin:{__algoliaSearchParameters:W({clickAnalytics:!0},e?{userToken:e}:{}),insights:a}})}u("addAlgoliaAgent","insights-plugin"),l(),u("onUserTokenChange",l),u("getUserToken",null,(function(e,t){l(t)})),n((function(e){var t=e.item,n=e.state,r=e.event,i=e.source;L(t)&&o({state:n,event:r,insights:a,item:t,insightsEvents:[W({eventName:"Item Selected"},j({item:t,items:i.getItems().filter(L)}))]})})),r((function(e){var t=e.item,n=e.source,r=e.state,o=e.event;L(t)&&i({state:r,event:o,insights:a,item:t,insightsEvents:[W({eventName:"Item Active"},j({item:t,items:n.getItems().filter(L)}))]})}))},onStateChange:function(e){var t=e.state;c({state:t})},__autocomplete_pluginOptions:e}}function J(e,t){var n=t;return{then:function(t,r){return J(e.then(Y(t,n,e),Y(r,n,e)),n)},catch:function(t){return J(e.catch(Y(t,n,e)),n)},finally:function(t){return t&&n.onCancelList.push(t),J(e.finally(Y(t&&function(){return n.onCancelList=[],t()},n,e)),n)},cancel:function(){n.isCanceled=!0;var e=n.onCancelList;n.onCancelList=[],e.forEach((function(e){e()}))},isCanceled:function(){return!0===n.isCanceled}}}function X(e){return J(e,{isCanceled:!1,onCancelList:[]})}function Y(e,t,n){return e?function(n){return t.isCanceled?n:e(n)}:n}function Z(e,t,n,r){if(!n)return null;if(e<0&&(null===t||null!==r&&0===t))return n+e;var o=(null===t?-1:t)+e;return o<=-1||o>=n?null===r?null:0:o}function ee(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function te(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0},reshape:function(e){return e.sources}},e),{},{id:null!==(n=e.id)&&void 0!==n?n:d(),plugins:o,initialState:he({activeItemId:null,query:"",completion:null,collections:[],isOpen:!1,status:"idle",context:{}},e.initialState),onStateChange:function(t){var n;null===(n=e.onStateChange)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onStateChange)||void 0===n?void 0:n.call(e,t)}))},onSubmit:function(t){var n;null===(n=e.onSubmit)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onSubmit)||void 0===n?void 0:n.call(e,t)}))},onReset:function(t){var n;null===(n=e.onReset)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onReset)||void 0===n?void 0:n.call(e,t)}))},getSources:function(n){return Promise.all([].concat(ye(o.map((function(e){return e.getSources}))),[e.getSources]).filter(Boolean).map((function(e){return function(e,t){var n=[];return Promise.resolve(e(t)).then((function(e){return Promise.all(e.filter((function(e){return Boolean(e)})).map((function(e){if(e.sourceId,n.includes(e.sourceId))throw new Error("[Autocomplete] The `sourceId` ".concat(JSON.stringify(e.sourceId)," is not unique."));n.push(e.sourceId);var t={getItemInputValue:function(e){return e.state.query},getItemUrl:function(){},onSelect:function(e){(0,e.setIsOpen)(!1)},onActive:O,onResolve:O};Object.keys(t).forEach((function(e){t[e].__default=!0}));var r=te(te({},t),e);return Promise.resolve(r)})))}))}(e,n)}))).then((function(e){return m(e)})).then((function(e){return e.map((function(e){return he(he({},e),{},{onSelect:function(n){e.onSelect(n),t.forEach((function(e){var t;return null===(t=e.onSelect)||void 0===t?void 0:t.call(e,n)}))},onActive:function(n){e.onActive(n),t.forEach((function(e){var t;return null===(t=e.onActive)||void 0===t?void 0:t.call(e,n)}))},onResolve:function(n){e.onResolve(n),t.forEach((function(e){var t;return null===(t=e.onResolve)||void 0===t?void 0:t.call(e,n)}))}})}))}))},navigator:he({navigate:function(e){var t=e.itemUrl;r.location.assign(t)},navigateNewTab:function(e){var t=e.itemUrl,n=r.open(t,"_blank","noopener");null==n||n.focus()},navigateNewWindow:function(e){var t=e.itemUrl;r.open(t,"_blank","noopener")}},e.navigator)})}function Se(e){return Se="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Se(e)}function je(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Pe(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var He,Ve,We,Ke=null,Qe=(He=-1,Ve=-1,We=void 0,function(e){var t=++He;return Promise.resolve(e).then((function(e){return We&&t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function et(e){return et="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},et(e)}var tt=["props","refresh","store"],nt=["inputElement","formElement","panelElement"],rt=["inputElement"],ot=["inputElement","maxLength"],it=["source"],ut=["item","source"];function at(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function lt(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function ft(e){var t=e.props,n=e.refresh,r=e.store,o=st(e,tt);return{getEnvironmentProps:function(e){var n=e.inputElement,o=e.formElement,i=e.panelElement;function u(e){!r.getState().isOpen&&r.pendingRequests.isEmpty()||e.target===n||!1===[o,i].some((function(t){return n=t,r=e.target,n===r||n.contains(r);var n,r}))&&(r.dispatch("blur",null),t.debug||r.pendingRequests.cancelAll())}return lt({onTouchStart:u,onMouseDown:u,onTouchMove:function(e){!1!==r.getState().isOpen&&n===t.environment.document.activeElement&&e.target!==n&&n.blur()}},st(e,nt))},getRootProps:function(e){return lt({role:"combobox","aria-expanded":r.getState().isOpen,"aria-haspopup":"listbox","aria-owns":r.getState().isOpen?r.getState().collections.map((function(e){var n=e.source;return ie(t.id,"list",n)})).join(" "):void 0,"aria-labelledby":ie(t.id,"label")},e)},getFormProps:function(e){return e.inputElement,lt({action:"",noValidate:!0,role:"search",onSubmit:function(i){var u;i.preventDefault(),t.onSubmit(lt({event:i,refresh:n,state:r.getState()},o)),r.dispatch("submit",null),null===(u=e.inputElement)||void 0===u||u.blur()},onReset:function(i){var u;i.preventDefault(),t.onReset(lt({event:i,refresh:n,state:r.getState()},o)),r.dispatch("reset",null),null===(u=e.inputElement)||void 0===u||u.focus()}},st(e,rt))},getLabelProps:function(e){return lt({htmlFor:ie(t.id,"input"),id:ie(t.id,"label")},e)},getInputProps:function(e){var i;function u(e){(t.openOnFocus||Boolean(r.getState().query))&&$e(lt({event:e,props:t,query:r.getState().completion||r.getState().query,refresh:n,store:r},o)),r.dispatch("focus",null)}var a=e||{};a.inputElement;var l=a.maxLength,c=void 0===l?512:l,s=st(a,ot),f=oe(r.getState()),p=function(e){return Boolean(e&&e.match(ue))}((null===(i=t.environment.navigator)||void 0===i?void 0:i.userAgent)||""),m=t.enterKeyHint||(null!=f&&f.itemUrl&&!p?"go":"search");return lt({"aria-autocomplete":"both","aria-activedescendant":r.getState().isOpen&&null!==r.getState().activeItemId?ie(t.id,"item-".concat(r.getState().activeItemId),null==f?void 0:f.source):void 0,"aria-controls":r.getState().isOpen?r.getState().collections.map((function(e){var n=e.source;return ie(t.id,"list",n)})).join(" "):void 0,"aria-labelledby":ie(t.id,"label"),value:r.getState().completion||r.getState().query,id:ie(t.id,"input"),autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",enterKeyHint:m,spellCheck:"false",autoFocus:t.autoFocus,placeholder:t.placeholder,maxLength:c,type:"search",onChange:function(e){$e(lt({event:e,props:t,query:e.currentTarget.value.slice(0,c),refresh:n,store:r},o))},onKeyDown:function(e){!function(e){var t=e.event,n=e.props,r=e.refresh,o=e.store,i=Ze(e,Ge);if("ArrowUp"===t.key||"ArrowDown"===t.key){var u=function(){var e=oe(o.getState()),t=n.environment.document.getElementById(ie(n.id,"item-".concat(o.getState().activeItemId),null==e?void 0:e.source));t&&(t.scrollIntoViewIfNeeded?t.scrollIntoViewIfNeeded(!1):t.scrollIntoView(!1))},a=function(){var e=oe(o.getState());if(null!==o.getState().activeItemId&&e){var n=e.item,u=e.itemInputValue,a=e.itemUrl,l=e.source;l.onActive(Xe({event:t,item:n,itemInputValue:u,itemUrl:a,refresh:r,source:l,state:o.getState()},i))}};t.preventDefault(),!1===o.getState().isOpen&&(n.openOnFocus||Boolean(o.getState().query))?$e(Xe({event:t,props:n,query:o.getState().query,refresh:r,store:o},i)).then((function(){o.dispatch(t.key,{nextActiveItemId:n.defaultActiveItemId}),a(),setTimeout(u,0)})):(o.dispatch(t.key,{}),a(),u())}else if("Escape"===t.key)t.preventDefault(),o.dispatch(t.key,null),o.pendingRequests.cancelAll();else if("Tab"===t.key)o.dispatch("blur",null),o.pendingRequests.cancelAll();else if("Enter"===t.key){if(null===o.getState().activeItemId||o.getState().collections.every((function(e){return 0===e.items.length})))return void(n.debug||o.pendingRequests.cancelAll());t.preventDefault();var l=oe(o.getState()),c=l.item,s=l.itemInputValue,f=l.itemUrl,p=l.source;if(t.metaKey||t.ctrlKey)void 0!==f&&(p.onSelect(Xe({event:t,item:c,itemInputValue:s,itemUrl:f,refresh:r,source:p,state:o.getState()},i)),n.navigator.navigateNewTab({itemUrl:f,item:c,state:o.getState()}));else if(t.shiftKey)void 0!==f&&(p.onSelect(Xe({event:t,item:c,itemInputValue:s,itemUrl:f,refresh:r,source:p,state:o.getState()},i)),n.navigator.navigateNewWindow({itemUrl:f,item:c,state:o.getState()}));else if(t.altKey);else{if(void 0!==f)return p.onSelect(Xe({event:t,item:c,itemInputValue:s,itemUrl:f,refresh:r,source:p,state:o.getState()},i)),void n.navigator.navigate({itemUrl:f,item:c,state:o.getState()});$e(Xe({event:t,nextState:{isOpen:!1},props:n,query:s,refresh:r,store:o},i)).then((function(){p.onSelect(Xe({event:t,item:c,itemInputValue:s,itemUrl:f,refresh:r,source:p,state:o.getState()},i))}))}}}(lt({event:e,props:t,refresh:n,store:r},o))},onFocus:u,onBlur:O,onClick:function(n){e.inputElement!==t.environment.document.activeElement||r.getState().isOpen||u(n)}},s)},getPanelProps:function(e){return lt({onMouseDown:function(e){e.preventDefault()},onMouseLeave:function(){r.dispatch("mouseleave",null)}},e)},getListProps:function(e){var n=e||{},r=n.source,o=st(n,it);return lt({role:"listbox","aria-labelledby":ie(t.id,"label"),id:ie(t.id,"list",r)},o)},getItemProps:function(e){var i=e.item,u=e.source,a=st(e,ut);return lt({id:ie(t.id,"item-".concat(i.__autocomplete_id),u),role:"option","aria-selected":r.getState().activeItemId===i.__autocomplete_id,onMouseMove:function(e){if(i.__autocomplete_id!==r.getState().activeItemId){r.dispatch("mousemove",i.__autocomplete_id);var t=oe(r.getState());if(null!==r.getState().activeItemId&&t){var u=t.item,a=t.itemInputValue,l=t.itemUrl,c=t.source;c.onActive(lt({event:e,item:u,itemInputValue:a,itemUrl:l,refresh:n,source:c,state:r.getState()},o))}}},onMouseDown:function(e){e.preventDefault()},onClick:function(e){var a=u.getItemInputValue({item:i,state:r.getState()}),l=u.getItemUrl({item:i,state:r.getState()});(l?Promise.resolve():$e(lt({event:e,nextState:{isOpen:!1},props:t,query:a,refresh:n,store:r},o))).then((function(){u.onSelect(lt({event:e,item:i,itemInputValue:a,itemUrl:l,refresh:n,source:u,state:r.getState()},o))}))}},a)}}}function pt(e){return pt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},pt(e)}function mt(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function vt(e){for(var t=1;t=5&&((o||!e&&5===r)&&(u.push(r,0,o,n),r=6),e&&(u.push(r,e,0,n),r=6)),o=""},l=0;l"===t?(r=1,o=""):o=t+o[0]:i?t===i?i="":o+=t:'"'===t||"'"===t?i=t:">"===t?(a(),r=1):r&&("="===t?(r=5,n=o,o=""):"/"===t&&(r<5||">"===e[l][c+1])?(a(),3===r&&(u=u[0]),r=u,(u=u[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(a(),r=2):o+=t),3===r&&"!--"===o&&(r=4,u=u[0])}return a(),u}(e)),t),arguments,[])).length>1?t:t[0]}var kt=function(e){var t=e.environment,n=t.document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("class","aa-ClearIcon"),n.setAttribute("viewBox","0 0 24 24"),n.setAttribute("width","18"),n.setAttribute("height","18"),n.setAttribute("fill","currentColor");var r=t.document.createElementNS("http://www.w3.org/2000/svg","path");return r.setAttribute("d","M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"),n.appendChild(r),n};function xt(e,t){if("string"==typeof t){var n=e.document.querySelector(t);return"The element ".concat(JSON.stringify(t)," is not in the document."),n}return t}function Nt(){for(var e=arguments.length,t=new Array(e),n=0;n2&&(u.children=arguments.length>3?Jt.call(arguments,2):n),"function"==typeof e&&null!=e.defaultProps)for(i in e.defaultProps)void 0===u[i]&&(u[i]=e.defaultProps[i]);return sn(e,u,r,o,null)}function sn(e,t,n,r,o){var i={type:e,props:t,key:n,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==o?++Yt:o};return null==o&&null!=Xt.vnode&&Xt.vnode(i),i}function fn(e){return e.children}function pn(e,t){this.props=e,this.context=t}function mn(e,t){if(null==t)return e.__?mn(e.__,e.__.__k.indexOf(e)+1):null;for(var n;tt&&Zt.sort(nn));yn.__r=0}function bn(e,t,n,r,o,i,u,a,l,c){var s,f,p,m,v,d,y,b=r&&r.__k||on,g=b.length;for(n.__k=[],s=0;s0?sn(m.type,m.props,m.key,m.ref?m.ref:null,m.__v):m)){if(m.__=n,m.__b=n.__b+1,null===(p=b[s])||p&&m.key==p.key&&m.type===p.type)b[s]=void 0;else for(f=0;f=0;t--)if((n=e.__k[t])&&(r=On(n)))return r;return null}function _n(e,t,n){"-"===t[0]?e.setProperty(t,null==n?"":n):e[t]=null==n?"":"number"!=typeof n||un.test(t)?n:n+"px"}function Sn(e,t,n,r,o){var i;e:if("style"===t)if("string"==typeof n)e.style.cssText=n;else{if("string"==typeof r&&(e.style.cssText=r=""),r)for(t in r)n&&t in n||_n(e.style,t,"");if(n)for(t in n)r&&n[t]===r[t]||_n(e.style,t,n[t])}else if("o"===t[0]&&"n"===t[1])i=t!==(t=t.replace(/Capture$/,"")),t=t.toLowerCase()in e?t.toLowerCase().slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=n,n?r||e.addEventListener(t,i?Pn:jn,i):e.removeEventListener(t,i?Pn:jn,i);else if("dangerouslySetInnerHTML"!==t){if(o)t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!==t&&"height"!==t&&"href"!==t&&"list"!==t&&"form"!==t&&"tabIndex"!==t&&"download"!==t&&t in e)try{e[t]=null==n?"":n;break e}catch(e){}"function"==typeof n||(null==n||!1===n&&"-"!==t[4]?e.removeAttribute(t):e.setAttribute(t,n))}}function jn(e){return this.l[e.type+!1](Xt.event?Xt.event(e):e)}function Pn(e){return this.l[e.type+!0](Xt.event?Xt.event(e):e)}function wn(e,t,n,r,o,i,u,a,l){var c,s,f,p,m,v,d,y,b,g,h,O,_,S,j,P=t.type;if(void 0!==t.constructor)return null;null!=n.__h&&(l=n.__h,a=t.__e=n.__e,t.__h=null,i=[a]),(c=Xt.__b)&&c(t);try{e:if("function"==typeof P){if(y=t.props,b=(c=P.contextType)&&r[c.__c],g=c?b?b.props.value:c.__:r,n.__c?d=(s=t.__c=n.__c).__=s.__E:("prototype"in P&&P.prototype.render?t.__c=s=new P(y,g):(t.__c=s=new pn(y,g),s.constructor=P,s.render=Cn),b&&b.sub(s),s.props=y,s.state||(s.state={}),s.context=g,s.__n=r,f=s.__d=!0,s.__h=[],s._sb=[]),null==s.__s&&(s.__s=s.state),null!=P.getDerivedStateFromProps&&(s.__s==s.state&&(s.__s=an({},s.__s)),an(s.__s,P.getDerivedStateFromProps(y,s.__s))),p=s.props,m=s.state,s.__v=t,f)null==P.getDerivedStateFromProps&&null!=s.componentWillMount&&s.componentWillMount(),null!=s.componentDidMount&&s.__h.push(s.componentDidMount);else{if(null==P.getDerivedStateFromProps&&y!==p&&null!=s.componentWillReceiveProps&&s.componentWillReceiveProps(y,g),!s.__e&&null!=s.shouldComponentUpdate&&!1===s.shouldComponentUpdate(y,s.__s,g)||t.__v===n.__v){for(t.__v!==n.__v&&(s.props=y,s.state=s.__s,s.__d=!1),s.__e=!1,t.__e=n.__e,t.__k=n.__k,t.__k.forEach((function(e){e&&(e.__=t)})),h=0;h0&&void 0!==arguments[0]?arguments[0]:[];return{get:function(){return e},add:function(t){var n=e[e.length-1];(null==n?void 0:n.isHighlighted)===t.isHighlighted?e[e.length-1]={value:n.value+t.value,isHighlighted:n.isHighlighted}:e.push(t)}}}(n?[{value:n,isHighlighted:!1}]:[]);return t.forEach((function(e){var t=e.split(xn);r.add({value:t[0],isHighlighted:!0}),""!==t[1]&&r.add({value:t[1],isHighlighted:!1})})),r.get()}function Tn(e){return function(e){if(Array.isArray(e))return qn(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return qn(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return qn(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function qn(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n",""":'"',"'":"'"},Fn=new RegExp(/\w/i),Ln=/&(amp|quot|lt|gt|#39);/g,Un=RegExp(Ln.source);function Mn(e,t){var n,r,o,i=e[t],u=(null===(n=e[t+1])||void 0===n?void 0:n.isHighlighted)||!0,a=(null===(r=e[t-1])||void 0===r?void 0:r.isHighlighted)||!0;return Fn.test((o=i.value)&&Un.test(o)?o.replace(Ln,(function(e){return Rn[e]})):o)||a!==u?i.isHighlighted:a}function Hn(e){return Hn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Hn(e)}function Vn(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Wn(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function ur(e){return function(e){if(Array.isArray(e))return ar(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return ar(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return ar(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function ar(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0;if(!O.value.core.openOnFocus&&!t.query)return n;var r=Boolean(y.current||O.value.renderer.renderNoResults);return!n&&r||n},__autocomplete_metadata:{userAgents:br,options:e}}))})),j=f(n({collections:[],completion:null,context:{},isOpen:!1,query:"",activeItemId:null,status:"idle"},O.value.core.initialState)),P={getEnvironmentProps:O.value.renderer.getEnvironmentProps,getFormProps:O.value.renderer.getFormProps,getInputProps:O.value.renderer.getInputProps,getItemProps:O.value.renderer.getItemProps,getLabelProps:O.value.renderer.getLabelProps,getListProps:O.value.renderer.getListProps,getPanelProps:O.value.renderer.getPanelProps,getRootProps:O.value.renderer.getRootProps},w={setActiveItemId:S.value.setActiveItemId,setQuery:S.value.setQuery,setCollections:S.value.setCollections,setIsOpen:S.value.setIsOpen,setStatus:S.value.setStatus,setContext:S.value.setContext,refresh:S.value.refresh,navigator:S.value.navigator},I=m((function(){return Ct.bind(O.value.renderer.renderer.createElement)})),A=m((function(){return Gt({autocomplete:S.value,autocompleteScopeApi:w,classNames:O.value.renderer.classNames,environment:O.value.core.environment,isDetached:_.value,placeholder:O.value.core.placeholder,propGetters:P,setIsModalOpen:k,state:j.current,translations:O.value.renderer.translations})}));function E(){Ht(A.value.panel,{style:_.value?{}:yr({panelPlacement:O.value.renderer.panelPlacement,container:A.value.root,form:A.value.form,environment:O.value.core.environment})})}function D(e){j.current=e;var t={autocomplete:S.value,autocompleteScopeApi:w,classNames:O.value.renderer.classNames,components:O.value.renderer.components,container:O.value.renderer.container,html:I.value,dom:A.value,panelContainer:_.value?A.value.detachedContainer:O.value.renderer.panelContainer,propGetters:P,state:j.current,renderer:O.value.renderer.renderer},r=!b(e)&&!y.current&&O.value.renderer.renderNoResults||O.value.renderer.render;!function(e){var t=e.autocomplete,r=e.autocompleteScopeApi,o=e.dom,i=e.propGetters,u=e.state;Vt(o.root,i.getRootProps(n({state:u,props:t.getRootProps({})},r))),Vt(o.input,i.getInputProps(n({state:u,props:t.getInputProps({inputElement:o.input}),inputElement:o.input},r))),Ht(o.label,{hidden:"stalled"===u.status}),Ht(o.loadingIndicator,{hidden:"stalled"!==u.status}),Ht(o.clearButton,{hidden:!u.query}),Ht(o.detachedSearchButtonQuery,{textContent:u.query}),Ht(o.detachedSearchButtonPlaceholder,{hidden:Boolean(u.query)})}(t),function(e,t){var r=t.autocomplete,o=t.autocompleteScopeApi,u=t.classNames,a=t.html,l=t.dom,c=t.panelContainer,s=t.propGetters,f=t.state,p=t.components,m=t.renderer;if(f.isOpen){c.contains(l.panel)||"loading"===f.status||c.appendChild(l.panel),l.panel.classList.toggle("aa-Panel--stalled","stalled"===f.status);var v=f.collections.filter((function(e){var t=e.source,n=e.items;return t.templates.noResults||n.length>0})).map((function(e,t){var l=e.source,c=e.items;return m.createElement("section",{key:t,className:u.source,"data-autocomplete-source-id":l.sourceId},l.templates.header&&m.createElement("div",{className:u.sourceHeader},l.templates.header({components:p,createElement:m.createElement,Fragment:m.Fragment,items:c,source:l,state:f,html:a})),l.templates.noResults&&0===c.length?m.createElement("div",{className:u.sourceNoResults},l.templates.noResults({components:p,createElement:m.createElement,Fragment:m.Fragment,source:l,state:f,html:a})):m.createElement("ul",i({className:u.list},s.getListProps(n({state:f,props:r.getListProps({source:l})},o))),c.map((function(e){var t=r.getItemProps({item:e,source:l});return m.createElement("li",i({key:t.id,className:u.item},s.getItemProps(n({state:f,props:t},o))),l.templates.item({components:p,createElement:m.createElement,Fragment:m.Fragment,item:e,state:f,html:a}))}))),l.templates.footer&&m.createElement("div",{className:u.sourceFooter},l.templates.footer({components:p,createElement:m.createElement,Fragment:m.Fragment,items:c,source:l,state:f,html:a})))})),d=m.createElement(m.Fragment,null,m.createElement("div",{className:u.panelLayout},v),m.createElement("div",{className:"aa-GradientBottom"})),y=v.reduce((function(e,t){return e[t.props["data-autocomplete-source-id"]]=t,e}),{});e(n(n({children:d,state:f,sections:v,elements:y},m),{},{components:p,html:a},o),l.panel)}else c.contains(l.panel)&&c.removeChild(l.panel)}(r,t)}function C(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};l();var t=O.value.renderer,n=t.components,r=u(t,gr);g.current=qt(r,O.value.core,{components:Bt(n,(function(e){return!e.value.hasOwnProperty("__autocomplete_componentName")})),initialState:j.current},e),v(),c(),S.value.refresh().then((function(){D(j.current)}))}function k(e){requestAnimationFrame((function(){var t=O.value.core.environment.document.body.contains(A.value.detachedOverlay);e!==t&&(e?(O.value.core.environment.document.body.appendChild(A.value.detachedOverlay),O.value.core.environment.document.body.classList.add("aa-Detached"),A.value.input.focus()):(O.value.core.environment.document.body.removeChild(A.value.detachedOverlay),O.value.core.environment.document.body.classList.remove("aa-Detached")))}))}return a((function(){var e=S.value.getEnvironmentProps({formElement:A.value.form,panelElement:A.value.panel,inputElement:A.value.input});return Ht(O.value.core.environment,e),function(){Ht(O.value.core.environment,Object.keys(e).reduce((function(e,t){return n(n({},e),{},o({},t,void 0))}),{}))}})),a((function(){var e=_.value?O.value.core.environment.document.body:O.value.renderer.panelContainer,t=_.value?A.value.detachedOverlay:A.value.panel;return _.value&&j.current.isOpen&&k(!0),D(j.current),function(){e.contains(t)&&e.removeChild(t)}})),a((function(){var e=O.value.renderer.container;return e.appendChild(A.value.root),function(){e.removeChild(A.value.root)}})),a((function(){var e=p((function(e){D(e.state)}),0);return h.current=function(t){var n=t.state,r=t.prevState;(_.value&&r.isOpen!==n.isOpen&&k(n.isOpen),_.value||!n.isOpen||r.isOpen||E(),n.query!==r.query)&&O.value.core.environment.document.querySelectorAll(".aa-Panel--scrollable").forEach((function(e){0!==e.scrollTop&&(e.scrollTop=0)}));e({state:n})},function(){h.current=void 0}})),a((function(){var e=p((function(){var e=_.value;_.value=O.value.core.environment.matchMedia(O.value.renderer.detachedMediaQuery).matches,e!==_.value?C({}):requestAnimationFrame(E)}),20);return O.value.core.environment.addEventListener("resize",e),function(){O.value.core.environment.removeEventListener("resize",e)}})),a((function(){if(!_.value)return function(){};function e(e){A.value.detachedContainer.classList.toggle("aa-DetachedContainer--modal",e)}function t(t){e(t.matches)}var n=O.value.core.environment.matchMedia(getComputedStyle(O.value.core.environment.document.documentElement).getPropertyValue("--aa-detached-modal-media-query"));e(n.matches);var r=Boolean(n.addEventListener);return r?n.addEventListener("change",t):n.addListener(t),function(){r?n.removeEventListener("change",t):n.removeListener(t)}})),a((function(){return requestAnimationFrame(E),function(){}})),n(n({},w),{},{update:C,destroy:function(){l()}})},e.getAlgoliaFacets=function(e){var t=hr({transformResponse:function(e){return e.facetHits}}),r=e.queries.map((function(e){return n(n({},e),{},{type:"facet"})}));return t(n(n({},e),{},{queries:r}))},e.getAlgoliaResults=Or,Object.defineProperty(e,"__esModule",{value:!0})})); + diff --git a/docs/site_libs/quarto-search/fuse.min.js b/docs/site_libs/quarto-search/fuse.min.js new file mode 100644 index 0000000000000000000000000000000000000000..adc28356e2f591038d4e691cb5b418d7a490bfb1 --- /dev/null +++ b/docs/site_libs/quarto-search/fuse.min.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2022 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(C).length;if(n.has(i))return n.get(i);var o=1/Math.pow(i,.5*e),c=parseFloat(Math.round(o*r)/r);return n.set(i,c),c},clear:function(){n.clear()}}}var $=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?I.getFn:n,o=t.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o;r(this,e),this.norm=E(c,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return o(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,g(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();g(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?I.getFn:r,o=n.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o,a=new $({getFn:i,fieldNormWeight:c});return a.setKeys(e.map(_)),a.setSources(t),a.create(),a}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?I.distance:s,h=t.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=r/e.length;if(l)return f;var d=Math.abs(a-o);return u?f+d/u:d?1:f}function N(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:I.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}var P=32;function W(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?I.location:o,a=i.threshold,s=void 0===a?I.threshold:a,u=i.distance,h=void 0===u?I.distance:u,l=i.includeMatches,f=void 0===l?I.includeMatches:l,d=i.findAllMatches,v=void 0===d?I.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?I.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?I.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?I.ignoreLocation:k;if(r(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?t:t.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){n.chunks.push({pattern:e,alphabet:W(e),startIndex:t})},x=this.pattern.length;if(x>P){for(var w=0,L=x%P,S=x-L;w3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?I.location:i,c=r.distance,a=void 0===c?I.distance:c,s=r.threshold,u=void 0===s?I.threshold:s,h=r.findAllMatches,l=void 0===h?I.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?I.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?I.includeMatches:v,y=r.ignoreLocation,p=void 0===y?I.ignoreLocation:y;if(t.length>P)throw new Error(w(P));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,L=b,S=d>1||g,_=S?Array(M):[];(m=e.indexOf(t,L))>-1;){var O=R(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(O,x),L=m+k,S)for(var j=0;j=z;q-=1){var B=q-1,J=n[e.charAt(B)];if(S&&(_[B]=+!!J),K[q]=(K[q+1]<<1|1)&J,F&&(K[q]|=(A[q+1]|A[q])<<1|1|A[q+1]),K[q]&$&&(C=R(t,{errors:F,currentLocation:B,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=C,(L=B)<=b)break;z=Math.max(1,2*b-L)}}if(R(t,{errors:F+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p})>x)break;A=K}var U={isMatch:L>=0,score:Math.max(.001,C)};if(S){var V=N(_,d);V.length?g&&(U.indices=V):U.isMatch=!1}return U}(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:l}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(f(d),f(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),z=function(){function e(t){r(this,e),this.pattern=t}return o(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return D(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return D(e,this.singleRegex)}}]),e}();function D(e,t){var n=e.match(t);return n?n[1]:null}var K=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),n}(z),q=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),n}(z),B=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),n}(z),J=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),n}(z),U=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),n}(z),V=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),n}(z),G=function(e){a(n,e);var t=l(n);function n(e){var i,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?I.location:c,s=o.threshold,u=void 0===s?I.threshold:s,h=o.distance,l=void 0===h?I.distance:h,f=o.includeMatches,d=void 0===f?I.includeMatches:f,v=o.findAllMatches,g=void 0===v?I.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?I.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?I.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?I.ignoreLocation:M;return r(this,n),(i=t.call(this,e))._bitapSearch=new T(e,{location:a,threshold:u,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),i}return o(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(z),H=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(z),Q=[K,H,B,J,V,U,q,G],X=Q.length,Y=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;function Z(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(Y).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,o=void 0===i?I.isCaseSensitive:i,c=n.includeMatches,a=void 0===c?I.includeMatches:c,s=n.minMatchCharLength,u=void 0===s?I.minMatchCharLength:s,h=n.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=n.findAllMatches,d=void 0===f?I.findAllMatches:f,v=n.location,g=void 0===v?I.location:v,y=n.threshold,p=void 0===y?I.threshold:y,m=n.distance,k=void 0===m?I.distance:m;r(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:l,location:g,threshold:p,distance:k},this.pattern=o?t:t.toLowerCase(),this.query=Z(this.pattern,this.options)}return o(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function ve(e,t){t.score=e.score}function ge(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?I.includeMatches:r,o=n.includeScore,c=void 0===o?I.includeScore:o,a=[];return i&&a.push(de),c&&a.push(ve),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}var ye=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},I),i),this.options.useExtendedSearch,this._keyStore=new S(this.options.keys),this.setCollection(n,o)}return o(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof $))throw new Error("Incorrect 'index' type");this._myIndex=t||F(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){k(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,c=i.includeScore,a=i.shouldSort,s=i.sortFn,u=i.ignoreFieldNorm,h=g(e)?g(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return fe(h,{ignoreFieldNorm:u}),a&&h.sort(s),y(r)&&r>-1&&(h=h.slice(0,r)),ge(h,this._docs,{includeMatches:o,includeScore:c})}},{key:"_searchStringList",value:function(e){var t=re(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(k(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n,i=function e(n){var i=Object.keys(n),o=ue(n);if(!o&&i.length>1&&!se(n))return e(le(n));if(he(n)){var c=o?n[ce]:i[0],a=o?n[ae]:n[c];if(!g(a))throw new Error(x(c));var s={keyId:j(c),pattern:a};return r&&(s.searcher=re(a,t)),s}var u={children:[],operator:i[0]};return i.forEach((function(t){var r=n[t];v(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u};return se(e)||(e=le(e)),i(e)}(e,this.options),r=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}for(var s=[],u=0,h=n.children.length;u1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?I.getFn:n,i=t.fieldNormWeight,o=void 0===i?I.fieldNormWeight:i,c=e.keys,a=e.records,s=new $({getFn:r,fieldNormWeight:o});return s.setKeys(c),s.setIndexRecords(a),s},ye.config=I,function(){ne.push.apply(ne,arguments)}(te),ye},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t(); \ No newline at end of file diff --git a/docs/site_libs/quarto-search/quarto-search.js b/docs/site_libs/quarto-search/quarto-search.js new file mode 100644 index 0000000000000000000000000000000000000000..d788a9581279377b7798e76c8d79e19cac85ad8c --- /dev/null +++ b/docs/site_libs/quarto-search/quarto-search.js @@ -0,0 +1,1290 @@ +const kQueryArg = "q"; +const kResultsArg = "show-results"; + +// If items don't provide a URL, then both the navigator and the onSelect +// function aren't called (and therefore, the default implementation is used) +// +// We're using this sentinel URL to signal to those handlers that this +// item is a more item (along with the type) and can be handled appropriately +const kItemTypeMoreHref = "0767FDFD-0422-4E5A-BC8A-3BE11E5BBA05"; + +window.document.addEventListener("DOMContentLoaded", function (_event) { + // Ensure that search is available on this page. If it isn't, + // should return early and not do anything + var searchEl = window.document.getElementById("quarto-search"); + if (!searchEl) return; + + const { autocomplete } = window["@algolia/autocomplete-js"]; + + let quartoSearchOptions = {}; + let language = {}; + const searchOptionEl = window.document.getElementById( + "quarto-search-options" + ); + if (searchOptionEl) { + const jsonStr = searchOptionEl.textContent; + quartoSearchOptions = JSON.parse(jsonStr); + language = quartoSearchOptions.language; + } + + // note the search mode + if (quartoSearchOptions.type === "overlay") { + searchEl.classList.add("type-overlay"); + } else { + searchEl.classList.add("type-textbox"); + } + + // Used to determine highlighting behavior for this page + // A `q` query param is expected when the user follows a search + // to this page + const currentUrl = new URL(window.location); + const query = currentUrl.searchParams.get(kQueryArg); + const showSearchResults = currentUrl.searchParams.get(kResultsArg); + const mainEl = window.document.querySelector("main"); + + // highlight matches on the page + if (query && mainEl) { + // perform any highlighting + highlight(escapeRegExp(query), mainEl); + + // fix up the URL to remove the q query param + const replacementUrl = new URL(window.location); + replacementUrl.searchParams.delete(kQueryArg); + window.history.replaceState({}, "", replacementUrl); + } + + // function to clear highlighting on the page when the search query changes + // (e.g. if the user edits the query or clears it) + let highlighting = true; + const resetHighlighting = (searchTerm) => { + if (mainEl && highlighting && query && searchTerm !== query) { + clearHighlight(query, mainEl); + highlighting = false; + } + }; + + // Clear search highlighting when the user scrolls sufficiently + const resetFn = () => { + resetHighlighting(""); + window.removeEventListener("quarto-hrChanged", resetFn); + window.removeEventListener("quarto-sectionChanged", resetFn); + }; + + // Register this event after the initial scrolling and settling of events + // on the page + window.addEventListener("quarto-hrChanged", resetFn); + window.addEventListener("quarto-sectionChanged", resetFn); + + // Responsively switch to overlay mode if the search is present on the navbar + // Note that switching the sidebar to overlay mode requires more coordinate (not just + // the media query since we generate different HTML for sidebar overlays than we do + // for sidebar input UI) + const detachedMediaQuery = + quartoSearchOptions.type === "overlay" ? "all" : "(max-width: 991px)"; + + // If configured, include the analytics client to send insights + const plugins = configurePlugins(quartoSearchOptions); + + let lastState = null; + const { setIsOpen, setQuery, setCollections } = autocomplete({ + container: searchEl, + detachedMediaQuery: detachedMediaQuery, + defaultActiveItemId: 0, + panelContainer: "#quarto-search-results", + panelPlacement: quartoSearchOptions["panel-placement"], + debug: false, + openOnFocus: true, + plugins, + classNames: { + form: "d-flex", + }, + placeholder: language["search-text-placeholder"], + translations: { + clearButtonTitle: language["search-clear-button-title"], + detachedCancelButtonText: language["search-detached-cancel-button-title"], + submitButtonTitle: language["search-submit-button-title"], + }, + initialState: { + query, + }, + getItemUrl({ item }) { + return item.href; + }, + onStateChange({ state }) { + // If this is a file URL, note that + + // Perhaps reset highlighting + resetHighlighting(state.query); + + // If the panel just opened, ensure the panel is positioned properly + if (state.isOpen) { + if (lastState && !lastState.isOpen) { + setTimeout(() => { + positionPanel(quartoSearchOptions["panel-placement"]); + }, 150); + } + } + + // Perhaps show the copy link + showCopyLink(state.query, quartoSearchOptions); + + lastState = state; + }, + reshape({ sources, state }) { + return sources.map((source) => { + try { + const items = source.getItems(); + + // Validate the items + validateItems(items); + + // group the items by document + const groupedItems = new Map(); + items.forEach((item) => { + const hrefParts = item.href.split("#"); + const baseHref = hrefParts[0]; + const isDocumentItem = hrefParts.length === 1; + + const items = groupedItems.get(baseHref); + if (!items) { + groupedItems.set(baseHref, [item]); + } else { + // If the href for this item matches the document + // exactly, place this item first as it is the item that represents + // the document itself + if (isDocumentItem) { + items.unshift(item); + } else { + items.push(item); + } + groupedItems.set(baseHref, items); + } + }); + + const reshapedItems = []; + let count = 1; + for (const [_key, value] of groupedItems) { + const firstItem = value[0]; + reshapedItems.push({ + ...firstItem, + type: kItemTypeDoc, + }); + + const collapseMatches = quartoSearchOptions["collapse-after"]; + const collapseCount = + typeof collapseMatches === "number" ? collapseMatches : 1; + + if (value.length > 1) { + const target = `search-more-${count}`; + const isExpanded = + state.context.expanded && + state.context.expanded.includes(target); + + const remainingCount = value.length - collapseCount; + + for (let i = 1; i < value.length; i++) { + if (collapseMatches && i === collapseCount) { + reshapedItems.push({ + target, + title: isExpanded + ? language["search-hide-matches-text"] + : remainingCount === 1 + ? `${remainingCount} ${language["search-more-match-text"]}` + : `${remainingCount} ${language["search-more-matches-text"]}`, + type: kItemTypeMore, + href: kItemTypeMoreHref, + }); + } + + if (isExpanded || !collapseMatches || i < collapseCount) { + reshapedItems.push({ + ...value[i], + type: kItemTypeItem, + target, + }); + } + } + } + count += 1; + } + + return { + ...source, + getItems() { + return reshapedItems; + }, + }; + } catch (error) { + // Some form of error occurred + return { + ...source, + getItems() { + return [ + { + title: error.name || "An Error Occurred While Searching", + text: + error.message || + "An unknown error occurred while attempting to perform the requested search.", + type: kItemTypeError, + }, + ]; + }, + }; + } + }); + }, + navigator: { + navigate({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + window.location.assign(itemUrl); + } + }, + navigateNewTab({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + const windowReference = window.open(itemUrl, "_blank", "noopener"); + if (windowReference) { + windowReference.focus(); + } + } + }, + navigateNewWindow({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + window.open(itemUrl, "_blank", "noopener"); + } + }, + }, + getSources({ state, setContext, setActiveItemId, refresh }) { + return [ + { + sourceId: "documents", + getItemUrl({ item }) { + if (item.href) { + return offsetURL(item.href); + } else { + return undefined; + } + }, + onSelect({ + item, + state, + setContext, + setIsOpen, + setActiveItemId, + refresh, + }) { + if (item.type === kItemTypeMore) { + toggleExpanded(item, state, setContext, setActiveItemId, refresh); + + // Toggle more + setIsOpen(true); + } + }, + getItems({ query }) { + if (query === null || query === "") { + return []; + } + + const limit = quartoSearchOptions.limit; + if (quartoSearchOptions.algolia) { + return algoliaSearch(query, limit, quartoSearchOptions.algolia); + } else { + // Fuse search options + const fuseSearchOptions = { + isCaseSensitive: false, + shouldSort: true, + minMatchCharLength: 2, + limit: limit, + }; + + return readSearchData().then(function (fuse) { + return fuseSearch(query, fuse, fuseSearchOptions); + }); + } + }, + templates: { + noResults({ createElement }) { + const hasQuery = lastState.query; + + return createElement( + "div", + { + class: `quarto-search-no-results${ + hasQuery ? "" : " no-query" + }`, + }, + language["search-no-results-text"] + ); + }, + header({ items, createElement }) { + // count the documents + const count = items.filter((item) => { + return item.type === kItemTypeDoc; + }).length; + + if (count > 0) { + return createElement( + "div", + { class: "search-result-header" }, + `${count} ${language["search-matching-documents-text"]}` + ); + } else { + return createElement( + "div", + { class: "search-result-header-no-results" }, + `` + ); + } + }, + footer({ _items, createElement }) { + if ( + quartoSearchOptions.algolia && + quartoSearchOptions.algolia["show-logo"] + ) { + const libDir = quartoSearchOptions.algolia["libDir"]; + const logo = createElement("img", { + src: offsetURL( + `${libDir}/quarto-search/search-by-algolia.svg` + ), + class: "algolia-search-logo", + }); + return createElement( + "a", + { href: "http://www.algolia.com/" }, + logo + ); + } + }, + + item({ item, createElement }) { + return renderItem( + item, + createElement, + state, + setActiveItemId, + setContext, + refresh, + quartoSearchOptions + ); + }, + }, + }, + ]; + }, + }); + + window.quartoOpenSearch = () => { + setIsOpen(false); + setIsOpen(true); + focusSearchInput(); + }; + + document.addEventListener("keyup", (event) => { + const { key } = event; + const kbds = quartoSearchOptions["keyboard-shortcut"]; + const focusedEl = document.activeElement; + + const isFormElFocused = [ + "input", + "select", + "textarea", + "button", + "option", + ].find((tag) => { + return focusedEl.tagName.toLowerCase() === tag; + }); + + if ( + kbds && + kbds.includes(key) && + !isFormElFocused && + !document.activeElement.isContentEditable + ) { + event.preventDefault(); + window.quartoOpenSearch(); + } + }); + + // Remove the labeleledby attribute since it is pointing + // to a non-existent label + if (quartoSearchOptions.type === "overlay") { + const inputEl = window.document.querySelector( + "#quarto-search .aa-Autocomplete" + ); + if (inputEl) { + inputEl.removeAttribute("aria-labelledby"); + } + } + + function throttle(func, wait) { + let waiting = false; + return function () { + if (!waiting) { + func.apply(this, arguments); + waiting = true; + setTimeout(function () { + waiting = false; + }, wait); + } + }; + } + + // If the main document scrolls dismiss the search results + // (otherwise, since they're floating in the document they can scroll with the document) + window.document.body.onscroll = throttle(() => { + // Only do this if we're not detached + // Bug #7117 + // This will happen when the keyboard is shown on ios (resulting in a scroll) + // which then closed the search UI + if (!window.matchMedia(detachedMediaQuery).matches) { + setIsOpen(false); + } + }, 50); + + if (showSearchResults) { + setIsOpen(true); + focusSearchInput(); + } +}); + +function configurePlugins(quartoSearchOptions) { + const autocompletePlugins = []; + const algoliaOptions = quartoSearchOptions.algolia; + if ( + algoliaOptions && + algoliaOptions["analytics-events"] && + algoliaOptions["search-only-api-key"] && + algoliaOptions["application-id"] + ) { + const apiKey = algoliaOptions["search-only-api-key"]; + const appId = algoliaOptions["application-id"]; + + // Aloglia insights may not be loaded because they require cookie consent + // Use deferred loading so events will start being recorded when/if consent + // is granted. + const algoliaInsightsDeferredPlugin = deferredLoadPlugin(() => { + if ( + window.aa && + window["@algolia/autocomplete-plugin-algolia-insights"] + ) { + window.aa("init", { + appId, + apiKey, + useCookie: true, + }); + + const { createAlgoliaInsightsPlugin } = + window["@algolia/autocomplete-plugin-algolia-insights"]; + // Register the insights client + const algoliaInsightsPlugin = createAlgoliaInsightsPlugin({ + insightsClient: window.aa, + onItemsChange({ insights, insightsEvents }) { + const events = insightsEvents.flatMap((event) => { + // This API limits the number of items per event to 20 + const chunkSize = 20; + const itemChunks = []; + const eventItems = event.items; + for (let i = 0; i < eventItems.length; i += chunkSize) { + itemChunks.push(eventItems.slice(i, i + chunkSize)); + } + // Split the items into multiple events that can be sent + const events = itemChunks.map((items) => { + return { + ...event, + items, + }; + }); + return events; + }); + + for (const event of events) { + insights.viewedObjectIDs(event); + } + }, + }); + return algoliaInsightsPlugin; + } + }); + + // Add the plugin + autocompletePlugins.push(algoliaInsightsDeferredPlugin); + return autocompletePlugins; + } +} + +// For plugins that may not load immediately, create a wrapper +// plugin and forward events and plugin data once the plugin +// is initialized. This is useful for cases like cookie consent +// which may prevent the analytics insights event plugin from initializing +// immediately. +function deferredLoadPlugin(createPlugin) { + let plugin = undefined; + let subscribeObj = undefined; + const wrappedPlugin = () => { + if (!plugin && subscribeObj) { + plugin = createPlugin(); + if (plugin && plugin.subscribe) { + plugin.subscribe(subscribeObj); + } + } + return plugin; + }; + + return { + subscribe: (obj) => { + subscribeObj = obj; + }, + onStateChange: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onStateChange) { + plugin.onStateChange(obj); + } + }, + onSubmit: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onSubmit) { + plugin.onSubmit(obj); + } + }, + onReset: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onReset) { + plugin.onReset(obj); + } + }, + getSources: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.getSources) { + return plugin.getSources(obj); + } else { + return Promise.resolve([]); + } + }, + data: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.data) { + plugin.data(obj); + } + }, + }; +} + +function validateItems(items) { + // Validate the first item + if (items.length > 0) { + const item = items[0]; + const missingFields = []; + if (item.href == undefined) { + missingFields.push("href"); + } + if (!item.title == undefined) { + missingFields.push("title"); + } + if (!item.text == undefined) { + missingFields.push("text"); + } + + if (missingFields.length === 1) { + throw { + name: `Error: Search index is missing the ${missingFields[0]} field.`, + message: `The items being returned for this search do not include all the required fields. Please ensure that your index items include the ${missingFields[0]} field or use index-fields in your _quarto.yml file to specify the field names.`, + }; + } else if (missingFields.length > 1) { + const missingFieldList = missingFields + .map((field) => { + return `${field}`; + }) + .join(", "); + + throw { + name: `Error: Search index is missing the following fields: ${missingFieldList}.`, + message: `The items being returned for this search do not include all the required fields. Please ensure that your index items includes the following fields: ${missingFieldList}, or use index-fields in your _quarto.yml file to specify the field names.`, + }; + } + } +} + +let lastQuery = null; +function showCopyLink(query, options) { + const language = options.language; + lastQuery = query; + // Insert share icon + const inputSuffixEl = window.document.body.querySelector( + ".aa-Form .aa-InputWrapperSuffix" + ); + + if (inputSuffixEl) { + let copyButtonEl = window.document.body.querySelector( + ".aa-Form .aa-InputWrapperSuffix .aa-CopyButton" + ); + + if (copyButtonEl === null) { + copyButtonEl = window.document.createElement("button"); + copyButtonEl.setAttribute("class", "aa-CopyButton"); + copyButtonEl.setAttribute("type", "button"); + copyButtonEl.setAttribute("title", language["search-copy-link-title"]); + copyButtonEl.onmousedown = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const linkIcon = "bi-clipboard"; + const checkIcon = "bi-check2"; + + const shareIconEl = window.document.createElement("i"); + shareIconEl.setAttribute("class", `bi ${linkIcon}`); + copyButtonEl.appendChild(shareIconEl); + inputSuffixEl.prepend(copyButtonEl); + + const clipboard = new window.ClipboardJS(".aa-CopyButton", { + text: function (_trigger) { + const copyUrl = new URL(window.location); + copyUrl.searchParams.set(kQueryArg, lastQuery); + copyUrl.searchParams.set(kResultsArg, "1"); + return copyUrl.toString(); + }, + }); + clipboard.on("success", function (e) { + // Focus the input + + // button target + const button = e.trigger; + const icon = button.querySelector("i.bi"); + + // flash "checked" + icon.classList.add(checkIcon); + icon.classList.remove(linkIcon); + setTimeout(function () { + icon.classList.remove(checkIcon); + icon.classList.add(linkIcon); + }, 1000); + }); + } + + // If there is a query, show the link icon + if (copyButtonEl) { + if (lastQuery && options["copy-button"]) { + copyButtonEl.style.display = "flex"; + } else { + copyButtonEl.style.display = "none"; + } + } + } +} + +/* Search Index Handling */ +// create the index +var fuseIndex = undefined; +var shownWarning = false; + +// fuse index options +const kFuseIndexOptions = { + keys: [ + { name: "title", weight: 20 }, + { name: "section", weight: 20 }, + { name: "text", weight: 10 }, + ], + ignoreLocation: true, + threshold: 0.1, +}; + +async function readSearchData() { + // Initialize the search index on demand + if (fuseIndex === undefined) { + if (window.location.protocol === "file:" && !shownWarning) { + window.alert( + "Search requires JavaScript features disabled when running in file://... URLs. In order to use search, please run this document in a web server." + ); + shownWarning = true; + return; + } + const fuse = new window.Fuse([], kFuseIndexOptions); + + // fetch the main search.json + const response = await fetch(offsetURL("search.json")); + if (response.status == 200) { + return response.json().then(function (searchDocs) { + searchDocs.forEach(function (searchDoc) { + fuse.add(searchDoc); + }); + fuseIndex = fuse; + return fuseIndex; + }); + } else { + return Promise.reject( + new Error( + "Unexpected status from search index request: " + response.status + ) + ); + } + } + + return fuseIndex; +} + +function inputElement() { + return window.document.body.querySelector(".aa-Form .aa-Input"); +} + +function focusSearchInput() { + setTimeout(() => { + const inputEl = inputElement(); + if (inputEl) { + inputEl.focus(); + } + }, 50); +} + +/* Panels */ +const kItemTypeDoc = "document"; +const kItemTypeMore = "document-more"; +const kItemTypeItem = "document-item"; +const kItemTypeError = "error"; + +function renderItem( + item, + createElement, + state, + setActiveItemId, + setContext, + refresh, + quartoSearchOptions +) { + switch (item.type) { + case kItemTypeDoc: + return createDocumentCard( + createElement, + "file-richtext", + item.title, + item.section, + item.text, + item.href, + item.crumbs, + quartoSearchOptions + ); + case kItemTypeMore: + return createMoreCard( + createElement, + item, + state, + setActiveItemId, + setContext, + refresh + ); + case kItemTypeItem: + return createSectionCard( + createElement, + item.section, + item.text, + item.href + ); + case kItemTypeError: + return createErrorCard(createElement, item.title, item.text); + default: + return undefined; + } +} + +function createDocumentCard( + createElement, + icon, + title, + section, + text, + href, + crumbs, + quartoSearchOptions +) { + const iconEl = createElement("i", { + class: `bi bi-${icon} search-result-icon`, + }); + const titleEl = createElement("p", { class: "search-result-title" }, title); + const titleContents = [iconEl, titleEl]; + const showParent = quartoSearchOptions["show-item-context"]; + if (crumbs && showParent) { + let crumbsOut = undefined; + const crumbClz = ["search-result-crumbs"]; + if (showParent === "root") { + crumbsOut = crumbs.length > 1 ? crumbs[0] : undefined; + } else if (showParent === "parent") { + crumbsOut = crumbs.length > 1 ? crumbs[crumbs.length - 2] : undefined; + } else { + crumbsOut = crumbs.length > 1 ? crumbs.join(" > ") : undefined; + crumbClz.push("search-result-crumbs-wrap"); + } + + const crumbEl = createElement( + "p", + { class: crumbClz.join(" ") }, + crumbsOut + ); + titleContents.push(crumbEl); + } + + const titleContainerEl = createElement( + "div", + { class: "search-result-title-container" }, + titleContents + ); + + const textEls = []; + if (section) { + const sectionEl = createElement( + "p", + { class: "search-result-section" }, + section + ); + textEls.push(sectionEl); + } + const descEl = createElement("p", { + class: "search-result-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + textEls.push(descEl); + + const textContainerEl = createElement( + "div", + { class: "search-result-text-container" }, + textEls + ); + + const containerEl = createElement( + "div", + { + class: "search-result-container", + }, + [titleContainerEl, textContainerEl] + ); + + const linkEl = createElement( + "a", + { + href: offsetURL(href), + class: "search-result-link", + }, + containerEl + ); + + const classes = ["search-result-doc", "search-item"]; + if (!section) { + classes.push("document-selectable"); + } + + return createElement( + "div", + { + class: classes.join(" "), + }, + linkEl + ); +} + +function createMoreCard( + createElement, + item, + state, + setActiveItemId, + setContext, + refresh +) { + const moreCardEl = createElement( + "div", + { + class: "search-result-more search-item", + onClick: (e) => { + // Handle expanding the sections by adding the expanded + // section to the list of expanded sections + toggleExpanded(item, state, setContext, setActiveItemId, refresh); + e.stopPropagation(); + }, + }, + item.title + ); + + return moreCardEl; +} + +function toggleExpanded(item, state, setContext, setActiveItemId, refresh) { + const expanded = state.context.expanded || []; + if (expanded.includes(item.target)) { + setContext({ + expanded: expanded.filter((target) => target !== item.target), + }); + } else { + setContext({ expanded: [...expanded, item.target] }); + } + + refresh(); + setActiveItemId(item.__autocomplete_id); +} + +function createSectionCard(createElement, section, text, href) { + const sectionEl = createSection(createElement, section, text, href); + return createElement( + "div", + { + class: "search-result-doc-section search-item", + }, + sectionEl + ); +} + +function createSection(createElement, title, text, href) { + const descEl = createElement("p", { + class: "search-result-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + + const titleEl = createElement("p", { class: "search-result-section" }, title); + const linkEl = createElement( + "a", + { + href: offsetURL(href), + class: "search-result-link", + }, + [titleEl, descEl] + ); + return linkEl; +} + +function createErrorCard(createElement, title, text) { + const descEl = createElement("p", { + class: "search-error-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + + const titleEl = createElement("p", { + class: "search-error-title", + dangerouslySetInnerHTML: { + __html: ` ${title}`, + }, + }); + const errorEl = createElement("div", { class: "search-error" }, [ + titleEl, + descEl, + ]); + return errorEl; +} + +function positionPanel(pos) { + const panelEl = window.document.querySelector( + "#quarto-search-results .aa-Panel" + ); + const inputEl = window.document.querySelector( + "#quarto-search .aa-Autocomplete" + ); + + if (panelEl && inputEl) { + panelEl.style.top = `${Math.round(panelEl.offsetTop)}px`; + if (pos === "start") { + panelEl.style.left = `${Math.round(inputEl.left)}px`; + } else { + panelEl.style.right = `${Math.round(inputEl.offsetRight)}px`; + } + } +} + +/* Highlighting */ +// highlighting functions +function highlightMatch(query, text) { + if (text) { + const start = text.toLowerCase().indexOf(query.toLowerCase()); + if (start !== -1) { + const startMark = ""; + const endMark = ""; + + const end = start + query.length; + text = + text.slice(0, start) + + startMark + + text.slice(start, end) + + endMark + + text.slice(end); + const startInfo = clipStart(text, start); + const endInfo = clipEnd( + text, + startInfo.position + startMark.length + endMark.length + ); + text = + startInfo.prefix + + text.slice(startInfo.position, endInfo.position) + + endInfo.suffix; + + return text; + } else { + return text; + } + } else { + return text; + } +} + +function clipStart(text, pos) { + const clipStart = pos - 50; + if (clipStart < 0) { + // This will just return the start of the string + return { + position: 0, + prefix: "", + }; + } else { + // We're clipping before the start of the string, walk backwards to the first space. + const spacePos = findSpace(text, pos, -1); + return { + position: spacePos.position, + prefix: "", + }; + } +} + +function clipEnd(text, pos) { + const clipEnd = pos + 200; + if (clipEnd > text.length) { + return { + position: text.length, + suffix: "", + }; + } else { + const spacePos = findSpace(text, clipEnd, 1); + return { + position: spacePos.position, + suffix: spacePos.clipped ? "…" : "", + }; + } +} + +function findSpace(text, start, step) { + let stepPos = start; + while (stepPos > -1 && stepPos < text.length) { + const char = text[stepPos]; + if (char === " " || char === "," || char === ":") { + return { + position: step === 1 ? stepPos : stepPos - step, + clipped: stepPos > 1 && stepPos < text.length, + }; + } + stepPos = stepPos + step; + } + + return { + position: stepPos - step, + clipped: false, + }; +} + +// removes highlighting as implemented by the mark tag +function clearHighlight(searchterm, el) { + const childNodes = el.childNodes; + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + if (node.nodeType === Node.ELEMENT_NODE) { + if ( + node.tagName === "MARK" && + node.innerText.toLowerCase() === searchterm.toLowerCase() + ) { + el.replaceChild(document.createTextNode(node.innerText), node); + } else { + clearHighlight(searchterm, node); + } + } + } +} + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +} + +// highlight matches +function highlight(term, el) { + const termRegex = new RegExp(term, "ig"); + const childNodes = el.childNodes; + + // walk back to front avoid mutating elements in front of us + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + + if (node.nodeType === Node.TEXT_NODE) { + // Search text nodes for text to highlight + const text = node.nodeValue; + + let startIndex = 0; + let matchIndex = text.search(termRegex); + if (matchIndex > -1) { + const markFragment = document.createDocumentFragment(); + while (matchIndex > -1) { + const prefix = text.slice(startIndex, matchIndex); + markFragment.appendChild(document.createTextNode(prefix)); + + const mark = document.createElement("mark"); + mark.appendChild( + document.createTextNode( + text.slice(matchIndex, matchIndex + term.length) + ) + ); + markFragment.appendChild(mark); + + startIndex = matchIndex + term.length; + matchIndex = text.slice(startIndex).search(new RegExp(term, "ig")); + if (matchIndex > -1) { + matchIndex = startIndex + matchIndex; + } + } + if (startIndex < text.length) { + markFragment.appendChild( + document.createTextNode(text.slice(startIndex, text.length)) + ); + } + + el.replaceChild(markFragment, node); + } + } else if (node.nodeType === Node.ELEMENT_NODE) { + // recurse through elements + highlight(term, node); + } + } +} + +/* Link Handling */ +// get the offset from this page for a given site root relative url +function offsetURL(url) { + var offset = getMeta("quarto:offset"); + return offset ? offset + url : url; +} + +// read a meta tag value +function getMeta(metaName) { + var metas = window.document.getElementsByTagName("meta"); + for (let i = 0; i < metas.length; i++) { + if (metas[i].getAttribute("name") === metaName) { + return metas[i].getAttribute("content"); + } + } + return ""; +} + +function algoliaSearch(query, limit, algoliaOptions) { + const { getAlgoliaResults } = window["@algolia/autocomplete-preset-algolia"]; + + const applicationId = algoliaOptions["application-id"]; + const searchOnlyApiKey = algoliaOptions["search-only-api-key"]; + const indexName = algoliaOptions["index-name"]; + const indexFields = algoliaOptions["index-fields"]; + const searchClient = window.algoliasearch(applicationId, searchOnlyApiKey); + const searchParams = algoliaOptions["params"]; + const searchAnalytics = !!algoliaOptions["analytics-events"]; + + return getAlgoliaResults({ + searchClient, + queries: [ + { + indexName: indexName, + query, + params: { + hitsPerPage: limit, + clickAnalytics: searchAnalytics, + ...searchParams, + }, + }, + ], + transformResponse: (response) => { + if (!indexFields) { + return response.hits.map((hit) => { + return hit.map((item) => { + return { + ...item, + text: highlightMatch(query, item.text), + }; + }); + }); + } else { + const remappedHits = response.hits.map((hit) => { + return hit.map((item) => { + const newItem = { ...item }; + ["href", "section", "title", "text", "crumbs"].forEach( + (keyName) => { + const mappedName = indexFields[keyName]; + if ( + mappedName && + item[mappedName] !== undefined && + mappedName !== keyName + ) { + newItem[keyName] = item[mappedName]; + delete newItem[mappedName]; + } + } + ); + newItem.text = highlightMatch(query, newItem.text); + return newItem; + }); + }); + return remappedHits; + } + }, + }); +} + +let subSearchTerm = undefined; +let subSearchFuse = undefined; +const kFuseMaxWait = 125; + +async function fuseSearch(query, fuse, fuseOptions) { + let index = fuse; + // Fuse.js using the Bitap algorithm for text matching which runs in + // O(nm) time (no matter the structure of the text). In our case this + // means that long search terms mixed with large index gets very slow + // + // This injects a subIndex that will be used once the terms get long enough + // Usually making this subindex is cheap since there will typically be + // a subset of results matching the existing query + if (subSearchFuse !== undefined && query.startsWith(subSearchTerm)) { + // Use the existing subSearchFuse + index = subSearchFuse; + } else if (subSearchFuse !== undefined) { + // The term changed, discard the existing fuse + subSearchFuse = undefined; + subSearchTerm = undefined; + } + + // Search using the active fuse + const then = performance.now(); + const resultsRaw = await index.search(query, fuseOptions); + const now = performance.now(); + + const results = resultsRaw.map((result) => { + const addParam = (url, name, value) => { + const anchorParts = url.split("#"); + const baseUrl = anchorParts[0]; + const sep = baseUrl.search("\\?") > 0 ? "&" : "?"; + anchorParts[0] = baseUrl + sep + name + "=" + value; + return anchorParts.join("#"); + }; + + return { + title: result.item.title, + section: result.item.section, + href: addParam(result.item.href, kQueryArg, query), + text: highlightMatch(query, result.item.text), + crumbs: result.item.crumbs, + }; + }); + + // If we don't have a subfuse and the query is long enough, go ahead + // and create a subfuse to use for subsequent queries + if ( + now - then > kFuseMaxWait && + subSearchFuse === undefined && + resultsRaw.length < fuseOptions.limit + ) { + subSearchTerm = query; + subSearchFuse = new window.Fuse([], kFuseIndexOptions); + resultsRaw.forEach((rr) => { + subSearchFuse.add(rr.item); + }); + } + return results; +} diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 0000000000000000000000000000000000000000..c367d9366729c4881cc39d8c013a3d309aece0cd --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,127 @@ + + + + https://www.fastht.ml/docs/index.html + 2025-06-05T05:56:08.709Z + + + https://www.fastht.ml/docs/tutorials/index.html + 2025-05-18T23:30:33.077Z + + + https://www.fastht.ml/docs/tutorials/jupyter_and_fasthtml.html + 2025-06-05T05:56:08.861Z + + + https://www.fastht.ml/docs/tutorials/quickstart_for_web_devs.html + 2025-06-05T05:56:08.783Z + + + https://www.fastht.ml/docs/api/components.html + 2025-06-05T05:56:08.793Z + + + https://www.fastht.ml/docs/api/js.html + 2025-06-05T05:56:08.839Z + + + https://www.fastht.ml/docs/api/cli.html + 2025-06-05T05:56:08.764Z + + + https://www.fastht.ml/docs/api/xtend.html + 2025-06-05T05:56:08.766Z + + + https://www.fastht.ml/docs/explains/websockets.html + 2025-06-05T05:56:08.712Z + + + https://www.fastht.ml/docs/explains/explaining_xt_components.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/explains/stripe.html + 2025-06-05T05:56:08.712Z + + + https://www.fastht.ml/docs/explains/faq.html + 2025-06-05T05:56:08.709Z + + + https://www.fastht.ml/docs/unpublished/tutorial_for_web_devs.html + 2025-06-05T05:56:08.709Z + + + https://www.fastht.ml/docs/ref/response_types.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/ref/live_reload.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/ref/best_practice.html + 2025-06-05T06:06:30.727Z + + + https://www.fastht.ml/docs/ref/concise_guide.html + 2025-06-05T05:56:08.713Z + + + https://www.fastht.ml/docs/ref/handlers.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/ref/defining_xt_component.html + 2025-06-05T05:56:08.709Z + + + https://www.fastht.ml/docs/explains/minidataapi.html + 2025-06-05T05:56:08.711Z + + + https://www.fastht.ml/docs/explains/routes.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/explains/background_tasks.html + 2025-06-05T05:56:08.708Z + + + https://www.fastht.ml/docs/explains/oauth.html + 2025-06-05T05:56:08.713Z + + + https://www.fastht.ml/docs/api/core.html + 2025-06-05T05:56:08.840Z + + + https://www.fastht.ml/docs/api/pico.html + 2025-06-05T05:56:08.760Z + + + https://www.fastht.ml/docs/api/oauth.html + 2025-06-05T05:56:08.843Z + + + https://www.fastht.ml/docs/api/svg.html + 2025-06-05T05:56:08.858Z + + + https://www.fastht.ml/docs/api/jupyter.html + 2025-06-05T05:56:08.808Z + + + https://www.fastht.ml/docs/tutorials/e2e.html + 2025-06-05T05:56:08.851Z + + + https://www.fastht.ml/docs/tutorials/best_practice.html + 2025-06-05T06:03:10.577Z + + + https://www.fastht.ml/docs/tutorials/by_example.html + 2025-06-05T05:56:08.854Z + + diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..bc4cd9a704a97d141a10413f91249cf0372519a0 --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,51 @@ +.cell { margin-bottom: 1rem; } +.cell > .sourceCode { margin-bottom: 0; } +.cell-output > pre { margin-bottom: 0; } + +.cell-output > pre, .cell-output > .sourceCode > pre, .cell-output-stdout > pre { + margin-left: 0.8rem; + margin-top: 0; + background: none; + border-left: 2px solid lightsalmon; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.cell-output > .sourceCode { border: none; } + +.cell-output > .sourceCode { + background: none; + margin-top: 0; +} + +div.description { + padding-left: 2px; + padding-top: 5px; + font-size: 1.25rem; + color: rgba(0, 0, 0, 0.60); + opacity: 70%; +} + + +div.sidebar-item-container .active { + background-color: #E8E8FC; + color: #000; +} + +div.sidebar-item-container a { + color: #000; + padding: 4px 6px; + border-radius: 6px; + font-size: 1rem; +} + +li.sidebar-item { + margin-top: 3px; +} + +span.menu-text { + line-height: 20px; +} + +.navbar-container { max-width: 1282px; } + diff --git a/docs/tutorials/best_practice.html b/docs/tutorials/best_practice.html new file mode 100644 index 0000000000000000000000000000000000000000..6890825fbcf6a5c91685c246fafe93ce886e87ab --- /dev/null +++ b/docs/tutorials/best_practice.html @@ -0,0 +1,1020 @@ + + + + + + + + + +FastHTML Best Practices – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    + +
    + + +
    + + + +
    + +
    +
    +

    FastHTML Best Practices

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

    FastHTML applications are different to applications using FastAPI/react, Django, etc. Don’t assume that FastHTML best practices are the same as those for other frameworks. Best practices embody the fast.ai philosophy: remove ceremony, leverage smart defaults, and write code that’s both concise and clear. The following are some particular opportunities that both humans and language models sometimes miss:

    +
    +

    Database Table Creation - Use dataclasses and idempotent patterns

    +

    Before:

    +
    todos = db.t.todos
    +if not todos.exists():
    +todos.create(id=int, task=str, completed=bool, created=str, pk='id')
    +

    After:

    +
    class Todo: id:int; task:str; completed:bool; created:str
    +todos = db.create(Todo)
    +

    FastLite’s create() is idempotent - it creates the table if needed and returns the table object either way. Using a dataclass-style definition is cleaner and more Pythonic. The id field is automatically the primary key.

    +
    +
    +

    Route Naming Conventions - Let function names define routes

    +

    Before:

    +
    @rt("/")
    +def get(): return Titled("Todo List", ...)
    +
    +@rt("/add")
    +def post(task: str): ...
    +

    After:

    +
    @rt
    +def index(): return Titled("Todo List", ...) # Special name for "/"
    +@rt
    +def add(task: str): ... # Function name becomes route
    +

    Use @rt without arguments and let the function name define the route. The special name index maps to /.

    +
    +
    +

    Query Parameters over Path Parameters - Cleaner URL patterns

    +

    Before:

    +
    @rt("/toggle/{todo_id}")
    +def post(todo_id: int): ...
    +# URL: /toggle/123
    +

    After:

    +
    @rt
    +def toggle(id: int): ...
    +# URL: /toggle?id=123
    +

    Query parameters are more idiomatic in FastHTML and avoid duplicating param names in the path.

    +
    +
    +

    Leverage Return Values - Chain operations in one line

    +
    +

    Before:

    +
    @rt
    +def add(task: str):
    +  new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat())
    +  return todo_item(todos[new_todo])
    +
    +@rt
    +def toggle(id: int):
    +  todo = todos[id]
    +  todos.update(completed=not todo.completed, id=id)
    +  return todo_item(todos[id])
    +

    After:

    +
    @rt
    +def add(task: str):
    +  return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat()))
    +
    +@rt
    +def toggle(id: int):
    +  return todo_item(todos.update(completed=not todos[id].completed, id=id))
    +

    Both insert() and update() return the affected object, enabling functional chaining.

    +
    +
    +
    +

    Use .to() for URL Generation - Type-safe route references

    +

    Before:

    +
    hx_post=f"/toggle?id={todo.id}"
    +

    After:

    +
    hx_post=toggle.to(id=todo.id)
    +

    The .to() method generates URLs with type safety and is refactoring-friendly.

    +
    +
    +

    Built-in CSS Frameworks - PicoCSS comes free with fast_app()

    +

    Before:

    +
    style = Style("""
    +.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; }
    +/* ... many more lines ... */
    +""")
    +

    After:

    +
    # Just use semantic HTML - Pico styles it automatically
    +Container(...), Article(...), Card(...), Group(...)
    +

    fast_app() includes PicoCSS by default. Use semantic HTML elements that Pico styles automatically. Use MonsterUI (like shadcn, but for FastHTML) for more complex UI needs.

    +
    +
    +

    Smart Defaults - Titled creates Container, serve() handles main

    +

    Before:

    +
    return Titled("Todo List", Container(...))
    +
    +if __name__ == "__main__":
    +  serve()
    +

    After:

    +
    return Titled("Todo List", ...)  # Container is automatic
    +
    +serve()  # No need for if __name__ guard
    +

    Titled already wraps content in a Container, and serve() handles the main check internally.

    +
    +
    +

    FastHTML Handles Iterables - No unpacking needed for generators

    +

    Before:

    +
    Section(*[todo_item(todo) for todo in all_todos], id="todo-list")
    +

    After:

    +
    Section(map(todo_item, all_todos), id="todo-list")
    +

    FastHTML components accept iterables directly - no need to unpack with *.

    +
    +
    +

    Functional Patterns - Use map() over list comprehensions

    +

    List comprehensions are great, but map() is often cleaner for simple transformations, especially when combined with FastHTML’s iterable handling.

    +
    +
    +

    Minimal Code - Remove comments and unnecessary returns

    +

    Before:

    +
    @rt
    +def delete(id: int):
    +  # Delete from database
    +  todos.delete(id)
    +  # Return empty response
    +  return ""
    +

    After:

    +
    @rt
    +def delete(id: int): todos.delete(id)
    +
      +
    • Skip comments when code is self-documenting
    • +
    • Don’t return empty strings - None is returned by default
    • +
    • Use a single line for a single idea.
    • +
    +
    +
    +

    Use POST for All Mutations

    +

    Before:

    +
    hx_delete=f"/delete?id={todo.id}"
    +

    After:

    +
    hx_post=delete.to(id=todo.id)
    +

    FastHTML routes handle only GET and POST by default. Using only these two verbs is more idiomatic and simpler.

    +
    +
    +

    Modern HTMX Event Syntax

    +

    Before:

    +
    hx_on="htmx:afterRequest: this.reset()"
    +

    After:

    +
    hx_on__after_request="this.reset()"
    +

    This works because:

    +
      +
    • hx-on="event: code" is deprecated; hx-on-event="code" is preferred
    • +
    • FastHTML converts _ to - (so hx_on__after_request becomes hx-on--after-request)
    • +
    • :: in HTMX can be used as a shortcut for :htmx:.
    • +
    • HTMX natively accepts - instead of : (so -htmx- works like :htmx:)
    • +
    • HTMX accepts e.g after-request as an alternative to camelCase afterRequest
    • +
    + + +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/docs/tutorials/best_practice.md b/docs/tutorials/best_practice.md new file mode 100644 index 0000000000000000000000000000000000000000..590ef947bd5ce8ebe480f2bd15f7b7bd74baa476 --- /dev/null +++ b/docs/tutorials/best_practice.md @@ -0,0 +1,262 @@ +# FastHTML Best Practices + + +FastHTML applications are different to applications using FastAPI/react, +Django, etc. Don’t assume that FastHTML best practices are the same as +those for other frameworks. Best practices embody the fast.ai +philosophy: remove ceremony, leverage smart defaults, and write code +that’s both concise and clear. The following are some particular +opportunities that both humans and language models sometimes miss: + +## Database Table Creation - Use dataclasses and idempotent patterns + +**Before:** + +``` python +todos = db.t.todos +if not todos.exists(): +todos.create(id=int, task=str, completed=bool, created=str, pk='id') +``` + +**After:** + +``` python +class Todo: id:int; task:str; completed:bool; created:str +todos = db.create(Todo) +``` + +FastLite’s `create()` is idempotent - it creates the table if needed and +returns the table object either way. Using a dataclass-style definition +is cleaner and more Pythonic. The `id` field is automatically the +primary key. + +## Route Naming Conventions - Let function names define routes + +**Before:** + +``` python +@rt("/") +def get(): return Titled("Todo List", ...) + +@rt("/add") +def post(task: str): ... +``` + +**After:** + +``` python +@rt +def index(): return Titled("Todo List", ...) # Special name for "/" +@rt +def add(task: str): ... # Function name becomes route +``` + +Use `@rt` without arguments and let the function name define the route. +The special name `index` maps to `/`. + +## Query Parameters over Path Parameters - Cleaner URL patterns + +**Before:** + +``` python +@rt("/toggle/{todo_id}") +def post(todo_id: int): ... +# URL: /toggle/123 +``` + +**After:** + +``` python +@rt +def toggle(id: int): ... +# URL: /toggle?id=123 +``` + +Query parameters are more idiomatic in FastHTML and avoid duplicating +param names in the path. + +## Leverage Return Values - Chain operations in one line + +
    + +**Before:** + +``` python +@rt +def add(task: str): + new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat()) + return todo_item(todos[new_todo]) + +@rt +def toggle(id: int): + todo = todos[id] + todos.update(completed=not todo.completed, id=id) + return todo_item(todos[id]) +``` + +**After:** + +``` python +@rt +def add(task: str): + return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat())) + +@rt +def toggle(id: int): + return todo_item(todos.update(completed=not todos[id].completed, id=id)) +``` + +Both `insert()` and `update()` return the affected object, enabling +functional chaining. + +
    + +## Use `.to()` for URL Generation - Type-safe route references + +**Before:** + +``` python +hx_post=f"/toggle?id={todo.id}" +``` + +**After:** + +``` python +hx_post=toggle.to(id=todo.id) +``` + +The `.to()` method generates URLs with type safety and is +refactoring-friendly. + +## Built-in CSS Frameworks - PicoCSS comes free with fast_app() + +**Before:** + +``` python +style = Style(""" +.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; } +/* ... many more lines ... */ +""") +``` + +**After:** + +``` python +# Just use semantic HTML - Pico styles it automatically +Container(...), Article(...), Card(...), Group(...) +``` + +`fast_app()` includes PicoCSS by default. Use semantic HTML elements +that Pico styles automatically. Use MonsterUI (like shadcn, but for +FastHTML) for more complex UI needs. + +## Smart Defaults - Titled creates Container, serve() handles main + +**Before:** + +``` python +return Titled("Todo List", Container(...)) + +if __name__ == "__main__": + serve() +``` + +**After:** + +``` python +return Titled("Todo List", ...) # Container is automatic + +serve() # No need for if __name__ guard +``` + +`Titled` already wraps content in a `Container`, and `serve()` handles +the main check internally. + +## FastHTML Handles Iterables - No unpacking needed for generators + +**Before:** + +``` python +Section(*[todo_item(todo) for todo in all_todos], id="todo-list") +``` + +**After:** + +``` python +Section(map(todo_item, all_todos), id="todo-list") +``` + +FastHTML components accept iterables directly - no need to unpack with +`*`. + +## Functional Patterns - Use map() over list comprehensions + +List comprehensions are great, but `map()` is often cleaner for simple +transformations, especially when combined with FastHTML’s iterable +handling. + +## Minimal Code - Remove comments and unnecessary returns + +**Before:** + +``` python +@rt +def delete(id: int): + # Delete from database + todos.delete(id) + # Return empty response + return "" +``` + +**After:** + +``` python +@rt +def delete(id: int): todos.delete(id) +``` + +- Skip comments when code is self-documenting +- Don’t return empty strings - `None` is returned by default +- Use a single line for a single idea. + +## Use POST for All Mutations + +**Before:** + +``` python +hx_delete=f"/delete?id={todo.id}" +``` + +**After:** + +``` python +hx_post=delete.to(id=todo.id) +``` + +FastHTML routes handle only GET and POST by default. Using only these +two verbs is more idiomatic and simpler. + +## Modern HTMX Event Syntax + +**Before:** + +``` python +hx_on="htmx:afterRequest: this.reset()" +``` + +**After:** + +``` python +hx_on__after_request="this.reset()" +``` + +This works because: + +- `hx-on="event: code"` is deprecated; `hx-on-event="code"` is preferred +- FastHTML converts `_` to `-` (so `hx_on__after_request` becomes + `hx-on--after-request`) +- `::` in HTMX can be used as a shortcut for `:htmx:`. +- HTMX natively accepts `-` instead of `:` (so `-htmx-` works like + `:htmx:`) +- HTMX accepts e.g `after-request` as an alternative to camelCase + `afterRequest` diff --git a/docs/tutorials/by_example.html b/docs/tutorials/by_example.html new file mode 100644 index 0000000000000000000000000000000000000000..85edbebb60dd97d93d34936f205534140d50c49d --- /dev/null +++ b/docs/tutorials/by_example.html @@ -0,0 +1,1939 @@ + + + + + + + + + + +FastHTML By Example – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    + +
    + + +
    + + + +
    + +
    +
    +

    FastHTML By Example

    +
    + +
    +
    + An introduction to FastHTML from the ground up, with four complete examples +
    +
    + + +
    + + + + +
    + + + +
    + + + +

    This tutorial provides an alternate introduction to FastHTML by building out example applications. We also illustrate how to use FastHTML foundations to create custom web apps. Finally, this document serves as minimal context for a LLM to turn it into a FastHTML assistant.

    +

    Let’s get started.

    +
    +

    FastHTML Basics

    +

    FastHTML is just Python. You can install it with pip install python-fasthtml. Extensions/components built for it can likewise be distributed via PyPI or as simple Python files.

    +

    The core usage of FastHTML is to define routes, and then to define what to do at each route. This is similar to the FastAPI web framework (in fact we implemented much of the functionality to match the FastAPI usage examples), but where FastAPI focuses on returning JSON data to build APIs, FastHTML focuses on returning HTML data.

    +

    Here’s a simple FastHTML app that returns a “Hello, World” message:

    +
    +
    from fasthtml.common import FastHTML, serve
    +
    +app = FastHTML()
    +
    +@app.get("/")
    +def home():
    +    return "<h1>Hello, World</h1>"
    +
    +serve()
    +
    +

    To run this app, place it in a file, say app.py, and then run it with python app.py.

    +
    INFO:     Will watch for changes in these directories: ['/home/jonathan/fasthtml-example']
    +INFO:     Uvicorn running on http://127.0.0.1:5001 (Press CTRL+C to quit)
    +INFO:     Started reloader process [871942] using WatchFiles
    +INFO:     Started server process [871945]
    +INFO:     Waiting for application startup.
    +INFO:     Application startup complete.
    +

    If you navigate to http://127.0.0.1:5001 in a browser, you’ll see your “Hello, World”. If you edit the app.py file and save it, the server will reload and you’ll see the updated message when you refresh the page in your browser.

    +
    +
    +

    Constructing HTML

    +

    Notice we wrote some HTML in the previous example. We don’t want to do that! Some web frameworks require that you learn HTML, CSS, JavaScript AND some templating language AND python. We want to do as much as possible with just one language. Fortunately, the Python module fastcore.xml has all we need for constructing HTML from Python, and FastHTML includes all the tags you need to get started. For example:

    +
    +
    from fasthtml.common import *
    +page = Html(
    +    Head(Title('Some page')),
    +    Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass')))
    +print(to_xml(page))
    +
    +
    <!doctype html></!doctype>
    +
    +<html>
    +  <head>
    +    <title>Some page</title>
    +  </head>
    +  <body>
    +    <div class="myclass">
    +Some text, 
    +      <a href="https://example.com">A link</a>
    +      <img src="https://placehold.co/200">
    +    </div>
    +  </body>
    +</html>
    +
    +
    +
    +
    +
    show(page)
    +
    + + + + + Some page + + +
    +Some text, + A link + +
    + + +
    +
    +

    If that import * worries you, you can always import only the tags you need.

    +

    FastHTML is smart enough to know about fastcore.xml, and so you don’t need to use the to_xml function to convert your FT objects to HTML. You can just return them as you would any other Python object. For example, if we modify our previous example to use fastcore.xml, we can return an FT object directly:

    +
    +
    from fasthtml.common import *
    +app = FastHTML()
    +
    +@app.get("/")
    +def home():
    +    page = Html(
    +        Head(Title('Some page')),
    +        Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass')))
    +    return page
    +
    +serve()
    +
    +

    This will render the HTML in the browser.

    +

    For debugging, you can right-click on the rendered HTML in the browser and select “Inspect” to see the underlying HTML that was generated. There you’ll also find the ‘network’ tab, which shows you the requests that were made to render the page. Refresh and look for the request to 127.0.0.1 - and you’ll see it’s just a GET request to /, and the response body is the HTML you just returned.

    +
    +
    +
    + +
    +
    +Live Reloading +
    +
    +
    +

    You can also enable live reloading so you don’t have to manually refresh your browser to view updates.

    +
    +
    +

    You can also use Starlette’s TestClient to try it out in a notebook:

    +
    +
    from starlette.testclient import TestClient
    +client = TestClient(app)
    +r = client.get("/")
    +print(r.text)
    +
    +
    <html>
    +  <head><title>Some page</title>
    +</head>
    +  <body><div class="myclass">
    +Some text, 
    +  <a href="https://example.com">A link</a>
    +  <img src="https://placehold.co/200">
    +</div>
    +</body>
    +</html>
    +
    +
    +
    +

    FastHTML wraps things in an Html tag if you don’t do it yourself (unless the request comes from htmx, in which case you get the element directly). See FT objects and HTML for more on creating custom components or adding HTML rendering to existing Python objects. To give the page a non-default title, return a Title before your main content:

    +
    +
    app = FastHTML()
    +
    +@app.get("/")
    +def home():
    +    return Title("Page Demo"), Div(H1('Hello, World'), P('Some text'), P('Some more text'))
    +
    +client = TestClient(app)
    +print(client.get("/").text)
    +
    +
    <!doctype html></!doctype>
    +
    +<html>
    +  <head>
    +    <title>Page Demo</title>
    +    <meta charset="utf-8"></meta>
    +    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"></meta>
    +    <script src="https://unpkg.com/htmx.org@next/dist/htmx.min.js"></script>
    +    <script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@1.3.0/surreal.js"></script>
    +    <script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>
    +  </head>
    +  <body>
    +<div>
    +  <h1>Hello, World</h1>
    +  <p>Some text</p>
    +  <p>Some more text</p>
    +</div>
    +  </body>
    +</html>
    +
    +
    +
    +

    We’ll use this pattern often in the examples to follow.

    +
    +
    +

    Defining Routes

    +

    The HTTP protocol defines a number of methods (‘verbs’) to send requests to a server. The most common are GET, POST, PUT, DELETE, and HEAD. We saw ‘GET’ in action before - when you navigate to a URL, you’re making a GET request to that URL. We can do different things on a route for different HTTP methods. For example:

    +
    @app.route("/", methods='get')
    +def home():
    +    return H1('Hello, World')
    +
    +@app.route("/", methods=['post', 'put'])
    +def post_or_put():
    +    return "got a POST or PUT request"
    +

    This says that when someone navigates to the root URL “/” (i.e. sends a GET request), they will see the big “Hello, World” heading. When someone submits a POST or PUT request to the same URL, the server should return the string “got a post or put request”.

    +
    +
    +
    + +
    +
    +Test the POST request +
    +
    +
    +

    You can test the POST request with curl -X POST http://127.0.0.1:8000 -d "some data". This sends some data to the server, you should see the response “got a post or put request” printed in the terminal.

    +
    +
    +

    There are a few other ways you can specify the route+method - FastHTML has .get, .post, etc. as shorthand for route(..., methods=['get']), etc.

    +
    +
    @app.get("/")
    +def my_function():
    +    return "Hello World from a GET request"
    +
    +

    Or you can use the @rt decorator without a method but specify the method with the name of the function. For example:

    +
    +
    rt = app.route
    +
    +@rt("/")
    +def post():
    +    return "Hello World from a POST request"
    +
    +
    +
    client.post("/").text
    +
    +
    'Hello World from a POST request'
    +
    +
    +

    You’re welcome to pick whichever style you prefer. Using routes lets you show different content on different pages - ‘/home’, ‘/about’ and so on. You can also respond differently to different kinds of requests to the same route, as shown above. You can also pass data via the route:

    +
    + +
    +
    +
    +
    @app.get("/greet/{nm}")
    +def greet(nm:str):
    +    return f"Good day to you, {nm}!"
    +
    +client.get("/greet/Dave").text
    +
    +
    'Good day to you, Dave!'
    +
    +
    +
    +
    +
    +
    @rt("/greet/{nm}")
    +def get(nm:str):
    +    return f"Good day to you, {nm}!"
    +
    +client.get("/greet/Dave").text
    +
    +
    'Good day to you, Dave!'
    +
    +
    +
    +
    +
    +

    More on this in the More on Routing and Request Parameters section, which goes deeper into the different ways to get information from a request.

    +
    +
    +

    Styling Basics

    +

    Plain HTML probably isn’t quite what you imagine when you visualize your beautiful web app. CSS is the go-to language for styling HTML. But again, we don’t want to learn extra languages unless we absolutely have to! Fortunately, there are ways to get much more visually appealing sites by relying on the hard work of others, using existing CSS libraries. One of our favourites is PicoCSS. A common way to add CSS files to web pages is to use a <link> tag inside your HTML header, like this:

    +
    <header>
    +    ...
    +    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css">
    +</header>
    +

    For convenience, FastHTML already defines a Pico component for you with picolink:

    +
    +
    print(to_xml(picolink))
    +
    +
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css">
    +
    +<style>:root { --pico-font-size: 100%; }</style>
    +
    +
    +
    +
    +
    +
    + +
    +
    +Note +
    +
    +
    +

    picolink also includes a <style> tag, as we found that setting the font-size to 100% to be a good default. We show you how to override this below.

    +
    +
    +

    Since we typically want CSS styling on all pages of our app, FastHTML lets you define a shared HTML header with the hdrs argument as shown below:

    +
    +
    from fasthtml.common import *
    +1css = Style(':root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}')
    +2app = FastHTML(hdrs=(picolink, css))
    +
    +@app.route("/")
    +def get():
    +    return (Title("Hello World"), 
    +3            Main(H1('Hello, World'), cls="container"))
    +
    +
    +
    1
    +
    +Custom styling to override the pico defaults +
    +
    2
    +
    +Define shared headers for all pages +
    +
    3
    +
    +As per the pico docs, we put all of our content inside a <main> tag with a class of container: +
    +
    +
    +
    +
    +
    +
    + +
    +
    +Returning Tuples +
    +
    +
    +

    We’re returning a tuple here (a title and the main page). Returning a tuple, list, FT object, or an object with a __ft__ method tells FastHTML to turn the main body into a full HTML page that includes the headers (including the pico link and our custom css) which we passed in. This only occurs if the request isn’t from HTMX (for HTMX requests we need only return the rendered components).

    +
    +
    +

    You can check out the Pico examples page to see how different elements will look. If everything is working, the page should now render nice text with our custom font, and it should respect the user’s light/dark mode preferences too.

    +

    If you want to override the default styles or add more custom CSS, you can do so by adding a <style> tag to the headers as shown above. So you are allowed to write CSS to your heart’s content - we just want to make sure you don’t necessarily have to! Later on we’ll see examples using other component libraries and tailwind css to do more fancy styling things, along with tips to get an LLM to write all those fiddly bits so you don’t have to.

    +
    +
    +

    Web Page -> Web App

    +

    Showing content is all well and good, but we typically expect a bit more interactivity from something calling itself a web app! So, let’s add a few different pages, and use a form to let users add messages to a list:

    +
    +
    app = FastHTML()
    +messages = ["This is a message, which will get rendered as a paragraph"]
    +
    +@app.get("/")
    +def home():
    +    return Main(H1('Messages'), 
    +                *[P(msg) for msg in messages],
    +                A("Link to Page 2 (to add messages)", href="/page2"))
    +
    +@app.get("/page2")
    +def page2():
    +    return Main(P("Add a message with the form below:"),
    +                Form(Input(type="text", name="data"),
    +                     Button("Submit"),
    +                     action="/", method="post"))
    +
    +@app.post("/")
    +def add_message(data:str):
    +    messages.append(data)
    +    return home()
    +
    +

    We re-render the entire homepage to show the newly added message. This is fine, but modern web apps often don’t re-render the entire page, they just update a part of the page. In fact even very complicated applications are often implemented as ‘Single Page Apps’ (SPAs). This is where HTMX comes in.

    +
    +
    +

    HTMX

    +

    HTMX addresses some key limitations of HTML. In vanilla HTML, links can trigger a GET request to show a new page, and forms can send requests containing data to the server. A lot of ‘Web 1.0’ design revolved around ways to use these to do everything we wanted. But why should only some elements be allowed to trigger requests? And why should we refresh the entire page with the result each time one does? HTMX extends HTML to allow us to trigger requests from any element on all kinds of events, and to update a part of the page without refreshing the entire page. It’s a powerful tool for building modern web apps.

    +

    It does this by adding attributes to HTML tags to make them do things. For example, here’s a page with a counter and a button that increments it:

    +
    +
    app = FastHTML()
    +
    +count = 0
    +
    +@app.get("/")
    +def home():
    +    return Title("Count Demo"), Main(
    +        H1("Count Demo"),
    +        P(f"Count is set to {count}", id="count"),
    +        Button("Increment", hx_post="/increment", hx_target="#count", hx_swap="innerHTML")
    +    )
    +
    +@app.post("/increment")
    +def increment():
    +    print("incrementing")
    +    global count
    +    count += 1
    +    return f"Count is set to {count}"
    +
    +

    The button triggers a POST request to /increment (since we set hx_post="/increment"), which increments the count and returns the new count. The hx_target attribute tells HTMX where to put the result. If no target is specified it replaces the element that triggered the request. The hx_swap attribute specifies how it adds the result to the page. Useful options are:

    +
      +
    • innerHTML: Replace the target element’s content with the result.
    • +
    • outerHTML: Replace the target element with the result.
    • +
    • beforebegin: Insert the result before the target element.
    • +
    • beforeend: Insert the result inside the target element, after its last child.
    • +
    • afterbegin: Insert the result inside the target element, before its first child.
    • +
    • afterend: Insert the result after the target element.
    • +
    +

    You can also use an hx_swap of delete to delete the target element regardless of response, or of none to do nothing.

    +

    By default, requests are triggered by the “natural” event of an element - click in the case of a button (and most other elements). You can also specify different triggers, along with various modifiers - see the HTMX docs for more.

    +

    This pattern of having elements trigger requests that modify or replace other elements is a key part of the HTMX philosophy. It takes a little getting used to, but once mastered it is extremely powerful.

    +
    +

    Replacing Elements Besides the Target

    +

    Sometimes having a single target is not enough, and we’d like to specify some additional elements to update or remove. In these cases, returning elements with an id that matches the element to be replaced and hx_swap_oob='true' will replace those elements too. We’ll use this in the next example to clear an input field when we submit a form.

    +
    +
    +
    +

    Full Example #1 - ToDo App

    +

    The canonical demo web app! A TODO list. Rather than create yet another variant for this tutorial, we recommend starting with this video tutorial from Jeremy:

    +
    +
    +
    +

    +
    image.png
    +
    +
    +

    We’ve made a number of variants of this app - so in addition to the version shown in the video you can browse this series of examples with increasing complexity, the heavily-commented “idiomatic” version here, and the example linked from the FastHTML homepage.

    +
    +
    +

    Full Example #2 - Image Generation App

    +

    Let’s create an image generation app. We’d like to wrap a text-to-image model in a nice UI, where the user can type in a prompt and see a generated image appear. We’ll use a model hosted by Replicate to actually generate the images. Let’s start with the homepage, with a form to submit prompts and a div to hold the generated images:

    +
    # Main page
    +@app.get("/")
    +def get():
    +    inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt")
    +    add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list', hx_swap="afterbegin")
    +    gen_list = Div(id='gen-list')
    +    return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')
    +

    Submitting the form will trigger a POST request to /, so next we need to generate an image and add it to the list. One problem: generating images is slow! We’ll start the generation in a separate thread, but this now surfaces a different problem: we want to update the UI right away, but our image will only be ready a few seconds later. This is a common pattern - think about how often you see a loading spinner online. We need a way to return a temporary bit of UI which will eventually be replaced by the final image. Here’s how we might do this:

    +
    def generation_preview(id):
    +    if os.path.exists(f"gens/{id}.png"):
    +        return Div(Img(src=f"/gens/{id}.png"), id=f'gen-{id}')
    +    else:
    +        return Div("Generating...", id=f'gen-{id}', 
    +                   hx_post=f"/generations/{id}",
    +                   hx_trigger='every 1s', hx_swap='outerHTML')
    +    
    +@app.post("/generations/{id}")
    +def get(id:int): return generation_preview(id)
    +
    +@app.post("/")
    +def post(prompt:str):
    +    id = len(generations)
    +    generate_and_save(prompt, id)
    +    generations.append(prompt)
    +    clear_input =  Input(id="new-prompt", name="prompt", placeholder="Enter a prompt", hx_swap_oob='true')
    +    return generation_preview(id), clear_input
    +
    +@threaded
    +def generate_and_save(prompt, id): ... 
    +

    The form sends the prompt to the / route, which starts the generation in a separate thread then returns two things:

    +
      +
    • A generation preview element that will be added to the top of the gen-list div (since that is the target_id of the form which triggered the request)
    • +
    • An input field that will replace the form’s input field (that has the same id), using the hx_swap_oob=‘true’ trick. This clears the prompt field so the user can type another prompt.
    • +
    +

    The generation preview first returns a temporary “Generating…” message, which polls the /generations/{id} route every second. This is done by setting hx_post to the route and hx_trigger to ‘every 1s’. The /generations/{id} route returns the preview element every second until the image is ready, at which point it returns the final image. Since the final image replaces the temporary one (hx_swap=‘outerHTML’), the polling stops running and the generation preview is now complete.

    +

    This works nicely - the user can submit several prompts without having to wait for the first one to generate, and as the images become available they are added to the list. You can see the full code of this version here.

    +
    +

    Again, with Style

    +

    The app is functional, but can be improved. The next version adds more stylish generation previews, lays out the images in a grid layout that is responsive to different screen sizes, and adds a database to track generations and make them persistent. The database part is very similar to the todo list example, so let’s just quickly look at how we add the nice grid layout. This is what the result looks like:

    +
    +
    +

    +
    image.png
    +
    +
    +

    Step one was looking around for existing components. The Pico CSS library we’ve been using has a rudimentary grid but recommends using an alternative layout system. One of the options listed was Flexbox.

    +

    To use Flexbox you create a “row” with one or more elements. You can specify how wide things should be with a specific syntax in the class name. For example, col-xs-12 means a box that will take up 12 columns (out of 12 total) of the row on extra small screens, col-sm-6 means a column that will take up 6 columns of the row on small screens, and so on. So if you want four columns on large screens you would use col-lg-3 for each item (i.e. each item is using 3 columns out of 12).

    +
    <div class="row">
    +    <div class="col-xs-12">
    +        <div class="box">This takes up the full width</div>
    +    </div>
    +</div>
    +

    This was non-intuitive to me. Thankfully ChatGPT et al know web stuff quite well, and we can also experiment in a notebook to test things out:

    +
    +
    grid = Html(
    +    Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css"),
    +    Div(
    +        Div(Div("This takes up the full width", cls="box", style="background-color: #800000;"), cls="col-xs-12"),
    +        Div(Div("This takes up half", cls="box", style="background-color: #008000;"), cls="col-xs-6"),
    +        Div(Div("This takes up half", cls="box", style="background-color: #0000B0;"), cls="col-xs-6"),
    +        cls="row", style="color: #fff;"
    +    )
    +)
    +show(grid)
    +
    + + + + +
    +
    +
    This takes up the full width
    +
    +
    +
    This takes up half
    +
    +
    +
    This takes up half
    +
    +
    + +
    +
    +

    Aside: when in doubt with CSS stuff, add a background color or a border so you can see what’s happening!

    +

    Translating this into our app, we have a new homepage with a div (class="row") to store the generated images / previews, and a generation_preview function that returns boxes with the appropriate classes and styles to make them appear in the grid. I chose a layout with different numbers of columns for different screen sizes, but you could also just specify the col-xs class if you wanted the same layout on all devices.

    +
    gridlink = Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css")
    +app = FastHTML(hdrs=(picolink, gridlink))
    +
    +# Main page
    +@app.get("/")
    +def get():
    +    inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt")
    +    add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list', hx_swap="afterbegin")
    +    gen_containers = [generation_preview(g) for g in gens(limit=10)] # Start with last 10
    +    gen_list = Div(*gen_containers[::-1], id='gen-list', cls="row") # flexbox container: class = row
    +    return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')
    +
    +# Show the image (if available) and prompt for a generation
    +def generation_preview(g):
    +    grid_cls = "box col-xs-12 col-sm-6 col-md-4 col-lg-3"
    +    image_path = f"{g.folder}/{g.id}.png"
    +    if os.path.exists(image_path):
    +        return Div(Card(
    +                       Img(src=image_path, alt="Card image", cls="card-img-top"),
    +                       Div(P(B("Prompt: "), g.prompt, cls="card-text"),cls="card-body"),
    +                   ), id=f'gen-{g.id}', cls=grid_cls)
    +    return Div(f"Generating gen {g.id} with prompt {g.prompt}", 
    +            id=f'gen-{g.id}', hx_get=f"/gens/{g.id}", 
    +            hx_trigger="every 2s", hx_swap="outerHTML", cls=grid_cls)
    +

    You can see the final result in main.py in the image_app_simple example directory, along with info on deploying it (tl;dr don’t!). We’ve also deployed a version that only shows your generations (tied to browser session) and has a credit system to save our bank accounts. You can access that here. Now for the next question: how do we keep track of different users?

    +
    +
    +

    Again, with Sessions

    +

    At the moment everyone sees all images! How do we keep some sort of unique identifier tied to a user? Before going all the way to setting up users, login pages etc., let’s look at a way to at least limit generations to the user’s session. You could do this manually with cookies. For convenience and security, fasthtml (via Starlette) has a special mechanism for storing small amounts of data in the user’s browser via the session argument to your route. This acts like a dictionary and you can set and get values from it. For example, here we look for a session_id key, and if it doesn’t exist we generate a new one:

    +
    @app.get("/")
    +def get(session):
    +    if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())
    +    return H1(f"Session ID: {session['session_id']}")
    +

    Refresh the page a few times - you’ll notice that the session ID remains the same. If you clear your browsing data, you’ll get a new session ID. And if you load the page in a different browser (but not a different tab), you’ll get a new session ID. This will persist within the current browser, letting us use it as a key for our generations. As a bonus, someone can’t spoof this session id by passing it in another way (for example, sending a query parameter). Behind the scenes, the data is stored in a browser cookie but it is signed with a secret key that stops the user or anyone nefarious from being able to tamper with it. The cookie is decoded back into a dictionary by something called a middleware function, which we won’t cover here. All you need to know is that we can use this to store bits of state in the user’s browser.

    +

    In the image app example, we can add a session_id column to our database, and modify our homepage like so:

    +
    @app.get("/")
    +def get(session):
    +    if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())
    +    inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt")
    +    add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list', hx_swap="afterbegin")
    +    gen_containers = [generation_preview(g) for g in gens(limit=10, where=f"session_id == '{session['session_id']}'")]
    +    ...
    +

    So we check if the session id exists in the session, add one if not, and then limit the generations shown to only those tied to this session id. We filter the database with a where clause - see [TODO link Jeremy’s example for a more reliable way to do this]. The only other change we need to make is to store the session id in the database when a generation is made. You can check out this version here. You could instead write this app without relying on a database at all - simply storing the filenames of the generated images in the session, for example. But this more general approach of linking some kind of unique session identifier to users or data in our tables is a useful general pattern for more complex examples.

    +
    +
    +

    Again, with Credits!

    +

    Generating images with replicate costs money. So next let’s add a pool of credits that get used up whenever anyone generates an image. To recover our lost funds, we’ll also set up a payment system so that generous users can buy more credits for everyone. You could modify this to let users buy credits tied to their session ID, but at that point you risk having angry customers losing their money after wiping their browser history, and should consider setting up proper account management :)

    +

    Taking payments with Stripe is intimidating but very doable. Here’s a tutorial that shows the general principle using Flask. As with other popular tasks in the web-dev world, ChatGPT knows a lot about Stripe - but you should exercise extra caution when writing code that handles money!

    +

    For the finished example we add the bare minimum:

    +
      +
    • A way to create a Stripe checkout session and redirect the user to the session URL
    • +
    • ‘Success’ and ‘Cancel’ routes to handle the result of the checkout
    • +
    • A route that listens for a webhook from Stripe to update the number of credits when a payment is made.
    • +
    +

    In a typical application you’ll want to keep track of which users make payments, catch other kinds of stripe events and so on. This example is more a ‘this is possible, do your own research’ than ‘this is how you do it’. But hopefully it does illustrate the key idea: there is no magic here. Stripe (and many other technologies) relies on sending users to different routes and shuttling data back and forth in requests. And we know how to do that!

    +
    +
    +
    +

    More on Routing and Request Parameters

    +

    There are a number of ways information can be passed to the server. When you specify arguments to a route, FastHTML will search the request for values with the same name, and convert them to the correct type. In order, it searches

    +
      +
    • The path parameters
    • +
    • The query parameters
    • +
    • The cookies
    • +
    • The headers
    • +
    • The session
    • +
    • Form data
    • +
    +

    There are also a few special arguments

    +
      +
    • request (or any prefix like req): gets the raw Starlette Request object
    • +
    • session (or any prefix like sess): gets the session object
    • +
    • auth
    • +
    • htmx
    • +
    • app
    • +
    +

    In this section let’s quickly look at some of these in action.

    +
    +
    from fasthtml.common import *
    +from starlette.testclient import TestClient
    +
    +app = FastHTML()
    +cli = TestClient(app)
    +
    +

    Part of the route (path parameters):

    +
    +
    @app.get('/user/{nm}')
    +def _(nm:str): return f"Good day to you, {nm}!"
    +
    +cli.get('/user/jph').text
    +
    +
    'Good day to you, jph!'
    +
    +
    +

    Matching with a regex:

    +
    +
    reg_re_param("imgext", "ico|gif|jpg|jpeg|webm")
    +
    +@app.get(r'/static/{path:path}/{fn}.{ext:imgext}')
    +def get_img(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"
    +
    +cli.get('/static/foo/jph.ico').text
    +
    +
    'Getting jph.ico from /foo/'
    +
    +
    +

    Using an enum (try using a string that isn’t in the enum):

    +
    +
    ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet")
    +
    +@app.get("/models/{nm}")
    +def model(nm:ModelName): return nm
    +
    +print(cli.get('/models/alexnet').text)
    +
    +
    alexnet
    +
    +
    +

    Casting to a Path:

    +
    +
    @app.get("/files/{path}")
    +def txt(path: Path): return path.with_suffix('.txt')
    +
    +print(cli.get('/files/foo').text)
    +
    +
    foo.txt
    +
    +
    +

    An integer with a default value:

    +
    +
    fake_db = [{"name": "Foo"}, {"name": "Bar"}]
    +
    +@app.get("/items/")
    +def read_item(idx: int = 0): return fake_db[idx]
    +
    +print(cli.get('/items/?idx=1').text)
    +
    +
    {"name":"Bar"}
    +
    +
    +
    +
    # Equivalent to `/items/?idx=0`.
    +print(cli.get('/items/').text)
    +
    +
    {"name":"Foo"}
    +
    +
    +

    Boolean values (takes anything “truthy” or “falsy”):

    +
    +
    @app.get("/booly/")
    +def booly(coming:bool=True): return 'Coming' if coming else 'Not coming'
    +
    +print(cli.get('/booly/?coming=true').text)
    +
    +
    Coming
    +
    +
    +
    +
    print(cli.get('/booly/?coming=no').text)
    +
    +
    Not coming
    +
    +
    +

    Getting dates:

    +
    +
    @app.get("/datie/")
    +def datie(d:parsed_date): return d
    +
    +date_str = "17th of May, 2024, 2p"
    +print(cli.get(f'/datie/?d={date_str}').text)
    +
    +
    2024-05-17 14:00:00
    +
    +
    +

    Matching a dataclass:

    +
    +
    from dataclasses import dataclass, asdict
    +
    +@dataclass
    +class Bodie:
    +    a:int;b:str
    +
    +@app.route("/bodie/{nm}")
    +def post(nm:str, data:Bodie):
    +    res = asdict(data)
    +    res['nm'] = nm
    +    return res
    +
    +cli.post('/bodie/me', data=dict(a=1, b='foo')).text
    +
    +
    '{"a":1,"b":"foo","nm":"me"}'
    +
    +
    +
    +

    Cookies

    +

    Cookies can be set via a Starlette Response object, and can be read back by specifying the name:

    +
    +
    from datetime import datetime
    +
    +@app.get("/setcookie")
    +def setc(req):
    +    now = datetime.now()
    +    res = Response(f'Set to {now}')
    +    res.set_cookie('now', str(now))
    +    return res
    +
    +cli.get('/setcookie').text
    +
    +
    'Set to 2024-07-20 23:14:54.364793'
    +
    +
    +
    +
    @app.get("/getcookie")
    +def getc(now:parsed_date): return f'Cookie was set at time {now.time()}'
    +
    +cli.get('/getcookie').text
    +
    +
    'Cookie was set at time 23:14:54.364793'
    +
    +
    +
    +
    +

    User Agent and HX-Request

    +

    An argument of user_agent will match the header User-Agent. This holds for special headers like HX-Request (used by HTMX to signal when a request comes from an HTMX request) - the general pattern is that “-” is replaced with “_” and strings are turned to lowercase.

    +
    +
    @app.get("/ua")
    +async def ua(user_agent:str): return user_agent
    +
    +cli.get('/ua', headers={'User-Agent':'FastHTML'}).text
    +
    +
    'FastHTML'
    +
    +
    +
    +
    @app.get("/hxtest")
    +def hxtest(htmx): return htmx.request
    +
    +cli.get('/hxtest', headers={'HX-Request':'1'}).text
    +
    +
    '1'
    +
    +
    +
    +
    +

    Starlette Requests

    +

    If you add an argument called request(or any prefix of that, for example req) it will be populated with the Starlette Request object. This is useful if you want to do your own processing manually. For example, although FastHTML will parse forms for you, you could instead get form data like so:

    +
    @app.get("/form")
    +async def form(request:Request):
    +    form_data = await request.form()
    +    a = form_data.get('a')
    +

    See the Starlette docs for more information on the Request object.

    +
    +
    +

    Starlette Responses

    +

    You can return a Starlette Response object from a route to control the response. For example:

    +
    @app.get("/redirect")
    +def redirect():
    +    return RedirectResponse(url="/")
    +

    We used this to set cookies in the previous example. See the Starlette docs for more information on the Response object.

    +
    +
    +

    Static Files

    +

    We often want to serve static files like images. This is easily done! For common file types (images, CSS etc) we can create a route that returns a Starlette FileResponse like so:

    +
    # For images, CSS, etc.
    +@app.get("/{fname:path}.{ext:static}")
    +def static(fname: str, ext: str):
    +  return FileResponse(f'{fname}.{ext}')
    +

    You can customize it to suit your needs (for example, only serving files in a certain directory). You’ll notice some variant of this route in all our complete examples - even for apps with no static files the browser will typically request a /favicon.ico file, for example, and as the astute among you will have noticed this has sparked a bit of competition between Johno and Jeremy regarding which country flag should serve as the default!

    +
    +
    +

    WebSockets

    +

    For certain applications such as multiplayer games, websockets can be a powerful feature. Luckily HTMX and FastHTML has you covered! Simply specify that you wish to include the websocket header extension from HTMX:

    +
    app = FastHTML(exts='ws')
    +rt = app.route
    +

    With that, you are now able to specify the different websocket specific HTMX goodies. For example, say we have a website we want to setup a websocket, you can simply:

    +
    def mk_inp(): return Input(id='msg')
    +
    +@rt('/')
    +async def get(request):
    +    cts = Div(
    +        Div(id='notifications'),
    +        Form(mk_inp(), id='form', ws_send=True),
    +        hx_ext='ws', ws_connect='/ws')
    +    return Titled('Websocket Test', cts)
    +

    And this will setup a connection on the route /ws along with a form that will send a message to the websocket whenever the form is submitted. Let’s go ahead and handle this route:

    +
    @app.ws('/ws')
    +async def ws(msg:str, send):
    +    await send(Div('Hello ' + msg, id="notifications"))
    +    await sleep(2)
    +    return Div('Goodbye ' + msg, id="notifications"), mk_inp()
    +

    One thing you might have noticed is a lack of target id for our websocket trigger for swapping HTML content. This is because HTMX always swaps content with websockets with Out of Band Swaps. Therefore, HTMX will look for the id in the returned HTML content from the server for determining what to swap. To send stuff to the client, you can either use the send parameter or simply return the content or both!

    +

    Now, sometimes you might want to perform actions when a client connects or disconnects such as add or remove a user from a player queue. To hook into these events, you can pass your connection or disconnection function to the app.ws decorator:

    +
    async def on_connect(send):
    +    print('Connected!')
    +    await send(Div('Hello, you have connected', id="notifications"))
    +
    +async def on_disconnect(ws):
    +    print('Disconnected!')
    +
    +@app.ws('/ws', conn=on_connect, disconn=on_disconnect)
    +async def ws(msg:str, send):
    +    await send(Div('Hello ' + msg, id="notifications"))
    +    await sleep(2)
    +    return Div('Goodbye ' + msg, id="notifications"), mk_inp()
    +
    +
    +
    +

    Full Example #3 - Chatbot Example with DaisyUI Components

    +

    Let’s go back to the topic of adding components or styling beyond the simple PicoCSS examples so far. How might we adopt a component or framework? In this example, let’s build a chatbot UI leveraging the DaisyUI chat bubble. The final result will look like this:

    +
    +
    +

    +
    image.png
    +
    +
    +

    At first glance, DaisyUI’s chat component looks quite intimidating. The examples look like this:

    +
    <div class="chat chat-start">
    +  <div class="chat-image avatar">
    +    <div class="w-10 rounded-full">
    +      <img alt="Tailwind CSS chat bubble component" src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg" />
    +    </div>
    +  </div>
    +  <div class="chat-header">
    +    Obi-Wan Kenobi
    +    <time class="text-xs opacity-50">12:45</time>
    +  </div>
    +  <div class="chat-bubble">You were the Chosen One!</div>
    +  <div class="chat-footer opacity-50">
    +    Delivered
    +  </div>
    +</div>
    +<div class="chat chat-end">
    +  <div class="chat-image avatar">
    +    <div class="w-10 rounded-full">
    +      <img alt="Tailwind CSS chat bubble component" src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg" />
    +    </div>
    +  </div>
    +  <div class="chat-header">
    +    Anakin
    +    <time class="text-xs opacity-50">12:46</time>
    +  </div>
    +  <div class="chat-bubble">I hate you!</div>
    +  <div class="chat-footer opacity-50">
    +    Seen at 12:46
    +  </div>
    +</div>
    +

    We have several things going for us however.

    +
      +
    • ChatGPT knows DaisyUI and Tailwind (DaisyUI is a Tailwind component library)
    • +
    • We can build things up piece by piece with AI standing by to help.
    • +
    +

    https://h2f.answer.ai/ is a tool that can convert HTML to FT (fastcore.xml) and back, which is useful for getting a quick starting point when you have an HTML example to start from.

    +

    We can strip out some unnecessary bits and try to get the simplest possible example working in a notebook first:

    +
    +
    # Loading tailwind and daisyui
    +headers = (Script(src="https://cdn.tailwindcss.com"),
    +           Link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css"))
    +
    +# Displaying a single message
    +d = Div(
    +    Div("Chat header here", cls="chat-header"),
    +    Div("My message goes here", cls="chat-bubble chat-bubble-primary"),
    +    cls="chat chat-start"
    +)
    +# show(Html(*headers, d)) # uncomment to view
    +
    +

    Now we can extend this to render multiple messages, with the message being on the left (chat-start) or right (chat-end) depending on the role. While we’re at it, we can also change the color (chat-bubble-primary) of the message and put them all in a chat-box div:

    +
    +
    messages = [
    +    {"role":"user", "content":"Hello"},
    +    {"role":"assistant", "content":"Hi, how can I assist you?"}
    +]
    +
    +def ChatMessage(msg):
    +    return Div(
    +        Div(msg['role'], cls="chat-header"),
    +        Div(msg['content'], cls=f"chat-bubble chat-bubble-{'primary' if msg['role'] == 'user' else 'secondary'}"),
    +        cls=f"chat chat-{'end' if msg['role'] == 'user' else 'start'}")
    +
    +chatbox = Div(*[ChatMessage(msg) for msg in messages], cls="chat-box", id="chatlist")
    +
    +# show(Html(*headers, chatbox)) # Uncomment to view
    +
    +

    Next, it was back to the ChatGPT to tweak the chat box so it wouldn’t grow as messages were added. I asked:

    +
    "I have something like this (it's working now) 
    +[code]
    +The messages are added to this div so it grows over time. 
    +Is there a way I can set it's height to always be 80% of the total window height with a scroll bar if needed?"
    +

    Based on this query GPT4o helpfully shared that “This can be achieved using Tailwind CSS utility classes. Specifically, you can use h-[80vh] to set the height to 80% of the viewport height, and overflow-y-auto to add a vertical scroll bar when needed.”

    +

    To put it another way: none of the CSS classes in the following example were written by a human, and what edits I did make were informed by advice from the AI that made it relatively painless!

    +

    The actual chat functionality of the app is based on our claudette library. As with the image example, we face a potential hiccup in that getting a response from an LLM is slow. We need a way to have the user message added to the UI immediately, and then have the response added once it’s available. We could do something similar to the image generation example above, or use websockets. Check out the full example for implementations of both, along with further details.

    +
    +
    +

    Full Example #4 - Multiplayer Game of Life Example with Websockets

    +

    Let’s see how we can implement a collaborative website using Websockets in FastHTML. To showcase this, we will use the famous Conway’s Game of Life, which is a game that takes place in a grid world. Each cell in the grid can be either alive or dead. The cell’s state is initially given by a user before the game is started and then evolves through the iteration of the grid world once the clock starts. Whether a cell’s state will change from the previous state depends on simple rules based on its neighboring cells’ states. Here is the standard Game of Life logic implemented in Python courtesy of ChatGPT:

    +
    grid = [[0 for _ in range(20)] for _ in range(20)]
    +def update_grid(grid: list[list[int]]) -> list[list[int]]:
    +    new_grid = [[0 for _ in range(20)] for _ in range(20)]
    +    def count_neighbors(x, y):
    +        directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
    +        count = 0
    +        for dx, dy in directions:
    +            nx, ny = x + dx, y + dy
    +            if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]): count += grid[nx][ny]
    +        return count
    +    for i in range(len(grid)):
    +        for j in range(len(grid[0])):
    +            neighbors = count_neighbors(i, j)
    +            if grid[i][j] == 1:
    +                if neighbors < 2 or neighbors > 3: new_grid[i][j] = 0
    +                else: new_grid[i][j] = 1
    +            elif neighbors == 3: new_grid[i][j] = 1
    +    return new_grid
    +

    This would be a very dull game if we were to run it, since the initial state of everything would remain dead. Therefore, we need a way of letting the user give an initial state before starting the game. FastHTML to the rescue!

    +
    def Grid():
    +    cells = []
    +    for y, row in enumerate(game_state['grid']):
    +        for x, cell in enumerate(row):
    +            cell_class = 'alive' if cell else 'dead'
    +            cell = Div(cls=f'cell {cell_class}', hx_put='/update', hx_vals={'x': x, 'y': y}, hx_swap='none', hx_target='#gol', hx_trigger='click')
    +            cells.append(cell)
    +    return Div(*cells, id='grid')
    +
    +@rt('/update')
    +async def put(x: int, y: int):
    +    grid[y][x] = 1 if grid[y][x] == 0 else 0
    +

    Above is a component for representing the game’s state that the user can interact with and update on the server using cool HTMX features such as hx_vals for determining which cell was clicked to make it dead or alive. Now, you probably noticed that the HTTP request in this case is a PUT request, which does not return anything and this means our client’s view of the grid world and the server’s game state will immediately become out of sync :(. We could of course just return a new Grid component with the updated state, but that would only work for a single client, if we had more, they quickly get out of sync with each other and the server. Now Websockets to the rescue!

    +

    Websockets are a way for the server to keep a persistent connection with clients and send data to the client without explicitly being requested for information, which is not possible with HTTP. Luckily FastHTML and HTMX work well with Websockets. Simply state you wish to use websockets for your app and define a websocket route:

    +
    ...
    +app = FastHTML(hdrs=(picolink, gridlink, css, htmx_ws), exts='ws')
    +
    +player_queue = []
    +async def update_players():
    +    for i, player in enumerate(player_queue):
    +        try: await player(Grid())
    +        except: player_queue.pop(i)
    +async def on_connect(send): player_queue.append(send)
    +async def on_disconnect(send): await update_players()
    +
    +@app.ws('/gol', conn=on_connect, disconn=on_disconnect)
    +async def ws(msg:str, send): pass
    +
    +def Home(): return Title('Game of Life'), Main(gol, Div(Grid(), id='gol', cls='row center-xs'), hx_ext="ws", ws_connect="/gol")
    +
    +@rt('/update')
    +async def put(x: int, y: int):
    +    grid[y][x] = 1 if grid[y][x] == 0 else 0
    +    await update_players()
    +...
    +

    Here we simply keep track of all the players that have connected or disconnected to our site and when an update occurs, we send updates to all the players still connected via websockets. Via HTMX, you are still simply exchanging HTML from the server to the client and will swap in the content based on how you setup your hx_swap attribute. There is only one difference, that being all swaps are OOB. You can find more information on the HTMX websocket extension documentation page here. You can find a full fledge hosted example of this app here.

    +
    +
    +

    FT objects and HTML

    +

    These FT objects create a ‘FastTag’ structure [tag,children,attrs] for to_xml(). When we call Div(...), the elements we pass in are the children. Attributes are passed in as keywords. class and for are special words in python, so we use cls, klass or _class instead of class and fr or _for instead of for. Note these objects are just 3-element lists - you can create custom ones too as long as they’re also 3-element lists. Alternately, leaf nodes can be strings instead (which is why you can do Div('some text')). If you pass something that isn’t a 3-element list or a string, it will be converted to a string using str()… unless (our final trick) you define a __ft__ method that will run before str(), so you can render things a custom way.

    +

    For example, here’s one way we could make a custom class that can be rendered into HTML:

    +
    +
    class Person:
    +    def __init__(self, name, age):
    +        self.name = name
    +        self.age = age
    +
    +    def __ft__(self):
    +        return ['div', [f'{self.name} is {self.age} years old.'], {}]
    +
    +p = Person('Jonathan', 28)
    +print(to_xml(Div(p, "more text", cls="container")))
    +
    +
    <div class="container">
    +  <div>Jonathan is 28 years old.</div>
    +more text
    +</div>
    +
    +
    +
    +

    In the examples, you’ll see we often patch in __ft__ methods to existing classes to control how they’re rendered. For example, if Person didn’t have a __ft__ method or we wanted to override it, we could add a new one like this:

    +
    +
    from fastcore.all import patch
    +
    +@patch
    +def __ft__(self:Person):
    +    return Div("Person info:", Ul(Li("Name:",self.name), Li("Age:", self.age)))
    +
    +show(p)
    +
    +
    +Person info: +
      +
    • +Name: +Jonathan +
    • +
    • +Age: +28 +
    • +
    +
    +
    +
    +

    Some tags from fastcore.xml are overwritten by fasthtml.core and a few are further extended by fasthtml.xtend using this method. Over time, we hope to see others developing custom components too, giving us a larger and larger ecosystem of reusable components.

    +
    +
    +

    Custom Scripts and Styling

    +

    There are many popular JavaScript and CSS libraries that can be used via a simple Script or Style tag. But in some cases you will need to write more custom code. FastHTML’s js.py contains a few examples that may be useful as reference.

    +

    For example, to use the marked.js library to render markdown in a div, including in components added after the page has loaded via htmx, we do something like this:

    +
    import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
    +proc_htmx('%s', e => e.innerHTML = marked.parse(e.textContent));
    +

    proc_htmx is a shortcut that we wrote to apply a function to elements matching a selector, including the element that triggered the event. Here’s the code for reference:

    +
    export function proc_htmx(sel, func) {
    +  htmx.onLoad(elt => {
    +    const elements = htmx.findAll(elt, sel);
    +    if (elt.matches(sel)) elements.unshift(elt)
    +    elements.forEach(func);
    +  });
    +}
    +

    The AI Pictionary example uses a larger chunk of custom JavaScript to handle the drawing canvas. It’s a good example of the type of application where running code on the client side makes the most sense, but still shows how you can integrate it with FastHTML on the server side to add functionality (like the AI responses) easily.

    +

    Adding styling with custom CSS and libraries such as tailwind is done the same way we add custom JavaScript. The doodle example uses Doodle.CSS to style the page in a quirky way.

    +
    +
    +

    Deploying Your App

    +

    We can deploy FastHTML almost anywhere you can deploy python apps. We’ve tested Railway, Replit, HuggingFace, and PythonAnywhere.

    +
    +

    Railway

    +
      +
    1. Install the Railway CLI and sign up for an account.
    2. +
    3. Set up a folder with our app as main.py
    4. +
    5. In the folder, run railway login.
    6. +
    7. Use the fh_railway_deploy script to deploy our project:
    8. +
    +
    fh_railway_deploy MY_APP_NAME
    +

    What the script does for us:

    +
      +
    1. Do we have an existing railway project? +
        +
      • Yes: Link the project folder to our existing Railway project.
      • +
      • No: Create a new Railway project.
      • +
    2. +
    3. Deploy the project. We’ll see the logs as the service is built and run!
    4. +
    5. Fetches and displays the URL of our app.
    6. +
    7. By default, mounts a /app/data folder on the cloud to our app’s root folder. The app is run in /app by default, so from our app anything we store in /data will persist across restarts.
    8. +
    +

    A final note about Railway: We can add secrets like API keys that can be accessed as environment variables from our apps via ‘Variables’. For example, for the image generation app, we can add a REPLICATE_API_KEY variable, and then in main.py we can access it as os.environ['REPLICATE_API_KEY'].

    +
    +
    +

    Replit

    +

    Fork this repl for a minimal example you can edit to your heart’s content. .replit has been edited to add the right run command (run = ["uvicorn", "main:app", "--reload"]) and to set up the ports correctly. FastHTML was installed with poetry add python-fasthtml, you can add additional packages as needed in the same way. Running the app in Replit will show you a webview, but you may need to open in a new tab for all features (such as cookies) to work. When you’re ready, you can deploy your app by clicking the ‘Deploy’ button. You pay for usage - for an app that is mostly idle the cost is usually a few cents per month.

    +

    You can store secrets like API keys via the ‘Secrets’ tab in the Replit project settings.

    +
    +
    +

    HuggingFace

    +

    Follow the instructions in this repository to deploy to HuggingFace spaces.

    +
    +
    +
    +

    Where Next?

    +

    We’ve covered a lot of ground here! Hopefully this has given you plenty to work with in building your own FastHTML apps. If you have any questions, feel free to ask in the #fasthtml Discord channel (in the fastai Discord community). You can look through the other examples in the fasthtml-example repository for more ideas, and keep an eye on Jeremy’s YouTube channel where we’ll be releasing a number of “dev chats” related to FastHTML in the near future.

    + + +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/docs/tutorials/by_example.html.md b/docs/tutorials/by_example.html.md new file mode 100644 index 0000000000000000000000000000000000000000..060431428e3d0da24509f58b9dd2df68517e2428 --- /dev/null +++ b/docs/tutorials/by_example.html.md @@ -0,0 +1,1560 @@ +# FastHTML By Example + + + + +This tutorial provides an alternate introduction to FastHTML by building +out example applications. We also illustrate how to use FastHTML +foundations to create custom web apps. Finally, this document serves as +minimal context for a LLM to turn it into a FastHTML assistant. + +Let’s get started. + +## FastHTML Basics + +FastHTML is *just Python*. You can install it with +`pip install python-fasthtml`. Extensions/components built for it can +likewise be distributed via PyPI or as simple Python files. + +The core usage of FastHTML is to define routes, and then to define what +to do at each route. This is similar to the +[FastAPI](https://fastapi.tiangolo.com/) web framework (in fact we +implemented much of the functionality to match the FastAPI usage +examples), but where FastAPI focuses on returning JSON data to build +APIs, FastHTML focuses on returning HTML data. + +Here’s a simple FastHTML app that returns a “Hello, World” message: + +``` python +from fasthtml.common import FastHTML, serve + +app = FastHTML() + +@app.get("/") +def home(): + return "

    Hello, World

    " + +serve() +``` + +To run this app, place it in a file, say `app.py`, and then run it with +`python app.py`. + + INFO: Will watch for changes in these directories: ['/home/jonathan/fasthtml-example'] + INFO: Uvicorn running on http://127.0.0.1:5001 (Press CTRL+C to quit) + INFO: Started reloader process [871942] using WatchFiles + INFO: Started server process [871945] + INFO: Waiting for application startup. + INFO: Application startup complete. + +If you navigate to in a browser, you’ll see your +“Hello, World”. If you edit the `app.py` file and save it, the server +will reload and you’ll see the updated message when you refresh the page +in your browser. + +## Constructing HTML + +Notice we wrote some HTML in the previous example. We don’t want to do +that! Some web frameworks require that you learn HTML, CSS, JavaScript +AND some templating language AND python. We want to do as much as +possible with just one language. Fortunately, the Python module +[fastcore.xml](https://fastcore.fast.ai/xml.html) has all we need for +constructing HTML from Python, and FastHTML includes all the tags you +need to get started. For example: + +``` python +from fasthtml.common import * +page = Html( + Head(Title('Some page')), + Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass'))) +print(to_xml(page)) +``` + + + + + + Some page + + +
    + Some text, + A link + +
    + + + +``` python +show(page) +``` + + + + + Some page + + +
    +Some text, + A link + +
    + + + +If that `import *` worries you, you can always import only the tags you +need. + +FastHTML is smart enough to know about fastcore.xml, and so you don’t +need to use the `to_xml` function to convert your FT objects to HTML. +You can just return them as you would any other Python object. For +example, if we modify our previous example to use fastcore.xml, we can +return an FT object directly: + +``` python +from fasthtml.common import * +app = FastHTML() + +@app.get("/") +def home(): + page = Html( + Head(Title('Some page')), + Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass'))) + return page + +serve() +``` + +This will render the HTML in the browser. + +For debugging, you can right-click on the rendered HTML in the browser +and select “Inspect” to see the underlying HTML that was generated. +There you’ll also find the ‘network’ tab, which shows you the requests +that were made to render the page. Refresh and look for the request to +`127.0.0.1` - and you’ll see it’s just a `GET` request to `/`, and the +response body is the HTML you just returned. + +
    + +> **Live Reloading** +> +> You can also enable [live reloading](../ref/live_reload.ipynb) so you +> don’t have to manually refresh your browser to view updates. + +
    + +You can also use Starlette’s `TestClient` to try it out in a notebook: + +``` python +from starlette.testclient import TestClient +client = TestClient(app) +r = client.get("/") +print(r.text) +``` + + + Some page + +
    + Some text, + A link + +
    + + + +FastHTML wraps things in an Html tag if you don’t do it yourself (unless +the request comes from htmx, in which case you get the element +directly). See [FT objects and HTML](#ft-objects-and-html) for more on +creating custom components or adding HTML rendering to existing Python +objects. To give the page a non-default title, return a Title before +your main content: + +``` python +app = FastHTML() + +@app.get("/") +def home(): + return Title("Page Demo"), Div(H1('Hello, World'), P('Some text'), P('Some more text')) + +client = TestClient(app) +print(client.get("/").text) +``` + + + + + + Page Demo + + + + + + + +
    +

    Hello, World

    +

    Some text

    +

    Some more text

    +
    + + + +We’ll use this pattern often in the examples to follow. + +## Defining Routes + +The HTTP protocol defines a number of methods (‘verbs’) to send requests +to a server. The most common are GET, POST, PUT, DELETE, and HEAD. We +saw ‘GET’ in action before - when you navigate to a URL, you’re making a +GET request to that URL. We can do different things on a route for +different HTTP methods. For example: + +``` python +@app.route("/", methods='get') +def home(): + return H1('Hello, World') + +@app.route("/", methods=['post', 'put']) +def post_or_put(): + return "got a POST or PUT request" +``` + +This says that when someone navigates to the root URL “/” (i.e. sends a +GET request), they will see the big “Hello, World” heading. When someone +submits a POST or PUT request to the same URL, the server should return +the string “got a post or put request”. + +
    + +> **Test the POST request** +> +> You can test the POST request with +> `curl -X POST http://127.0.0.1:8000 -d "some data"`. This sends some +> data to the server, you should see the response “got a post or put +> request” printed in the terminal. + +
    + +There are a few other ways you can specify the route+method - FastHTML +has `.get`, `.post`, etc. as shorthand for +`route(..., methods=['get'])`, etc. + +``` python +@app.get("/") +def my_function(): + return "Hello World from a GET request" +``` + +Or you can use the `@rt` decorator without a method but specify the +method with the name of the function. For example: + +``` python +rt = app.route + +@rt("/") +def post(): + return "Hello World from a POST request" +``` + +``` python +client.post("/").text +``` + + 'Hello World from a POST request' + +You’re welcome to pick whichever style you prefer. Using routes lets you +show different content on different pages - ‘/home’, ‘/about’ and so on. +You can also respond differently to different kinds of requests to the +same route, as shown above. You can also pass data via the route: + +
    + +## `@app.get` + +``` python +@app.get("/greet/{nm}") +def greet(nm:str): + return f"Good day to you, {nm}!" + +client.get("/greet/Dave").text +``` + + 'Good day to you, Dave!' + +## `@rt` + +``` python +@rt("/greet/{nm}") +def get(nm:str): + return f"Good day to you, {nm}!" + +client.get("/greet/Dave").text +``` + + 'Good day to you, Dave!' + +
    + +More on this in the [More on Routing and Request +Parameters](#more-on-routing-and-request-parameters) section, which goes +deeper into the different ways to get information from a request. + +## Styling Basics + +Plain HTML probably isn’t quite what you imagine when you visualize your +beautiful web app. CSS is the go-to language for styling HTML. But +again, we don’t want to learn extra languages unless we absolutely have +to! Fortunately, there are ways to get much more visually appealing +sites by relying on the hard work of others, using existing CSS +libraries. One of our favourites is [PicoCSS](https://picocss.com/). A +common way to add CSS files to web pages is to use a +[``](https://www.w3schools.com/tags/tag_link.asp) tag inside your +[HTML header](https://www.w3schools.com/tags/tag_header.asp), like this: + +``` html +
    + ... + +
    +``` + +For convenience, FastHTML already defines a Pico component for you with +`picolink`: + +``` python +print(to_xml(picolink)) +``` + + + + + +
    + +> **Note** +> +> `picolink` also includes a ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    + +
    + + +
    + + + +
    + +
    +
    +

    JS App Walkthrough

    +
    + +
    +
    + How to build a website with custom JavaScript in FastHTML step-by-step +
    +
    + + +
    + + + + +
    + + + +
    + + + +
    +

    Installation

    +

    You’ll need the following software to complete the tutorial, read on for specific installation instructions:

    +
      +
    1. Python
    2. +
    3. A Python package manager such as pip (which normally comes with Python) or uv
    4. +
    5. FastHTML
    6. +
    7. Web browser
    8. +
    9. Railway.app account
    10. +
    +

    If you haven’t worked with Python before, we recommend getting started with Miniconda.

    +

    Note that you will only need to follow the steps in the installation section once per environment. If you create a new repo, you won’t need to redo these.

    +
    +

    Install FastHTML

    +

    For Mac, Windows and Linux, enter:

    +
    pip install python-fasthtml
    +
    +
    +
    +

    First steps

    +

    By the end of this section you’ll have your own FastHTML website with tests deployed to railway.app.

    +
    +

    Create a hello world

    +

    Create a new folder to organize all the files for your project. Inside this folder, create a file called main.py and add the following code to it:

    +
    +
    +
    main.py
    +
    +
    from fasthtml.common import *
    +
    +app = FastHTML()
    +rt = app.route
    +
    +@rt('/')
    +def get():
    +    return 'Hello, world!'
    +
    +serve()
    +
    +

    Finally, run python main.py in your terminal and open your browser to the ‘Link’ that appears.

    +
    +
    +

    QuickDraw: A FastHTML Adventure 🎨✨

    +

    The end result of this tutorial will be QuickDraw, a real-time collaborative drawing app using FastHTML. Here is what the final site will look like:

    +
    +
    +

    +
    QuickDraw
    +
    +
    +
    +

    Drawing Rooms

    +

    Drawing rooms are the core concept of our application. Each room represents a separate drawing space where a user can let their inner Picasso shine. Here’s a detailed breakdown:

    +
      +
    1. Room Creation and Storage
    2. +
    +
    +
    +
    main.py
    +
    +
    db = database('data/drawapp.db')
    +rooms = db.t.rooms
    +if rooms not in db.t:
    +    rooms.create(id=int, name=str, created_at=str, pk='id')
    +Room = rooms.dataclass()
    +
    +@patch
    +def __ft__(self:Room):
    +    return Li(A(self.name, href=f"/rooms/{self.id}"))
    +
    +

    Or you can use our fast_app function to create a FastHTML app with a SQLite database and dataclass in one line:

    +
    +
    +
    main.py
    +
    +
    def render(room):
    +    return Li(A(room.name, href=f"/rooms/{room.id}"))
    +
    +app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')
    +
    +

    We are specifying a render function to convert our dataclass into HTML, which is the same as extending the __ft__ method from the patch decorator we used before. We will use this method for the rest of the tutorial since it is a lot cleaner and easier to read.

    +
      +
    • We’re using a SQLite database (via FastLite) to store our rooms.
    • +
    • Each room has an id (integer), a name (string), and a created_at timestamp (string).
    • +
    • The Room dataclass is automatically generated based on this structure.
    • +
    +
      +
    1. Creating a room
    2. +
    +
    +
    +
    main.py
    +
    +
    @rt("/")
    +def get():
    +    # The 'Input' id defaults to the same as the name, so you can omit it if you wish
    +    create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
    +                       Button("Create Room"),
    +                       hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin")
    +    rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
    +    return Titled("DrawCollab", 
    +                  H1("DrawCollab"),
    +                  create_room, rooms_list)
    +
    +@rt("/rooms")
    +async def post(room:Room):
    +    room.created_at = datetime.now().isoformat()
    +    return rooms.insert(room)
    +
    +
      +
    • When a user submits the “Create Room” form, this route is called.
    • +
    • It creates a new Room object, sets the creation time, and inserts it into the database.
    • +
    • It returns an HTML list item with a link to the new room, which is dynamically added to the room list on the homepage thanks to HTMX.
    • +
    +
      +
    1. Let’s give our rooms shape
    2. +
    +
    +
    +
    main.py
    +
    +
    @rt("/rooms/{id}")
    +async def get(id:int):
    +    room = rooms[id]
    +    return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/"))
    +
    +
      +
    • This route renders the interface for a specific room.
    • +
    • It fetches the room from the database and renders a title, heading, and paragraph.
    • +
    +

    Here is the full code so far:

    +
    +
    +
    main.py
    +
    +
    from fasthtml.common import *
    +from datetime import datetime
    +
    +def render(room):
    +    return Li(A(room.name, href=f"/rooms/{room.id}"))
    +
    +app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')
    +
    +@rt("/")
    +def get():
    +    create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
    +                       Button("Create Room"),
    +                       hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin")
    +    rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
    +    return Titled("DrawCollab", create_room, rooms_list)
    +
    +@rt("/rooms")
    +async def post(room:Room):
    +    room.created_at = datetime.now().isoformat()
    +    return rooms.insert(room)
    +
    +@rt("/rooms/{id}")
    +async def get(id:int):
    +    room = rooms[id]
    +    return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/"))
    +
    +serve()
    +
    +

    Now run python main.py in your terminal and open your browser to the ‘Link’ that appears. You should see a page with a form to create a new room and a list of existing rooms.

    +
    +
    +

    The Canvas - Let’s Get Drawing! 🖌️

    +

    Time to add the actual drawing functionality. We’ll use Fabric.js for this:

    +
    +
    +
    main.py
    +
    +
    # ... (keep the previous imports and database setup)
    +
    +@rt("/rooms/{id}")
    +async def get(id:int):
    +    room = rooms[id]
    +    canvas = Canvas(id="canvas", width="800", height="600")
    +    color_picker = Input(type="color", id="color-picker", value="#3CDD8C")
    +    brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10")
    +    
    +    js = """
    +    var canvas = new fabric.Canvas('canvas');
    +    canvas.isDrawingMode = true;
    +    canvas.freeDrawingBrush.color = '#3CDD8C';
    +    canvas.freeDrawingBrush.width = 10;
    +    
    +    document.getElementById('color-picker').onchange = function() {
    +        canvas.freeDrawingBrush.color = this.value;
    +    };
    +    
    +    document.getElementById('brush-size').oninput = function() {
    +        canvas.freeDrawingBrush.width = parseInt(this.value, 10);
    +    };
    +    """
    +    
    +    return Titled(f"Room: {room.name}",
    +                  A(Button("Leave Room"), href="/"),
    +                  canvas,
    +                  Div(color_picker, brush_size),
    +                  Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"),
    +                  Script(js))
    +
    +# ... (keep the serve() part)
    +
    +

    Now we’ve got a drawing canvas! FastHTML makes it easy to include external libraries and add custom JavaScript.

    +
    +
    +

    Saving and Loading Canvases 💾

    +

    Now that we have a working drawing canvas, let’s add the ability to save and load drawings. We’ll modify our database schema to include a canvas_data field, and add new routes for saving and loading canvas data. Here’s how we’ll update our code:

    +
      +
    1. Modify the database schema:
    2. +
    +
    +
    +
    main.py
    +
    +
    app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')
    +
    +
      +
    1. Add a save button that grabs the canvas’ state and sends it to the server:
    2. +
    +
    +
    +
    main.py
    +
    +
    @rt("/rooms/{id}")
    +async def get(id:int):
    +    room = rooms[id]
    +    canvas = Canvas(id="canvas", width="800", height="600")
    +    color_picker = Input(type="color", id="color-picker", value="#3CDD8C")
    +    brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10")
    +    save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}")
    +    # ... (rest of the function remains the same)
    +
    +
      +
    1. Add routes for saving and loading canvas data:
    2. +
    +
    +
    +
    main.py
    +
    +
    @rt("/rooms/{id}/save")
    +async def post(id:int, canvas_data:str):
    +    rooms.update({'canvas_data': canvas_data}, id)
    +    return "Canvas saved successfully"
    +
    +@rt("/rooms/{id}/load")
    +async def get(id:int):
    +    room = rooms[id]
    +    return room.canvas_data if room.canvas_data else "{}"
    +
    +
      +
    1. Update the JavaScript to load existing canvas data:
    2. +
    +
    +
    +
    main.py
    +
    +
    js = f"""
    +    var canvas = new fabric.Canvas('canvas');
    +    canvas.isDrawingMode = true;
    +    canvas.freeDrawingBrush.color = '#3CDD8C';
    +    canvas.freeDrawingBrush.width = 10;
    +    // Load existing canvas data
    +    fetch(`/rooms/{id}/load`)
    +    .then(response => response.json())
    +    .then(data => {{
    +        if (data && Object.keys(data).length > 0) {{
    +            canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));
    +        }}
    +    }});
    +    
    +    // ... (rest of the JavaScript remains the same)
    +"""
    +
    +

    With these changes, users can now save their drawings and load them when they return to the room. The canvas data is stored as a JSON string in the database, allowing for easy serialization and deserialization. Try it out! Create a new room, make a drawing, save it, and then reload the page. You should see your drawing reappear, ready for further editing.

    +

    Here is the completed code:

    +
    +
    +
    main.py
    +
    +
    from fasthtml.common import *
    +from datetime import datetime
    +
    +def render(room):
    +    return Li(A(room.name, href=f"/rooms/{room.id}"))
    +
    +app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')
    +
    +@rt("/")
    +def get():
    +    create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
    +                       Button("Create Room"),
    +                       hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin")
    +    rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
    +    return Titled("QuickDraw", 
    +                  create_room, rooms_list)
    +
    +@rt("/rooms")
    +async def post(room:Room):
    +    room.created_at = datetime.now().isoformat()
    +    return rooms.insert(room)
    +
    +@rt("/rooms/{id}")
    +async def get(id:int):
    +    room = rooms[id]
    +    canvas = Canvas(id="canvas", width="800", height="600")
    +    color_picker = Input(type="color", id="color-picker", value="#000000")
    +    brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10")
    +    save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}")
    +
    +    js = f"""
    +    var canvas = new fabric.Canvas('canvas');
    +    canvas.isDrawingMode = true;
    +    canvas.freeDrawingBrush.color = '#000000';
    +    canvas.freeDrawingBrush.width = 10;
    +
    +    // Load existing canvas data
    +    fetch(`/rooms/{id}/load`)
    +    .then(response => response.json())
    +    .then(data => {{
    +        if (data && Object.keys(data).length > 0) {{
    +            canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));
    +        }}
    +    }});
    +    
    +    document.getElementById('color-picker').onchange = function() {{
    +        canvas.freeDrawingBrush.color = this.value;
    +    }};
    +    
    +    document.getElementById('brush-size').oninput = function() {{
    +        canvas.freeDrawingBrush.width = parseInt(this.value, 10);
    +    }};
    +    """
    +    
    +    return Titled(f"Room: {room.name}",
    +                  A(Button("Leave Room"), href="/"),
    +                  canvas,
    +                  Div(color_picker, brush_size, save_button),
    +                  Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"),
    +                  Script(js))
    +
    +@rt("/rooms/{id}/save")
    +async def post(id:int, canvas_data:str):
    +    rooms.update({'canvas_data': canvas_data}, id)
    +    return "Canvas saved successfully"
    +
    +@rt("/rooms/{id}/load")
    +async def get(id:int):
    +    room = rooms[id]
    +    return room.canvas_data if room.canvas_data else "{}"
    +
    +serve()
    +
    +
    +
    +
    +

    Deploying to Railway

    +

    You can deploy your website to a number of hosting providers, for this tutorial we’ll be using Railway. To get started, make sure you create an account and install the Railway CLI. Once installed, make sure to run railway login to log in to your account.

    +

    To make deploying your website as easy as possible, FastHTMl comes with a built in CLI tool that will handle most of the deployment process for you. To deploy your website, run the following command in your terminal in the root directory of your project:

    +
    fh_railway_deploy quickdraw
    +
    +
    +
    + +
    +
    +Note +
    +
    +
    +

    Your app must be located in a main.py file for this to work.

    +
    +
    +
    +
    +

    Conclusion: You’re a FastHTML Artist Now! 🎨🚀

    +

    Congratulations! You’ve just built a sleek, interactive web application using FastHTML. Let’s recap what we’ve learned:

    +
      +
    1. FastHTML allows you to create dynamic web apps with minimal code.
    2. +
    3. We used FastHTML’s routing system to handle different pages and actions.
    4. +
    5. We integrated with a SQLite database to store room information and canvas data.
    6. +
    7. We utilized Fabric.js to create an interactive drawing canvas.
    8. +
    9. We implemented features like color picking, brush size adjustment, and canvas saving.
    10. +
    11. We used HTMX for seamless, partial page updates without full reloads.
    12. +
    13. We learned how to deploy our FastHTML application to Railway for easy hosting.
    14. +
    +

    You’ve taken your first steps into the world of FastHTML development. From here, the possibilities are endless! You could enhance the drawing app further by adding features like:

    +
      +
    • Implementing different drawing tools (e.g., shapes, text)
    • +
    • Adding user authentication
    • +
    • Creating a gallery of saved drawings
    • +
    • Implementing real-time collaborative drawing using WebSockets
    • +
    +

    Whatever you choose to build next, FastHTML has got your back. Now go forth and create something awesome! Happy coding! 🖼️🚀

    + + +
    +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/docs/tutorials/e2e.html.md b/docs/tutorials/e2e.html.md new file mode 100644 index 0000000000000000000000000000000000000000..81bf8603b6efd60f5ba4f1b7aacde408901bbb37 --- /dev/null +++ b/docs/tutorials/e2e.html.md @@ -0,0 +1,499 @@ +# JS App Walkthrough + + + + +## Installation + +You’ll need the following software to complete the tutorial, read on for +specific installation instructions: + +1. Python +2. A Python package manager such as pip (which normally comes with + Python) or uv +3. FastHTML +4. Web browser +5. Railway.app account + +If you haven’t worked with Python before, we recommend getting started +with [Miniconda](https://docs.anaconda.com/miniconda/). + +Note that you will only need to follow the steps in the installation +section once per environment. If you create a new repo, you won’t need +to redo these. + +### Install FastHTML + +For Mac, Windows and Linux, enter: + +``` sh +pip install python-fasthtml +``` + +## First steps + +By the end of this section you’ll have your own FastHTML website with +tests deployed to railway.app. + +### Create a hello world + +Create a new folder to organize all the files for your project. Inside +this folder, create a file called `main.py` and add the following code +to it: + +
    + +**main.py** + +``` python +from fasthtml.common import * + +app = FastHTML() +rt = app.route + +@rt('/') +def get(): + return 'Hello, world!' + +serve() +``` + +
    + +Finally, run `python main.py` in your terminal and open your browser to +the ‘Link’ that appears. + +### QuickDraw: A FastHTML Adventure 🎨✨ + +The end result of this tutorial will be QuickDraw, a real-time +collaborative drawing app using FastHTML. Here is what the final site +will look like: + +
    +QuickDraw + +
    + +#### Drawing Rooms + +Drawing rooms are the core concept of our application. Each room +represents a separate drawing space where a user can let their inner +Picasso shine. Here’s a detailed breakdown: + +1. Room Creation and Storage + +
    + +**main.py** + +``` python +db = database('data/drawapp.db') +rooms = db.t.rooms +if rooms not in db.t: + rooms.create(id=int, name=str, created_at=str, pk='id') +Room = rooms.dataclass() + +@patch +def __ft__(self:Room): + return Li(A(self.name, href=f"/rooms/{self.id}")) +``` + +
    + +Or you can use our `fast_app` function to create a FastHTML app with a +SQLite database and dataclass in one line: + +
    + +**main.py** + +``` python +def render(room): + return Li(A(room.name, href=f"/rooms/{room.id}")) + +app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id') +``` + +
    + +We are specifying a render function to convert our dataclass into HTML, +which is the same as extending the `__ft__` method from the `patch` +decorator we used before. We will use this method for the rest of the +tutorial since it is a lot cleaner and easier to read. + +- We’re using a SQLite database (via FastLite) to store our rooms. +- Each room has an id (integer), a name (string), and a created_at + timestamp (string). +- The Room dataclass is automatically generated based on this structure. + +2. Creating a room + +
    + +**main.py** + +``` python +@rt("/") +def get(): + # The 'Input' id defaults to the same as the name, so you can omit it if you wish + create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), + Button("Create Room"), + hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") + rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') + return Titled("DrawCollab", + H1("DrawCollab"), + create_room, rooms_list) + +@rt("/rooms") +async def post(room:Room): + room.created_at = datetime.now().isoformat() + return rooms.insert(room) +``` + +
    + +- When a user submits the “Create Room” form, this route is called. +- It creates a new Room object, sets the creation time, and inserts it + into the database. +- It returns an HTML list item with a link to the new room, which is + dynamically added to the room list on the homepage thanks to HTMX. + +3. Let’s give our rooms shape + +
    + +**main.py** + +``` python +@rt("/rooms/{id}") +async def get(id:int): + room = rooms[id] + return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/")) +``` + +
    + +- This route renders the interface for a specific room. +- It fetches the room from the database and renders a title, heading, + and paragraph. + +Here is the full code so far: + +
    + +**main.py** + +``` python +from fasthtml.common import * +from datetime import datetime + +def render(room): + return Li(A(room.name, href=f"/rooms/{room.id}")) + +app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id') + +@rt("/") +def get(): + create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), + Button("Create Room"), + hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") + rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') + return Titled("DrawCollab", create_room, rooms_list) + +@rt("/rooms") +async def post(room:Room): + room.created_at = datetime.now().isoformat() + return rooms.insert(room) + +@rt("/rooms/{id}") +async def get(id:int): + room = rooms[id] + return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/")) + +serve() +``` + +
    + +Now run `python main.py` in your terminal and open your browser to the +‘Link’ that appears. You should see a page with a form to create a new +room and a list of existing rooms. + +#### The Canvas - Let’s Get Drawing! 🖌️ + +Time to add the actual drawing functionality. We’ll use Fabric.js for +this: + +
    + +**main.py** + +``` python +# ... (keep the previous imports and database setup) + +@rt("/rooms/{id}") +async def get(id:int): + room = rooms[id] + canvas = Canvas(id="canvas", width="800", height="600") + color_picker = Input(type="color", id="color-picker", value="#3CDD8C") + brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10") + + js = """ + var canvas = new fabric.Canvas('canvas'); + canvas.isDrawingMode = true; + canvas.freeDrawingBrush.color = '#3CDD8C'; + canvas.freeDrawingBrush.width = 10; + + document.getElementById('color-picker').onchange = function() { + canvas.freeDrawingBrush.color = this.value; + }; + + document.getElementById('brush-size').oninput = function() { + canvas.freeDrawingBrush.width = parseInt(this.value, 10); + }; + """ + + return Titled(f"Room: {room.name}", + A(Button("Leave Room"), href="/"), + canvas, + Div(color_picker, brush_size), + Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"), + Script(js)) + +# ... (keep the serve() part) +``` + +
    + +Now we’ve got a drawing canvas! FastHTML makes it easy to include +external libraries and add custom JavaScript. + +#### Saving and Loading Canvases 💾 + +Now that we have a working drawing canvas, let’s add the ability to save +and load drawings. We’ll modify our database schema to include a +`canvas_data` field, and add new routes for saving and loading canvas +data. Here’s how we’ll update our code: + +1. Modify the database schema: + +
    + +**main.py** + +``` python +app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id') +``` + +
    + +2. Add a save button that grabs the canvas’ state and sends it to the + server: + +
    + +**main.py** + +``` python +@rt("/rooms/{id}") +async def get(id:int): + room = rooms[id] + canvas = Canvas(id="canvas", width="800", height="600") + color_picker = Input(type="color", id="color-picker", value="#3CDD8C") + brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10") + save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}") + # ... (rest of the function remains the same) +``` + +
    + +3. Add routes for saving and loading canvas data: + +
    + +**main.py** + +``` python +@rt("/rooms/{id}/save") +async def post(id:int, canvas_data:str): + rooms.update({'canvas_data': canvas_data}, id) + return "Canvas saved successfully" + +@rt("/rooms/{id}/load") +async def get(id:int): + room = rooms[id] + return room.canvas_data if room.canvas_data else "{}" +``` + +
    + +4. Update the JavaScript to load existing canvas data: + +
    + +**main.py** + +``` javascript +js = f""" + var canvas = new fabric.Canvas('canvas'); + canvas.isDrawingMode = true; + canvas.freeDrawingBrush.color = '#3CDD8C'; + canvas.freeDrawingBrush.width = 10; + // Load existing canvas data + fetch(`/rooms/{id}/load`) + .then(response => response.json()) + .then(data => {{ + if (data && Object.keys(data).length > 0) {{ + canvas.loadFromJSON(data, canvas.renderAll.bind(canvas)); + }} + }}); + + // ... (rest of the JavaScript remains the same) +""" +``` + +
    + +With these changes, users can now save their drawings and load them when +they return to the room. The canvas data is stored as a JSON string in +the database, allowing for easy serialization and deserialization. Try +it out! Create a new room, make a drawing, save it, and then reload the +page. You should see your drawing reappear, ready for further editing. + +Here is the completed code: + +
    + +**main.py** + +``` python +from fasthtml.common import * +from datetime import datetime + +def render(room): + return Li(A(room.name, href=f"/rooms/{room.id}")) + +app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id') + +@rt("/") +def get(): + create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), + Button("Create Room"), + hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") + rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') + return Titled("QuickDraw", + create_room, rooms_list) + +@rt("/rooms") +async def post(room:Room): + room.created_at = datetime.now().isoformat() + return rooms.insert(room) + +@rt("/rooms/{id}") +async def get(id:int): + room = rooms[id] + canvas = Canvas(id="canvas", width="800", height="600") + color_picker = Input(type="color", id="color-picker", value="#000000") + brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10") + save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}") + + js = f""" + var canvas = new fabric.Canvas('canvas'); + canvas.isDrawingMode = true; + canvas.freeDrawingBrush.color = '#000000'; + canvas.freeDrawingBrush.width = 10; + + // Load existing canvas data + fetch(`/rooms/{id}/load`) + .then(response => response.json()) + .then(data => {{ + if (data && Object.keys(data).length > 0) {{ + canvas.loadFromJSON(data, canvas.renderAll.bind(canvas)); + }} + }}); + + document.getElementById('color-picker').onchange = function() {{ + canvas.freeDrawingBrush.color = this.value; + }}; + + document.getElementById('brush-size').oninput = function() {{ + canvas.freeDrawingBrush.width = parseInt(this.value, 10); + }}; + """ + + return Titled(f"Room: {room.name}", + A(Button("Leave Room"), href="/"), + canvas, + Div(color_picker, brush_size, save_button), + Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"), + Script(js)) + +@rt("/rooms/{id}/save") +async def post(id:int, canvas_data:str): + rooms.update({'canvas_data': canvas_data}, id) + return "Canvas saved successfully" + +@rt("/rooms/{id}/load") +async def get(id:int): + room = rooms[id] + return room.canvas_data if room.canvas_data else "{}" + +serve() +``` + +
    + +### Deploying to Railway + +You can deploy your website to a number of hosting providers, for this +tutorial we’ll be using Railway. To get started, make sure you create an +[account](https://railway.app/) and install the [Railway +CLI](https://docs.railway.app/guides/cli). Once installed, make sure to +run `railway login` to log in to your account. + +To make deploying your website as easy as possible, FastHTMl comes with +a built in CLI tool that will handle most of the deployment process for +you. To deploy your website, run the following command in your terminal +in the root directory of your project: + +``` sh +fh_railway_deploy quickdraw +``` + +
    + +> **Note** +> +> Your app must be located in a `main.py` file for this to work. + +
    + +### Conclusion: You’re a FastHTML Artist Now! 🎨🚀 + +Congratulations! You’ve just built a sleek, interactive web application +using FastHTML. Let’s recap what we’ve learned: + +1. FastHTML allows you to create dynamic web apps with minimal code. +2. We used FastHTML’s routing system to handle different pages and + actions. +3. We integrated with a SQLite database to store room information and + canvas data. +4. We utilized Fabric.js to create an interactive drawing canvas. +5. We implemented features like color picking, brush size adjustment, + and canvas saving. +6. We used HTMX for seamless, partial page updates without full + reloads. +7. We learned how to deploy our FastHTML application to Railway for + easy hosting. + +You’ve taken your first steps into the world of FastHTML development. +From here, the possibilities are endless! You could enhance the drawing +app further by adding features like: + +- Implementing different drawing tools (e.g., shapes, text) +- Adding user authentication +- Creating a gallery of saved drawings +- Implementing real-time collaborative drawing using WebSockets + +Whatever you choose to build next, FastHTML has got your back. Now go +forth and create something awesome! Happy coding! 🖼️🚀 diff --git a/docs/tutorials/imgs/quickdraw.png b/docs/tutorials/imgs/quickdraw.png new file mode 100644 index 0000000000000000000000000000000000000000..37c8a7cdb43a79c0caf51444ffecaba002c8fff8 Binary files /dev/null and b/docs/tutorials/imgs/quickdraw.png differ diff --git a/docs/tutorials/index.html b/docs/tutorials/index.html new file mode 100644 index 0000000000000000000000000000000000000000..fa1c6d30e4c449f38827910d38cada402c044c9d --- /dev/null +++ b/docs/tutorials/index.html @@ -0,0 +1,939 @@ + + + + + + + + + +Tutorials – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    + +
    + + +
    + + + +
    + +
    +
    +

    Tutorials

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

    Click through to any of these tutorials to get started with FastHTML’s features.

    + + + + +
    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +Title + +Description +
    +FastHTML By Example + +An introduction to FastHTML from the ground up, with four complete examples +
    +Web Devs Quickstart + +A fast introduction to FastHTML for experienced web developers. +
    +JS App Walkthrough + +How to build a website with custom JavaScript in FastHTML step-by-step +
    +FastHTML Best Practices + +FastHTML applications are different to applications using FastAPI/react, Django, etc. Don’t assume that FastHTML best practices are the same as those for other frameworks.… +
    +Using Jupyter to write FastHTML + +Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications. +
    +
    +No matching items +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 0000000000000000000000000000000000000000..b84e32e20f6c491e138d81605a2f1bfc568bb1bb --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,5 @@ +# Tutorials + + +Click through to any of these tutorials to get started with FastHTML’s +features. diff --git a/docs/tutorials/jupyter_and_fasthtml.html b/docs/tutorials/jupyter_and_fasthtml.html new file mode 100644 index 0000000000000000000000000000000000000000..4830dff874f80f5fd8d08d3bd2b3a638da759bb1 --- /dev/null +++ b/docs/tutorials/jupyter_and_fasthtml.html @@ -0,0 +1,939 @@ + + + + + + + + + + +Using Jupyter to write FastHTML – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    + +
    + + +
    + + + +
    + +
    +
    +

    Using Jupyter to write FastHTML

    +
    + +
    +
    + Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications. +
    +
    + + +
    + + + + +
    + + + +
    + + + +

    Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.

    +
    +
    +
    + +
    +
    +Download this notebook and try it yourself +
    +
    +
    +

    The source code for this page is a Jupyter notebook. That makes it easy to directly experiment with it. However, as this is working code that means we have to comment out a few things in order for the documentation to build.

    +
    +
    +

    The first step is to import necessary libraries. As using FastHTML inside a Jupyter notebook is a special case, it remains a special import.

    +
    +
    from fasthtml.common import *
    +from fasthtml.jupyter import JupyUvi, HTMX
    +
    +

    Let’s create an app with fast_app.

    +
    +
    app, rt = fast_app(pico=True)
    +
    +

    Define a route to test the application.

    +
    +
    @rt
    +def index():
    +    return Titled('Hello, Jupyter',
    +           P('Welcome to the FastHTML + Jupyter example'),
    +           Button('Click', hx_get='/click', hx_target='#dest'),
    +           Div(id='dest')
    +    )
    +
    +

    Create a server object using JupyUvi, which also starts Uvicorn. The server runs in a separate thread from Jupyter, so it can use normal HTTP client functions in a notebook.

    +
    +
    server = JupyUvi(app)
    +
    + + +
    +
    +

    The HTMX callable displays the server’s HTMX application in an iframe which can be displayed by Jupyter notebook. Pass in the same port variable used in the JupyUvi callable above or leave it blank to use the default (8000).

    +
    +
    # This doesn't display in the docs - uncomment and run it to see it in action
    +# HTMX()
    +
    +

    We didn’t define the /click route, but that’s fine - we can define (or change) it any time, and it’s dynamically inserted into the running app. No need to restart or reload anything!

    +
    +
    @rt
    +def click(): return P('You clicked me!')
    +
    +
    +

    Full screen view

    +

    You can view your app outside of Jupyter by going to localhost:PORT, where PORT is usually the default 8000, so in most cases just click this link.

    +
    +
    +

    Graceful shutdowns

    +

    Use the server.stop() function displayed below. If you restart Jupyter without calling this line the thread may not be released and the HTMX callable above may throw errors. If that happens, a quick temporary fix is to specify a different port number in JupyUvi and HTMX with the port parameter.

    +

    Cleaner solutions to the dangling thread are to kill the dangling thread (dependant on each operating system) or restart the computer.

    +
    +
    server.stop()
    +
    + + +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/docs/tutorials/jupyter_and_fasthtml.html.md b/docs/tutorials/jupyter_and_fasthtml.html.md new file mode 100644 index 0000000000000000000000000000000000000000..1a9fa3d4f7ac439f026b641c66de45511532cf51 --- /dev/null +++ b/docs/tutorials/jupyter_and_fasthtml.html.md @@ -0,0 +1,105 @@ +# Using Jupyter to write FastHTML + + + + +Writing FastHTML applications in Jupyter notebooks requires a slightly +different process than normal Python applications. + +
    + +> **Download this notebook and try it yourself** +> +> The source code for this page is a [Jupyter +> notebook](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/tutorials/jupyter_and_fasthtml.ipynb). +> That makes it easy to directly experiment with it. However, as this is +> working code that means we have to comment out a few things in order +> for the documentation to build. + +
    + +The first step is to import necessary libraries. As using FastHTML +inside a Jupyter notebook is a special case, it remains a special +import. + +``` python +from fasthtml.common import * +from fasthtml.jupyter import JupyUvi, HTMX +``` + +Let’s create an app with `fast_app`. + +``` python +app, rt = fast_app(pico=True) +``` + +Define a route to test the application. + +``` python +@rt +def index(): + return Titled('Hello, Jupyter', + P('Welcome to the FastHTML + Jupyter example'), + Button('Click', hx_get='/click', hx_target='#dest'), + Div(id='dest') + ) +``` + +Create a `server` object using +[`JupyUvi`](https://www.fastht.ml/docs/api/jupyter.html#jupyuvi), which +also starts Uvicorn. The `server` runs in a separate thread from +Jupyter, so it can use normal HTTP client functions in a notebook. + +``` python +server = JupyUvi(app) +``` + + + +The [`HTMX`](https://www.fastht.ml/docs/api/jupyter.html#htmx) callable +displays the server’s HTMX application in an iframe which can be +displayed by Jupyter notebook. Pass in the same `port` variable used in +the [`JupyUvi`](https://www.fastht.ml/docs/api/jupyter.html#jupyuvi) +callable above or leave it blank to use the default (8000). + +``` python +# This doesn't display in the docs - uncomment and run it to see it in action +# HTMX() +``` + +We didn’t define the `/click` route, but that’s fine - we can define (or +change) it any time, and it’s dynamically inserted into the running app. +No need to restart or reload anything! + +``` python +@rt +def click(): return P('You clicked me!') +``` + +## Full screen view + +You can view your app outside of Jupyter by going to `localhost:PORT`, +where `PORT` is usually the default 8000, so in most cases just click +[this link](localhost:8000/). + +## Graceful shutdowns + +Use the `server.stop()` function displayed below. If you restart Jupyter +without calling this line the thread may not be released and the +[`HTMX`](https://www.fastht.ml/docs/api/jupyter.html#htmx) callable +above may throw errors. If that happens, a quick temporary fix is to +specify a different port number in JupyUvi and HTMX with the `port` +parameter. + +Cleaner solutions to the dangling thread are to kill the dangling thread +(dependant on each operating system) or restart the computer. + +``` python +server.stop() +``` diff --git a/docs/tutorials/quickstart-web-dev/quickstart-fasthtml.png b/docs/tutorials/quickstart-web-dev/quickstart-fasthtml.png new file mode 100644 index 0000000000000000000000000000000000000000..4f4e82b55bf7fddc3ae7d34c30aa20540b3f9853 Binary files /dev/null and b/docs/tutorials/quickstart-web-dev/quickstart-fasthtml.png differ diff --git a/docs/tutorials/quickstart_for_web_devs.html b/docs/tutorials/quickstart_for_web_devs.html new file mode 100644 index 0000000000000000000000000000000000000000..fefdb8707e3d29e483b104e63d2e3d6a59ca9cb0 --- /dev/null +++ b/docs/tutorials/quickstart_for_web_devs.html @@ -0,0 +1,2026 @@ + + + + + + + + + + +Web Devs Quickstart – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    + +
    + + +
    + + + +
    + +
    +
    +

    Web Devs Quickstart

    +
    + +
    +
    + A fast introduction to FastHTML for experienced web developers. +
    +
    + + +
    + + + + +
    + + + +
    + + + +
    +

    Installation

    +
    pip install python-fasthtml
    +
    +
    +

    A Minimal Application

    +

    A minimal FastHTML application looks something like this:

    +
    +
    +
    main.py
    +
    +
    1from fasthtml.common import *
    +
    +2app, rt = fast_app()
    +
    +3@rt("/")
    +4def get():
    +5    return Titled("FastHTML", P("Let's do this!"))
    +
    +6serve()
    +
    +
    +
    1
    +
    +We import what we need for rapid development! A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience. +
    +
    2
    +
    +We instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll take advantage of later in the tutorial. +
    +
    3
    +
    +We use the rt() decorator to tell FastHTML what to return when a user visits / in their browser. +
    +
    4
    +
    +We connect this route to HTTP GET requests by defining a view function called get(). +
    +
    5
    +
    +A tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach. +
    +
    6
    +
    +The serve() utility configures and runs FastHTML using a library called uvicorn. +
    +
    +

    Run the code:

    +
    python main.py
    +

    The terminal will look like this:

    +
    INFO:     Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
    +INFO:     Started reloader process [58058] using WatchFiles
    +INFO:     Started server process [58060]
    +INFO:     Waiting for application startup.
    +INFO:     Application startup complete.
    +

    Confirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:

    +

    +
    +
    +
    + +
    +
    +Note +
    +
    +
    +

    While some linters and developers will complain about the wildcard import, it is by design here and perfectly safe. FastHTML is very deliberate about the objects it exports in fasthtml.common. If it bothers you, you can import the objects you need individually, though it will make the code more verbose and less readable.

    +

    If you want to learn more about how FastHTML handles imports, we cover that here.

    +
    +
    +
    +
    +

    A Minimal Charting Application

    +

    The Script function allows you to include JavaScript. You can use Python to generate parts of your JS or JSON like this:

    +
    import json
    +from fasthtml.common import * 
    +
    +app, rt = fast_app(hdrs=(Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js"),))
    +
    +data = json.dumps({
    +    "data": [{"x": [1, 2, 3, 4],"type": "scatter"},
    +            {"x": [1, 2, 3, 4],"y": [16, 5, 11, 9],"type": "scatter"}],
    +    "title": "Plotly chart in FastHTML ",
    +    "description": "This is a demo dashboard",
    +    "type": "scatter"
    +})
    +
    +
    +@rt("/")
    +def get():
    +  return Titled("Chart Demo", Div(id="myDiv"),
    +    Script(f"var data = {data}; Plotly.newPlot('myDiv', data);"))
    +
    +serve()
    +
    +
    +

    Debug Mode

    +

    When we can’t figure out a bug in FastHTML, we can run it in DEBUG mode. When an error is thrown, the error screen is displayed in the browser. This error setting should never be used in a deployed app.

    +
    from fasthtml.common import *
    +
    +1app, rt = fast_app(debug=True)
    +
    +@rt("/")
    +def get():
    +2    1/0
    +    return Titled("FastHTML Error!", P("Let's error!"))
    +
    +serve()
    +
    +
    1
    +
    +debug=True sets debug mode on. +
    +
    2
    +
    +Python throws an error when it tries to divide an integer by zero. +
    +
    +
    +
    +

    Routing

    +

    FastHTML builds upon FastAPI’s friendly decorator pattern for specifying URLs, with extra features:

    +
    +
    +
    main.py
    +
    +
    from fasthtml.common import * 
    +
    +app, rt = fast_app()
    +
    +1@rt("/")
    +def get():
    +  return Titled("FastHTML", P("Let's do this!"))
    +
    +2@rt("/hello")
    +def get():
    +  return Titled("Hello, world!")
    +
    +serve()
    +
    +
    +
    1
    +
    +The “/” URL on line 5 is the home of a project. This would be accessed at 127.0.0.1:5001. +
    +
    2
    +
    +“/hello” URL on line 9 will be found by the project if the user visits 127.0.0.1:5001/hello. +
    +
    +
    +
    +
    + +
    +
    +Tip +
    +
    +
    +

    It looks like get() is being defined twice, but that’s not the case. Each function decorated with rt is totally separate, and is injected into the router. We’re not calling them in the module’s namespace (locals()). Rather, we’re loading them into the routing mechanism using the rt decorator.

    +
    +
    +

    You can do more! Read on to learn what we can do to make parts of the URL dynamic.

    +
    +
    +

    Variables in URLs

    +

    You can add variable sections to a URL by marking them with {variable_name}. Your function then receives the {variable_name} as a keyword argument, but only if it is the correct type. Here’s an example:

    +
    +
    +
    main.py
    +
    +
    from fasthtml.common import * 
    +
    +app, rt = fast_app()
    +
    +1@rt("/{name}/{age}")
    +2def get(name: str, age: int):
    +3  return Titled(f"Hello {name.title()}, age {age}")
    +
    +serve()
    +
    +
    +
    1
    +
    +We specify two variable names, name and age. +
    +
    2
    +
    +We define two function arguments named identically to the variables. You will note that we specify the Python types to be passed. +
    +
    3
    +
    +We use these functions in our project. +
    +
    +

    Try it out by going to this address: 127.0.0.1:5001/uma/5. You should get a page that says,

    +
    +

    “Hello Uma, age 5”.

    +
    +
    +

    What happens if we enter incorrect data?

    +

    The 127.0.0.1:5001/uma/5 URL works because 5 is an integer. If we enter something that is not, such as 127.0.0.1:5001/uma/five, then FastHTML will return an error instead of a web page.

    +
    +
    +
    + +
    +
    +FastHTML URL routing supports more complex types +
    +
    +
    +

    The two examples we provide here use Python’s built-in str and int types, but you can use your own types, including more complex ones such as those defined by libraries like attrs, pydantic, and even sqlmodel.

    +
    +
    +
    +
    +
    +

    HTTP Methods

    +

    FastHTML matches function names to HTTP methods. So far the URL routes we’ve defined have been for HTTP GET methods, the most common method for web pages.

    +

    Form submissions often are sent as HTTP POST. When dealing with more dynamic web page designs, also known as Single Page Apps (SPA for short), the need can arise for other methods such as HTTP PUT and HTTP DELETE. The way FastHTML handles this is by changing the function name.

    +
    +
    +
    main.py
    +
    +
    from fasthtml.common import * 
    +
    +app, rt = fast_app()
    +
    +@rt("/")  
    +1def get():
    +  return Titled("HTTP GET", P("Handle GET"))
    +
    +@rt("/")  
    +2def post():
    +  return Titled("HTTP POST", P("Handle POST"))
    +
    +serve()
    +
    +
    +
    1
    +
    +On line 6 because the get() function name is used, this will handle HTTP GETs going to the / URI. +
    +
    2
    +
    +On line 10 because the post() function name is used, this will handle HTTP POSTs going to the / URI. +
    +
    +
    +
    +

    CSS Files and Inline Styles

    +

    Here we modify default headers to demonstrate how to use the Sakura CSS microframework instead of FastHTML’s default of Pico CSS.

    +
    +
    +
    main.py
    +
    +
    from fasthtml.common import * 
    +
    +app, rt = fast_app(
    +1    pico=False,
    +    hdrs=(
    +        Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),
    +2        Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),
    +3        Style("p {color: red;}")
    +))
    +
    +@app.get("/")
    +def home():
    +    return Titled("FastHTML",
    +        P("Let's do this!"),
    +    )
    +
    +serve()
    +
    +
    +
    1
    +
    +By setting pico to False, FastHTML will not include pico.min.css. +
    +
    2
    +
    +This will generate an HTML <link> tag for sourcing the css for Sakura. +
    +
    3
    +
    +If you want an inline styles, the Style() function will put the result into the HTML. +
    +
    +
    +
    +

    Other Static Media File Locations

    +

    As you saw, Script and Link are specific to the most common static media use cases in web apps: including JavaScript, CSS, and images. But it also works with videos and other static media files. The default behavior is to look for these files in the root directory - typically we don’t do anything special to include them. We can change the default directory that is looked in for files by adding the static_path parameter to the fast_app function.

    +
    app, rt = fast_app(static_path='public')
    +

    FastHTML also allows us to define a route that uses FileResponse to serve the file at a specified path. This is useful for serving images, videos, and other media files from a different directory without having to change the paths of many files. So if we move the directory containing the media files, we only need to change the path in one place. In the example below, we call images from a directory called public.

    +
    @rt("/{fname:path}.{ext:static}")
    +async def get(fname:str, ext:str): 
    +    return FileResponse(f'public/{fname}.{ext}')
    +
    +
    +

    Rendering Markdown

    +
    from fasthtml.common import *
    +
    +hdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), )
    +
    +app, rt = fast_app(hdrs=hdrs)
    +
    +content = """
    +Here are some _markdown_ elements.
    +
    +- This is a list item
    +- This is another list item
    +- And this is a third list item
    +
    +**Fenced code blocks work here.**
    +"""
    +
    +@rt('/')
    +def get(req):
    +    return Titled("Markdown rendering example", Div(content,cls="marked"))
    +
    +serve()
    +
    +
    +

    Code highlighting

    +

    Here’s how to highlight code without any markdown configuration.

    +
    from fasthtml.common import *
    +
    +# Add the HighlightJS built-in header
    +hdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),)
    +
    +app, rt = fast_app(hdrs=hdrs)
    +
    +code_example = """
    +import datetime
    +import time
    +
    +for i in range(10):
    +    print(f"{datetime.datetime.now()}")
    +    time.sleep(1)
    +"""
    +
    +@rt('/')
    +def get(req):
    +    return Titled("Markdown rendering example",
    +        Div(
    +            # The code example needs to be surrounded by
    +            # Pre & Code elements
    +            Pre(Code(code_example))
    +    ))
    +
    +serve()
    +
    +
    +

    Defining new ft components

    +

    We can build our own ft components and combine them with other components. The simplest method is defining them as a function.

    +
    +
    from fasthtml.common import *
    +
    +
    +
    def hero(title, statement):
    +    return Div(H1(title),P(statement), cls="hero")
    +
    +# usage example
    +Main(
    +    hero("Hello World", "This is a hero statement")
    +)
    +
    +
    <main>  <div class="hero">
    +    <h1>Hello World</h1>
    +    <p>This is a hero statement</p>
    +  </div>
    +</main>
    +
    +
    +
    +

    Pass through components

    +

    For when we need to define a new component that allows zero-to-many components to be nested within them, we lean on Python’s *args and **kwargs mechanism. Useful for creating page layout controls.

    +
    +
    def layout(*args, **kwargs):
    +    """Dashboard layout for all our dashboard views"""
    +    return Main(
    +        H1("Dashboard"),
    +        Div(*args, **kwargs),
    +        cls="dashboard",
    +    )
    +
    +# usage example
    +layout(
    +    Ul(*[Li(o) for o in range(3)]),
    +    P("Some content", cls="description"),
    +)
    +
    +
    <main class="dashboard">  <h1>Dashboard</h1>
    +  <div>
    +    <ul>
    +      <li>0</li>
    +      <li>1</li>
    +      <li>2</li>
    +    </ul>
    +    <p class="description">Some content</p>
    +  </div>
    +</main>
    +
    +
    +
    +
    +

    Dataclasses as ft components

    +

    While functions are easy to read, for more complex components some might find it easier to use a dataclass.

    +
    +
    from dataclasses import dataclass
    +
    +@dataclass
    +class Hero:
    +    title: str
    +    statement: str
    +    
    +    def __ft__(self):
    +        """ The __ft__ method renders the dataclass at runtime."""
    +        return Div(H1(self.title),P(self.statement), cls="hero")
    +    
    +# usage example
    +Main(
    +    Hero("Hello World", "This is a hero statement")
    +)
    +
    +
    <main>  <div class="hero">
    +    <h1>Hello World</h1>
    +    <p>This is a hero statement</p>
    +  </div>
    +</main>
    +
    +
    +
    +
    +
    +

    Testing views in notebooks

    +

    Because of the ASGI event loop it is currently impossible to run FastHTML inside a notebook. However, we can still test the output of our views. To do this, we leverage Starlette, an ASGI toolkit that FastHTML uses.

    +
    +
    # First we instantiate our app, in this case we remove the
    +# default headers to reduce the size of the output.
    +app, rt = fast_app(default_hdrs=False)
    +
    +# Setting up the Starlette test client
    +from starlette.testclient import TestClient
    +client = TestClient(app)
    +
    +# Usage example
    +@rt("/")
    +def get():
    +    return Titled("FastHTML is awesome", 
    +        P("The fastest way to create web apps in Python"))
    +
    +print(client.get("/").text)
    +
    +
     <!doctype html>
    + <html>
    +   <head>
    +<title>FastHTML is awesome</title>   </head>
    +   <body>
    +<main class="container">       <h1>FastHTML is awesome</h1>
    +       <p>The fastest way to create web apps in Python</p>
    +</main>   </body>
    + </html>
    +
    +
    +
    +
    +
    +

    Forms

    +

    To validate data coming from users, first define a dataclass representing the data you want to check. Here’s an example representing a signup form.

    +
    +
    from dataclasses import dataclass
    +
    +@dataclass
    +class Profile: email:str; phone:str; age:int
    +
    +

    Create an FT component representing an empty version of that form. Don’t pass in any value to fill the form, that gets handled later.

    +
    +
    profile_form = Form(method="post", action="/profile")(
    +        Fieldset(
    +            Label('Email', Input(name="email")),
    +            Label("Phone", Input(name="phone")),
    +            Label("Age", Input(name="age")),
    +        ),
    +        Button("Save", type="submit"),
    +    )
    +profile_form
    +
    +
    <form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email      <input name="email">
    +</label><label>Phone      <input name="phone">
    +</label><label>Age      <input name="age">
    +</label></fieldset><button type="submit">Save</button></form>
    +
    +
    +

    Once the dataclass and form function are completed, we can add data to the form. To do that, instantiate the profile dataclass:

    +
    +
    profile = Profile(email='john@example.com', phone='123456789', age=5)
    +profile
    +
    +
    Profile(email='john@example.com', phone='123456789', age=5)
    +
    +
    +

    Then add that data to the profile_form using FastHTML’s fill_form class:

    +
    +
    fill_form(profile_form, profile)
    +
    +
    <form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email      <input name="email" value="john@example.com">
    +</label><label>Phone      <input name="phone" value="123456789">
    +</label><label>Age      <input name="age" value="5">
    +</label></fieldset><button type="submit">Save</button></form>
    +
    +
    +
    +

    Forms with views

    +

    The usefulness of FastHTML forms becomes more apparent when they are combined with FastHTML views. We’ll show how this works by using the test client from above. First, let’s create a SQlite database:

    +
    +
    db = database("profiles.db")
    +profiles = db.create(Profile, pk="email")
    +
    +

    Now we insert a record into the database:

    +
    +
    profiles.insert(profile)
    +
    +
    Profile(email='john@example.com', phone='123456789', age=5)
    +
    +
    +

    And we can then demonstrate in the code that form is filled and displayed to the user.

    +
    +
    @rt("/profile/{email}")
    +def profile(email:str):
    +1    profile = profiles[email]
    +2    filled_profile_form = fill_form(profile_form, profile)
    +    return Titled(f'Profile for {profile.email}', filled_profile_form)
    +
    +print(client.get(f"/profile/john@example.com").text)
    +
    +
    +
    1
    +
    +Fetch the profile using the profile table’s email primary key +
    +
    2
    +
    +Fill the form for display. +
    +
    +
    +
    +
     <!doctype html>
    + <html>
    +   <head>
    +<title>Profile for john@example.com</title>   </head>
    +   <body>
    +<main class="container">       <h1>Profile for john@example.com</h1>
    +<form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email             <input name="email" value="john@example.com">
    +</label><label>Phone             <input name="phone" value="123456789">
    +</label><label>Age             <input name="age" value="5">
    +</label></fieldset><button type="submit">Save</button></form></main>   </body>
    + </html>
    +
    +
    +
    +

    And now let’s demonstrate making a change to the data.

    +
    +
    @rt("/profile")
    +1def post(profile: Profile):
    +2    profiles.update(profile)
    +3    return RedirectResponse(url=f"/profile/{profile.email}")
    +
    +new_data = dict(email='john@example.com', phone='7654321', age=25)
    +4print(client.post("/profile", data=new_data).text)
    +
    +
    +
    1
    +
    +We use the Profile dataclass definition to set the type for the incoming profile content. This validates the field types for the incoming data +
    +
    2
    +
    +Taking our validated data, we updated the profiles table +
    +
    3
    +
    +We redirect the user back to their profile view +
    +
    4
    +
    +The display is of the profile form view showing the changes in data. +
    +
    +
    +
    +
     <!doctype html>
    + <html>
    +   <head>
    +<title>Profile for john@example.com</title>   </head>
    +   <body>
    +<main class="container">       <h1>Profile for john@example.com</h1>
    +<form enctype="multipart/form-data" method="post" action="/profile"><fieldset><label>Email             <input name="email" value="john@example.com">
    +</label><label>Phone             <input name="phone" value="7654321">
    +</label><label>Age             <input name="age" value="25">
    +</label></fieldset><button type="submit">Save</button></form></main>   </body>
    + </html>
    +
    +
    +
    +
    +
    +
    +

    Strings and conversion order

    +

    The general rules for rendering are: - __ft__ method will be called (for default components like P, H2, etc. or if you define your own components) - If you pass a string, it will be escaped - On other python objects, str() will be called

    +

    As a consequence, if you want to include plain HTML tags directly into e.g. a Div() they will get escaped by default (as a security measure to avoid code injections). This can be avoided by using NotStr(), a convenient way to reuse python code that returns already HTML. If you use pandas, you can use pandas.DataFrame.to_html() to get a nice table. To include the output a FastHTML, wrap it in NotStr(), like Div(NotStr(df.to_html())).

    +

    Above we saw how a dataclass behaves with the __ft__ method defined. On a plain dataclass, str() will be called (but not escaped).

    +
    +
    from dataclasses import dataclass
    +
    +@dataclass
    +class Hero:
    +    title: str
    +    statement: str
    +        
    +# rendering the dataclass with the default method
    +Main(
    +    Hero("<h1>Hello World</h1>", "This is a hero statement")
    +)
    +
    +
    <main>Hero(title='<h1>Hello World</h1>', statement='This is a hero statement')</main>
    +
    +
    +
    +
    # This will display the HTML as text on your page
    +Div("Let's include some HTML here: <div>Some HTML</div>")
    +
    +
    <div>Let&#x27;s include some HTML here: &lt;div&gt;Some HTML&lt;/div&gt;</div>
    +
    +
    +
    +
    # Keep the string untouched, will be rendered on the page
    +Div(NotStr("<div><h1>Some HTML</h1></div>"))
    +
    +
    <div><div><h1>Some HTML</h1></div></div>
    +
    +
    +
    +
    +

    Custom exception handlers

    +

    FastHTML allows customization of exception handlers, but does so gracefully. What this means is by default it includes all the <html> tags needed to display attractive content. Try it out!

    +
    from fasthtml.common import *
    +
    +def not_found(req, exc): return Titled("404: I don't exist!")
    +
    +exception_handlers = {404: not_found}
    +
    +app, rt = fast_app(exception_handlers=exception_handlers)
    +
    +@rt('/')
    +def get():
    +    return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
    +
    +serve()
    +

    We can also use lambda to make things more terse:

    +
    from fasthtml.common import *
    +
    +exception_handlers={
    +    404: lambda req, exc: Titled("404: I don't exist!"),
    +    418: lambda req, exc: Titled("418: I'm a teapot!")
    +}
    +
    +app, rt = fast_app(exception_handlers=exception_handlers)
    +
    +@rt('/')
    +def get():
    +    return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
    +
    +serve()
    +
    +
    +

    Cookies

    +

    We can set cookies using the cookie() function. In our example, we’ll create a timestamp cookie.

    +
    +
    from datetime import datetime
    +from IPython.display import HTML
    +
    +
    +
    @rt("/settimestamp")
    +def get(req):
    +    now = datetime.now()
    +    return P(f'Set to {now}'), cookie('now', datetime.now())
    +
    +HTML(client.get('/settimestamp').text)
    +
    + + + +FastHTML page + +

    Set to 2024-09-26 15:33:48.141869

    + + +
    +
    +

    Now let’s get it back using the same name for our parameter as the cookie name.

    +
    +
    @rt('/gettimestamp')
    +def get(now:parsed_date): return f'Cookie was set at time {now.time()}'
    +
    +client.get('/gettimestamp').text
    +
    +
    'Cookie was set at time 15:33:48.141903'
    +
    +
    +
    +
    +

    Sessions

    +

    For convenience and security, FastHTML has a mechanism for storing small amounts of data in the user’s browser. We can do this by adding a session argument to routes. FastHTML sessions are Python dictionaries, and we can leverage to our benefit. The example below shows how to concisely set and get sessions.

    +
    +
    @rt('/adder/{num}')
    +def get(session, num: int):
    +    session.setdefault('sum', 0)
    +    session['sum'] = session.get('sum') + num
    +    return Response(f'The sum is {session["sum"]}.')
    +
    +
    +
    +

    Toasts (also known as Messages)

    +

    Toasts, sometimes called “Messages” are small notifications usually in colored boxes used to notify users that something has happened. Toasts can be of four types:

    +
      +
    • info
    • +
    • success
    • +
    • warning
    • +
    • error
    • +
    +

    Examples toasts might include:

    +
      +
    • “Payment accepted”
    • +
    • “Data submitted”
    • +
    • “Request approved”
    • +
    +

    Toasts require the use of the setup_toasts() function plus every view needs these two features:

    +
      +
    • The session argument
    • +
    • Must return FT components
    • +
    +
    1setup_toasts(app)
    +
    +@rt('/toasting')
    +2def get(session):
    +    # Normally one toast is enough, this allows us to see
    +    # different toast types in action.
    +    add_toast(session, f"Toast is being cooked", "info")
    +    add_toast(session, f"Toast is ready", "success")
    +    add_toast(session, f"Toast is getting a bit crispy", "warning")
    +    add_toast(session, f"Toast is burning!", "error")
    +3    return Titled("I like toast")
    +
    +
    1
    +
    +setup_toasts is a helper function that adds toast dependencies. Usually this would be declared right after fast_app() +
    +
    2
    +
    +Toasts require sessions +
    +
    3
    +
    +Views with Toasts must return FT or FtResponse components. +
    +
    +

    💡 setup_toasts takes a duration input that allows you to specify how long a toast will be visible before disappearing. For example setup_toasts(duration=5) sets the toasts duration to 5 seconds. By default toasts disappear after 10 seconds.

    +

    ⚠️ Toasts don’t work with SPA like navigation that replaces the entire body such as this navigation trigger A('About', hx_get="/about", hx_swap="outerHTML", hx_push_url="true", hx_target="body"). As an alternative, wrap the content of your route in an element containing an id and set this id as the target for your navigation trigger (i.e. hx_target='#container_id').

    +
    +
    +

    Authentication and authorization

    +

    In FastHTML the tasks of authentication and authorization are handled with Beforeware. Beforeware are functions that run before the route handler is called. They are useful for global tasks like ensuring users are authenticated or have permissions to access a view.

    +

    First, we write a function that accepts a request and session arguments:

    +
    +
    # Status code 303 is a redirect that can change POST to GET,
    +# so it's appropriate for a login page.
    +login_redir = RedirectResponse('/login', status_code=303)
    +
    +def user_auth_before(req, sess):
    +    # The `auth` key in the request scope is automatically provided
    +    # to any handler which requests it, and can not be injected
    +    # by the user using query params, cookies, etc, so it should
    +    # be secure to use.    
    +    auth = req.scope['auth'] = sess.get('auth', None)
    +    # If the session key is not there, it redirects to the login page.
    +    if not auth: return login_redir
    +
    +

    Now we pass our user_auth_before function as the first argument into a Beforeware class. We also pass a list of regular expressions to the skip argument, designed to allow users to still get to the home and login pages.

    +
    +
    beforeware = Beforeware(
    +    user_auth_before,
    +    skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/']
    +)
    +
    +app, rt = fast_app(before=beforeware)
    +
    +
    +
    +

    Server-sent events (SSE)

    +

    With server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. Unlike WebSockets, SSE can only go in one direction: server to client. SSE is also part of the HTTP specification unlike WebSockets which uses its own specification.

    +

    FastHTML introduces several tools for working with SSE which are covered in the example below. While concise, there’s a lot going on in this function so we’ve annotated it quite a bit.

    +
    +
    import random
    +from asyncio import sleep
    +from fasthtml.common import *
    +
    +1hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"),)
    +app,rt = fast_app(hdrs=hdrs)
    +
    +@rt
    +def index():
    +    return Titled("SSE Random Number Generator",
    +        P("Generate pairs of random numbers, as the list grows scroll downwards."),
    +2        Div(hx_ext="sse",
    +3            sse_connect="/number-stream",
    +4            hx_swap="beforeend show:bottom",
    +5            sse_swap="message"))
    +
    +6shutdown_event = signal_shutdown()
    +
    +7async def number_generator():
    +8    while not shutdown_event.is_set():
    +        data = Article(random.randint(1, 100))
    +9        yield sse_message(data)
    +        await sleep(1)
    +
    +@rt("/number-stream")
    +10async def get(): return EventStream(number_generator())
    +
    +
    +
    1
    +
    +Import the HTMX SSE extension +
    +
    2
    +
    +Tell HTMX to load the SSE extension +
    +
    3
    +
    +Look at the /number-stream endpoint for SSE content +
    +
    4
    +
    +When new items come in from the SSE endpoint, add them at the end of the current content within the div. If they go beyond the screen, scroll downwards +
    +
    5
    +
    +Specify the name of the event. FastHTML’s default event name is “message”. Only change if you have more than one call to SSE endpoints within a view +
    +
    6
    +
    +Set up the asyncio event loop +
    +
    7
    +
    +Don’t forget to make this an async function! +
    +
    8
    +
    +Iterate through the asyncio event loop +
    +
    9
    +
    +We yield the data. Data ideally should be comprised of FT components as that plugs nicely into HTMX in the browser +
    +
    10
    +
    +The endpoint view needs to be an async function that returns a EventStream +
    +
    +
    +
    +
    +
    +

    Websockets

    +

    With websockets we can have bi-directional communications between a browser and client. Websockets are useful for things like chat and certain types of games. While websockets can be used for single direction messages from the server (i.e. telling users that a process is finished), that task is arguably better suited for SSE.

    +

    FastHTML provides useful tools for adding websockets to your pages.

    +
    +
    from fasthtml.common import *
    +from asyncio import sleep
    +
    +1app, rt = fast_app(exts='ws')
    +
    +2def mk_inp(): return Input(id='msg', autofocus=True)
    +
    +@rt('/')
    +async def get(request):
    +    cts = Div(
    +        Div(id='notifications'),
    +3        Form(mk_inp(), id='form', ws_send=True),
    +4        hx_ext='ws', ws_connect='/ws')
    +    return Titled('Websocket Test', cts)
    +
    +5async def on_connect(send):
    +    print('Connected!')
    +6    await send(Div('Hello, you have connected', id="notifications"))
    +
    +7async def on_disconnect(ws):
    +    print('Disconnected!')
    +
    +8@app.ws('/ws', conn=on_connect, disconn=on_disconnect)
    +9async def ws(msg:str, send):
    +10    await send(Div('Hello ' + msg, id="notifications"))
    +    await sleep(2)
    +11    return Div('Goodbye ' + msg, id="notifications"), mk_inp()
    +
    +
    +
    1
    +
    +To use websockets in FastHTML, you must instantiate the app with exts set to ‘ws’ +
    +
    2
    +
    +As we want to use websockets to reset the form, we define the mk_input function that can be called from multiple locations +
    +
    3
    +
    +We create the form and mark it with the ws_send attribute, which is documented here in the HTMX websocket specification. This tells HTMX to send a message to the nearest websocket based on the trigger for the form element, which for forms is pressing the enter key, an action considered to be a form submission +
    +
    4
    +
    +This is where the HTMX extension is loaded (hx_ext='ws') and the nearest websocket is defined (ws_connect='/ws') +
    +
    5
    +
    +When a websocket first connects we can optionally have it call a function that accepts a send argument. The send argument will push a message to the browser. +
    +
    6
    +
    +Here we use the send function that was passed into the on_connect function to send a Div with an id of notifications that HTMX assigns to the element in the page that already has an id of notifications +
    +
    7
    +
    +When a websocket disconnects we can call a function which takes no arguments. Typically the role of this function is to notify the server to take an action. In this case, we print a simple message to the console +
    +
    8
    +
    +We use the app.ws decorator to mark that /ws is the route for our websocket. We also pass in the two optional conn and disconn parameters to this decorator. As a fun experiment, remove the conn and disconn arguments and see what happens +
    +
    9
    +
    +Define the ws function as async. This is necessary for ASGI to be able to serve websockets. The function accepts two arguments, a msg that is user input from the browser, and a send function for pushing data back to the browser +
    +
    10
    +
    +The send function is used here to send HTML back to the page. As the HTML has an id of notifications, HTMX will overwrite what is already on the page with the same ID +
    +
    11
    +
    +The websocket function can also be used to return a value. In this case, it is a tuple of two HTML elements. HTMX will take the elements and replace them where appropriate. As both have id specified (notifications and msg respectively), they will replace their predecessor on the page. +
    +
    +
    +
    +
    +
    +

    File Uploads

    +

    A common task in web development is uploading files. The examples below are for uploading files to the hosting server, with information about the uploaded file presented to the user.

    +
    +
    +
    + +
    +
    +File uploads in production can be dangerous +
    +
    +
    +

    File uploads can be the target of abuse, accidental or intentional. That means users may attempt to upload files that are too large or present a security risk. This is especially of concern for public facing apps. File upload security is outside the scope of this tutorial, for now we suggest reading the OWASP File Upload Cheat Sheet.

    +
    +
    +
    +

    Single File Uploads

    +
    from fasthtml.common import *
    +from pathlib import Path
    +
    +app, rt = fast_app()
    +
    +upload_dir = Path("filez")
    +upload_dir.mkdir(exist_ok=True)
    +
    +@rt('/')
    +def get():
    +    return Titled("File Upload Demo",
    +        Article(
    +1            Form(hx_post=upload, hx_target="#result-one")(
    +2                Input(type="file", name="file"),
    +                Button("Upload", type="submit", cls='secondary'),
    +            ),
    +            Div(id="result-one")
    +        )
    +    )
    +
    +def FileMetaDataCard(file):
    +    return Article(
    +        Header(H3(file.filename)),
    +        Ul(
    +            Li('Size: ', file.size),            
    +            Li('Content Type: ', file.content_type),
    +            Li('Headers: ', file.headers),
    +        )
    +    )    
    +
    +@rt
    +3async def upload(file: UploadFile):
    +4    card = FileMetaDataCard(file)
    +5    filebuffer = await file.read()
    +6    (upload_dir / file.filename).write_bytes(filebuffer)
    +    return card
    +
    +serve()
    +
    +
    1
    +
    +Every form rendered with the Form FT component defaults to enctype="multipart/form-data" +
    +
    2
    +
    +Don’t forget to set the Input FT Component’s type to file +
    +
    3
    +
    +The upload view should receive a Starlette UploadFile type. You can add other form variables +
    +
    4
    +
    +We can access the metadata of the card (filename, size, content_type, headers), a quick and safe process. We set that to the card variable +
    +
    5
    +
    +In order to access the contents contained within a file we use the await method to read() it. As files may be quite large or contain bad data, this is a seperate step from accessing metadata +
    +
    6
    +
    +This step shows how to use Python’s built-in pathlib.Path library to write the file to disk. +
    +
    +
    +
    +

    Multiple File Uploads

    +
    from fasthtml.common import *
    +from pathlib import Path
    +
    +app, rt = fast_app()
    +
    +upload_dir = Path("filez")
    +upload_dir.mkdir(exist_ok=True)
    +
    +@rt('/')
    +def get():
    +    return Titled("Multiple File Upload Demo",
    +        Article(
    +1            Form(hx_post=upload_many, hx_target="#result-many")(
    +2                Input(type="file", name="files", multiple=True),
    +                Button("Upload", type="submit", cls='secondary'),
    +            ),
    +            Div(id="result-many")
    +        )
    +    )
    +
    +def FileMetaDataCard(file):
    +    return Article(
    +        Header(H3(file.filename)),
    +        Ul(
    +            Li('Size: ', file.size),            
    +            Li('Content Type: ', file.content_type),
    +            Li('Headers: ', file.headers),
    +        )
    +    )    
    +
    +@rt
    +3async def upload_many(files: list[UploadFile]):
    +    cards = []
    +4    for file in files:
    +5        cards.append(FileMetaDataCard(file))
    +6        filebuffer = await file.read()
    +7        (upload_dir / file.filename).write_bytes(filebuffer)
    +    return cards
    +
    +serve()
    +
    +
    1
    +
    +Every form rendered with the Form FT component defaults to enctype="multipart/form-data" +
    +
    2
    +
    +Don’t forget to set the Input FT Component’s type to file and assign the multiple attribute to True +
    +
    3
    +
    +The upload view should receive a list containing the Starlette UploadFile type. You can add other form variables +
    +
    4
    +
    +Iterate through the files +
    +
    5
    +
    +We can access the metadata of the card (filename, size, content_type, headers), a quick and safe process. We add that to the cards variable +
    +
    6
    +
    +In order to access the contents contained within a file we use the await method to read() it. As files may be quite large or contain bad data, this is a seperate step from accessing metadata +
    +
    7
    +
    +This step shows how to use Python’s built-in pathlib.Path library to write the file to disk. +
    +
    + + +
    +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/docs/tutorials/quickstart_for_web_devs.html.md b/docs/tutorials/quickstart_for_web_devs.html.md new file mode 100644 index 0000000000000000000000000000000000000000..0aef5e19a948f5e1349c5fe7d47c6b70823f8792 --- /dev/null +++ b/docs/tutorials/quickstart_for_web_devs.html.md @@ -0,0 +1,1309 @@ +# Web Devs Quickstart + + + + +## Installation + +``` bash +pip install python-fasthtml +``` + +## A Minimal Application + +A minimal FastHTML application looks something like this: + +
    + +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/") +def get(): + return Titled("FastHTML", P("Let's do this!")) + +serve() +``` + +
    + +Line 1 +We import what we need for rapid development! A carefully-curated set of +FastHTML functions and other Python objects is brought into our global +namespace for convenience. + +Line 3 +We instantiate a FastHTML app with the `fast_app()` utility function. +This provides a number of really useful defaults that we’ll take +advantage of later in the tutorial. + +Line 5 +We use the `rt()` decorator to tell FastHTML what to return when a user +visits `/` in their browser. + +Line 6 +We connect this route to HTTP GET requests by defining a view function +called `get()`. + +Line 7 +A tree of Python function calls that return all the HTML required to +write a properly formed web page. You’ll soon see the power of this +approach. + +Line 9 +The [`serve()`](https://www.fastht.ml/docs/api/core.html#serve) utility +configures and runs FastHTML using a library called `uvicorn`. + +Run the code: + +``` bash +python main.py +``` + +The terminal will look like this: + +``` bash +INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit) +INFO: Started reloader process [58058] using WatchFiles +INFO: Started server process [58060] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +Confirm FastHTML is running by opening your web browser to +[127.0.0.1:5001](http://127.0.0.1:5001). You should see something like +the image below: + +![](quickstart-web-dev/quickstart-fasthtml.png) + +
    + +> **Note** +> +> While some linters and developers will complain about the wildcard +> import, it is by design here and perfectly safe. FastHTML is very +> deliberate about the objects it exports in `fasthtml.common`. If it +> bothers you, you can import the objects you need individually, though +> it will make the code more verbose and less readable. +> +> If you want to learn more about how FastHTML handles imports, we cover +> that +> [here](https://www.fastht.ml/docs/explains/faq.html#why-use-import). + +
    + +## A Minimal Charting Application + +The [`Script`](https://www.fastht.ml/docs/api/xtend.html#script) +function allows you to include JavaScript. You can use Python to +generate parts of your JS or JSON like this: + +``` python +import json +from fasthtml.common import * + +app, rt = fast_app(hdrs=(Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js"),)) + +data = json.dumps({ + "data": [{"x": [1, 2, 3, 4],"type": "scatter"}, + {"x": [1, 2, 3, 4],"y": [16, 5, 11, 9],"type": "scatter"}], + "title": "Plotly chart in FastHTML ", + "description": "This is a demo dashboard", + "type": "scatter" +}) + + +@rt("/") +def get(): + return Titled("Chart Demo", Div(id="myDiv"), + Script(f"var data = {data}; Plotly.newPlot('myDiv', data);")) + +serve() +``` + +## Debug Mode + +When we can’t figure out a bug in FastHTML, we can run it in `DEBUG` +mode. When an error is thrown, the error screen is displayed in the +browser. This error setting should never be used in a deployed app. + +``` python +from fasthtml.common import * + +app, rt = fast_app(debug=True) + +@rt("/") +def get(): + 1/0 + return Titled("FastHTML Error!", P("Let's error!")) + +serve() +``` + +Line 3 +`debug=True` sets debug mode on. + +Line 7 +Python throws an error when it tries to divide an integer by zero. + +## Routing + +FastHTML builds upon FastAPI’s friendly decorator pattern for specifying +URLs, with extra features: + +
    + +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/") +def get(): + return Titled("FastHTML", P("Let's do this!")) + +@rt("/hello") +def get(): + return Titled("Hello, world!") + +serve() +``` + +
    + +Line 5 +The “/” URL on line 5 is the home of a project. This would be accessed +at [127.0.0.1:5001](http://127.0.0.1:5001). + +Line 9 +“/hello” URL on line 9 will be found by the project if the user visits +[127.0.0.1:5001/hello](http://127.0.0.1:5001/hello). + +
    + +> **Tip** +> +> It looks like `get()` is being defined twice, but that’s not the case. +> Each function decorated with `rt` is totally separate, and is injected +> into the router. We’re not calling them in the module’s namespace +> (`locals()`). Rather, we’re loading them into the routing mechanism +> using the `rt` decorator. + +
    + +You can do more! Read on to learn what we can do to make parts of the +URL dynamic. + +## Variables in URLs + +You can add variable sections to a URL by marking them with +`{variable_name}`. Your function then receives the `{variable_name}` as +a keyword argument, but only if it is the correct type. Here’s an +example: + +
    + +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/{name}/{age}") +def get(name: str, age: int): + return Titled(f"Hello {name.title()}, age {age}") + +serve() +``` + +
    + +Line 5 +We specify two variable names, `name` and `age`. + +Line 6 +We define two function arguments named identically to the variables. You +will note that we specify the Python types to be passed. + +Line 7 +We use these functions in our project. + +Try it out by going to this address: +[127.0.0.1:5001/uma/5](http://127.0.0.1:5001/uma/5). You should get a +page that says, + +> “Hello Uma, age 5”. + +### What happens if we enter incorrect data? + +The [127.0.0.1:5001/uma/5](http://127.0.0.1:5001/uma/5) URL works +because `5` is an integer. If we enter something that is not, such as +[127.0.0.1:5001/uma/five](http://127.0.0.1:5001/uma/five), then FastHTML +will return an error instead of a web page. + +
    + +> **FastHTML URL routing supports more complex types** +> +> The two examples we provide here use Python’s built-in `str` and `int` +> types, but you can use your own types, including more complex ones +> such as those defined by libraries like +> [attrs](https://pypi.org/project/attrs/), +> [pydantic](https://pypi.org/project/pydantic/), and even +> [sqlmodel](https://pypi.org/project/sqlmodel/). + +
    + +## HTTP Methods + +FastHTML matches function names to HTTP methods. So far the URL routes +we’ve defined have been for HTTP GET methods, the most common method for +web pages. + +Form submissions often are sent as HTTP POST. When dealing with more +dynamic web page designs, also known as Single Page Apps (SPA for +short), the need can arise for other methods such as HTTP PUT and HTTP +DELETE. The way FastHTML handles this is by changing the function name. + +
    + +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/") +def get(): + return Titled("HTTP GET", P("Handle GET")) + +@rt("/") +def post(): + return Titled("HTTP POST", P("Handle POST")) + +serve() +``` + +
    + +Line 6 +On line 6 because the `get()` function name is used, this will handle +HTTP GETs going to the `/` URI. + +Line 10 +On line 10 because the +[`post()`](https://www.fastht.ml/docs/explains/stripe.html#post) +function name is used, this will handle HTTP POSTs going to the `/` URI. + +## CSS Files and Inline Styles + +Here we modify default headers to demonstrate how to use the [Sakura CSS +microframework](https://github.com/oxalorg/sakura) instead of FastHTML’s +default of Pico CSS. + +
    + +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app( + pico=False, + hdrs=( + Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'), + Link(rel='stylesheet', href='assets/sakura.css', type='text/css'), + Style("p {color: red;}") +)) + +@app.get("/") +def home(): + return Titled("FastHTML", + P("Let's do this!"), + ) + +serve() +``` + +
    + +Line 4 +By setting `pico` to `False`, FastHTML will not include `pico.min.css`. + +Line 7 +This will generate an HTML `` tag for sourcing the css for Sakura. + +Line 8 +If you want an inline styles, the +[`Style()`](https://www.fastht.ml/docs/api/xtend.html#style) function +will put the result into the HTML. + +## Other Static Media File Locations + +As you saw, [`Script`](https://www.fastht.ml/docs/api/xtend.html#script) +and `Link` are specific to the most common static media use cases in web +apps: including JavaScript, CSS, and images. But it also works with +videos and other static media files. The default behavior is to look for +these files in the root directory - typically we don’t do anything +special to include them. We can change the default directory that is +looked in for files by adding the `static_path` parameter to the +`fast_app` function. + +``` python +app, rt = fast_app(static_path='public') +``` + +FastHTML also allows us to define a route that uses `FileResponse` to +serve the file at a specified path. This is useful for serving images, +videos, and other media files from a different directory without having +to change the paths of many files. So if we move the directory +containing the media files, we only need to change the path in one +place. In the example below, we call images from a directory called +`public`. + +``` python +@rt("/{fname:path}.{ext:static}") +async def get(fname:str, ext:str): + return FileResponse(f'public/{fname}.{ext}') +``` + +## Rendering Markdown + +``` python +from fasthtml.common import * + +hdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), ) + +app, rt = fast_app(hdrs=hdrs) + +content = """ +Here are some _markdown_ elements. + +- This is a list item +- This is another list item +- And this is a third list item + +**Fenced code blocks work here.** +""" + +@rt('/') +def get(req): + return Titled("Markdown rendering example", Div(content,cls="marked")) + +serve() +``` + +## Code highlighting + +Here’s how to highlight code without any markdown configuration. + +``` python +from fasthtml.common import * + +# Add the HighlightJS built-in header +hdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),) + +app, rt = fast_app(hdrs=hdrs) + +code_example = """ +import datetime +import time + +for i in range(10): + print(f"{datetime.datetime.now()}") + time.sleep(1) +""" + +@rt('/') +def get(req): + return Titled("Markdown rendering example", + Div( + # The code example needs to be surrounded by + # Pre & Code elements + Pre(Code(code_example)) + )) + +serve() +``` + +## Defining new `ft` components + +We can build our own `ft` components and combine them with other +components. The simplest method is defining them as a function. + +``` python +from fasthtml.common import * +``` + +``` python +def hero(title, statement): + return Div(H1(title),P(statement), cls="hero") + +# usage example +Main( + hero("Hello World", "This is a hero statement") +) +``` + +``` html +
    +

    Hello World

    +

    This is a hero statement

    +
    +
    +``` + +### Pass through components + +For when we need to define a new component that allows zero-to-many +components to be nested within them, we lean on Python’s `*args` and +`**kwargs` mechanism. Useful for creating page layout controls. + +``` python +def layout(*args, **kwargs): + """Dashboard layout for all our dashboard views""" + return Main( + H1("Dashboard"), + Div(*args, **kwargs), + cls="dashboard", + ) + +# usage example +layout( + Ul(*[Li(o) for o in range(3)]), + P("Some content", cls="description"), +) +``` + +``` html +

    Dashboard

    +
    +
      +
    • 0
    • +
    • 1
    • +
    • 2
    • +
    +

    Some content

    +
    +
    +``` + +### Dataclasses as ft components + +While functions are easy to read, for more complex components some might +find it easier to use a dataclass. + +``` python +from dataclasses import dataclass + +@dataclass +class Hero: + title: str + statement: str + + def __ft__(self): + """ The __ft__ method renders the dataclass at runtime.""" + return Div(H1(self.title),P(self.statement), cls="hero") + +# usage example +Main( + Hero("Hello World", "This is a hero statement") +) +``` + +``` html +
    +

    Hello World

    +

    This is a hero statement

    +
    +
    +``` + +## Testing views in notebooks + +Because of the ASGI event loop it is currently impossible to run +FastHTML inside a notebook. However, we can still test the output of our +views. To do this, we leverage Starlette, an ASGI toolkit that FastHTML +uses. + +``` python +# First we instantiate our app, in this case we remove the +# default headers to reduce the size of the output. +app, rt = fast_app(default_hdrs=False) + +# Setting up the Starlette test client +from starlette.testclient import TestClient +client = TestClient(app) + +# Usage example +@rt("/") +def get(): + return Titled("FastHTML is awesome", + P("The fastest way to create web apps in Python")) + +print(client.get("/").text) +``` + + + + + FastHTML is awesome + +

    FastHTML is awesome

    +

    The fastest way to create web apps in Python

    +
    + + +## Forms + +To validate data coming from users, first define a dataclass +representing the data you want to check. Here’s an example representing +a signup form. + +``` python +from dataclasses import dataclass + +@dataclass +class Profile: email:str; phone:str; age:int +``` + +Create an FT component representing an empty version of that form. Don’t +pass in any value to fill the form, that gets handled later. + +``` python +profile_form = Form(method="post", action="/profile")( + Fieldset( + Label('Email', Input(name="email")), + Label("Phone", Input(name="phone")), + Label("Age", Input(name="age")), + ), + Button("Save", type="submit"), + ) +profile_form +``` + +``` html +
    +``` + +Once the dataclass and form function are completed, we can add data to +the form. To do that, instantiate the profile dataclass: + +``` python +profile = Profile(email='john@example.com', phone='123456789', age=5) +profile +``` + + Profile(email='john@example.com', phone='123456789', age=5) + +Then add that data to the `profile_form` using FastHTML’s +[`fill_form`](https://www.fastht.ml/docs/api/components.html#fill_form) +class: + +``` python +fill_form(profile_form, profile) +``` + +``` html +
    +``` + +### Forms with views + +The usefulness of FastHTML forms becomes more apparent when they are +combined with FastHTML views. We’ll show how this works by using the +test client from above. First, let’s create a SQlite database: + +``` python +db = database("profiles.db") +profiles = db.create(Profile, pk="email") +``` + +Now we insert a record into the database: + +``` python +profiles.insert(profile) +``` + + Profile(email='john@example.com', phone='123456789', age=5) + +And we can then demonstrate in the code that form is filled and +displayed to the user. + +``` python +@rt("/profile/{email}") +def profile(email:str): + profile = profiles[email] + filled_profile_form = fill_form(profile_form, profile) + return Titled(f'Profile for {profile.email}', filled_profile_form) + +print(client.get(f"/profile/john@example.com").text) +``` + +Line 3 +Fetch the profile using the profile table’s `email` primary key + +Line 4 +Fill the form for display. + + + + + + + Profile for john@example.com + +

    Profile for john@example.com

    +
    + + +And now let’s demonstrate making a change to the data. + +``` python +@rt("/profile") +def post(profile: Profile): + profiles.update(profile) + return RedirectResponse(url=f"/profile/{profile.email}") + +new_data = dict(email='john@example.com', phone='7654321', age=25) +print(client.post("/profile", data=new_data).text) +``` + +Line 2 +We use the `Profile` dataclass definition to set the type for the +incoming `profile` content. This validates the field types for the +incoming data + +Line 3 +Taking our validated data, we updated the profiles table + +Line 4 +We redirect the user back to their profile view + +Line 7 +The display is of the profile form view showing the changes in data. + + + + + + + Profile for john@example.com + +

    Profile for john@example.com

    +
    + + +## Strings and conversion order + +The general rules for rendering are: - `__ft__` method will be called +(for default components like `P`, `H2`, etc. or if you define your own +components) - If you pass a string, it will be escaped - On other python +objects, `str()` will be called + +As a consequence, if you want to include plain HTML tags directly into +e.g. a `Div()` they will get escaped by default (as a security measure +to avoid code injections). This can be avoided by using `NotStr()`, a +convenient way to reuse python code that returns already HTML. If you +use pandas, you can use `pandas.DataFrame.to_html()` to get a nice +table. To include the output a FastHTML, wrap it in `NotStr()`, like +`Div(NotStr(df.to_html()))`. + +Above we saw how a dataclass behaves with the `__ft__` method defined. +On a plain dataclass, `str()` will be called (but not escaped). + +``` python +from dataclasses import dataclass + +@dataclass +class Hero: + title: str + statement: str + +# rendering the dataclass with the default method +Main( + Hero("

    Hello World

    ", "This is a hero statement") +) +``` + +``` html +
    Hero(title='

    Hello World

    ', statement='This is a hero statement')
    +``` + +``` python +# This will display the HTML as text on your page +Div("Let's include some HTML here:
    Some HTML
    ") +``` + +``` html +
    Let's include some HTML here: <div>Some HTML</div>
    +``` + +``` python +# Keep the string untouched, will be rendered on the page +Div(NotStr("

    Some HTML

    ")) +``` + +``` html +

    Some HTML

    +``` + +## Custom exception handlers + +FastHTML allows customization of exception handlers, but does so +gracefully. What this means is by default it includes all the `` +tags needed to display attractive content. Try it out! + +``` python +from fasthtml.common import * + +def not_found(req, exc): return Titled("404: I don't exist!") + +exception_handlers = {404: not_found} + +app, rt = fast_app(exception_handlers=exception_handlers) + +@rt('/') +def get(): + return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error")))) + +serve() +``` + +We can also use lambda to make things more terse: + +``` python +from fasthtml.common import * + +exception_handlers={ + 404: lambda req, exc: Titled("404: I don't exist!"), + 418: lambda req, exc: Titled("418: I'm a teapot!") +} + +app, rt = fast_app(exception_handlers=exception_handlers) + +@rt('/') +def get(): + return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error")))) + +serve() +``` + +## Cookies + +We can set cookies using the +[`cookie()`](https://www.fastht.ml/docs/api/core.html#cookie) function. +In our example, we’ll create a `timestamp` cookie. + +``` python +from datetime import datetime +from IPython.display import HTML +``` + +``` python +@rt("/settimestamp") +def get(req): + now = datetime.now() + return P(f'Set to {now}'), cookie('now', datetime.now()) + +HTML(client.get('/settimestamp').text) +``` + + + + +FastHTML page + +

    Set to 2024-09-26 15:33:48.141869

    + + + +Now let’s get it back using the same name for our parameter as the +cookie name. + +``` python +@rt('/gettimestamp') +def get(now:parsed_date): return f'Cookie was set at time {now.time()}' + +client.get('/gettimestamp').text +``` + + 'Cookie was set at time 15:33:48.141903' + +## Sessions + +For convenience and security, FastHTML has a mechanism for storing small +amounts of data in the user’s browser. We can do this by adding a +`session` argument to routes. FastHTML sessions are Python dictionaries, +and we can leverage to our benefit. The example below shows how to +concisely set and get sessions. + +``` python +@rt('/adder/{num}') +def get(session, num: int): + session.setdefault('sum', 0) + session['sum'] = session.get('sum') + num + return Response(f'The sum is {session["sum"]}.') +``` + +## Toasts (also known as Messages) + +Toasts, sometimes called “Messages” are small notifications usually in +colored boxes used to notify users that something has happened. Toasts +can be of four types: + +- info +- success +- warning +- error + +Examples toasts might include: + +- “Payment accepted” +- “Data submitted” +- “Request approved” + +Toasts require the use of the `setup_toasts()` function plus every view +needs these two features: + +- The session argument +- Must return FT components + +``` python +setup_toasts(app) + +@rt('/toasting') +def get(session): + # Normally one toast is enough, this allows us to see + # different toast types in action. + add_toast(session, f"Toast is being cooked", "info") + add_toast(session, f"Toast is ready", "success") + add_toast(session, f"Toast is getting a bit crispy", "warning") + add_toast(session, f"Toast is burning!", "error") + return Titled("I like toast") +``` + +Line 1 +`setup_toasts` is a helper function that adds toast dependencies. +Usually this would be declared right after `fast_app()` + +Line 4 +Toasts require sessions + +Line 11 +Views with Toasts must return FT or FtResponse components. + +💡 `setup_toasts` takes a `duration` input that allows you to specify +how long a toast will be visible before disappearing. For example +`setup_toasts(duration=5)` sets the toasts duration to 5 seconds. By +default toasts disappear after 10 seconds. + +⚠️ Toasts don’t work with SPA like navigation that replaces the entire +body such as this navigation trigger +`A('About', hx_get="/about", hx_swap="outerHTML", hx_push_url="true", hx_target="body")`. +As an alternative, wrap the content of your route in an element +containing an id and set this id as the target for your navigation +trigger (i.e. `hx_target='#container_id'`). + +## Authentication and authorization + +In FastHTML the tasks of authentication and authorization are handled +with Beforeware. Beforeware are functions that run before the route +handler is called. They are useful for global tasks like ensuring users +are authenticated or have permissions to access a view. + +First, we write a function that accepts a request and session arguments: + +``` python +# Status code 303 is a redirect that can change POST to GET, +# so it's appropriate for a login page. +login_redir = RedirectResponse('/login', status_code=303) + +def user_auth_before(req, sess): + # The `auth` key in the request scope is automatically provided + # to any handler which requests it, and can not be injected + # by the user using query params, cookies, etc, so it should + # be secure to use. + auth = req.scope['auth'] = sess.get('auth', None) + # If the session key is not there, it redirects to the login page. + if not auth: return login_redir +``` + +Now we pass our `user_auth_before` function as the first argument into a +[`Beforeware`](https://www.fastht.ml/docs/api/core.html#beforeware) +class. We also pass a list of regular expressions to the `skip` +argument, designed to allow users to still get to the home and login +pages. + +``` python +beforeware = Beforeware( + user_auth_before, + skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/'] +) + +app, rt = fast_app(before=beforeware) +``` + +## Server-sent events (SSE) + +With [server-sent +events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), +it’s possible for a server to send new data to a web page at any time, +by pushing messages to the web page. Unlike WebSockets, SSE can only go +in one direction: server to client. SSE is also part of the HTTP +specification unlike WebSockets which uses its own specification. + +FastHTML introduces several tools for working with SSE which are covered +in the example below. While concise, there’s a lot going on in this +function so we’ve annotated it quite a bit. + +``` python +import random +from asyncio import sleep +from fasthtml.common import * + +hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"),) +app,rt = fast_app(hdrs=hdrs) + +@rt +def index(): + return Titled("SSE Random Number Generator", + P("Generate pairs of random numbers, as the list grows scroll downwards."), + Div(hx_ext="sse", + sse_connect="/number-stream", + hx_swap="beforeend show:bottom", + sse_swap="message")) + +shutdown_event = signal_shutdown() + +async def number_generator(): + while not shutdown_event.is_set(): + data = Article(random.randint(1, 100)) + yield sse_message(data) + await sleep(1) + +@rt("/number-stream") +async def get(): return EventStream(number_generator()) +``` + +Line 5 +Import the HTMX SSE extension + +Line 12 +Tell HTMX to load the SSE extension + +Line 13 +Look at the `/number-stream` endpoint for SSE content + +Line 14 +When new items come in from the SSE endpoint, add them at the end of the +current content within the div. If they go beyond the screen, scroll +downwards + +Line 15 +Specify the name of the event. FastHTML’s default event name is +“message”. Only change if you have more than one call to SSE endpoints +within a view + +Line 17 +Set up the asyncio event loop + +Line 19 +Don’t forget to make this an `async` function! + +Line 20 +Iterate through the asyncio event loop + +Line 22 +We yield the data. Data ideally should be comprised of FT components as +that plugs nicely into HTMX in the browser + +Line 26 +The endpoint view needs to be an async function that returns a +[`EventStream`](https://www.fastht.ml/docs/api/core.html#eventstream) + +## Websockets + +With websockets we can have bi-directional communications between a +browser and client. Websockets are useful for things like chat and +certain types of games. While websockets can be used for single +direction messages from the server (i.e. telling users that a process is +finished), that task is arguably better suited for SSE. + +FastHTML provides useful tools for adding websockets to your pages. + +``` python +from fasthtml.common import * +from asyncio import sleep + +app, rt = fast_app(exts='ws') + +def mk_inp(): return Input(id='msg', autofocus=True) + +@rt('/') +async def get(request): + cts = Div( + Div(id='notifications'), + Form(mk_inp(), id='form', ws_send=True), + hx_ext='ws', ws_connect='/ws') + return Titled('Websocket Test', cts) + +async def on_connect(send): + print('Connected!') + await send(Div('Hello, you have connected', id="notifications")) + +async def on_disconnect(ws): + print('Disconnected!') + +@app.ws('/ws', conn=on_connect, disconn=on_disconnect) +async def ws(msg:str, send): + await send(Div('Hello ' + msg, id="notifications")) + await sleep(2) + return Div('Goodbye ' + msg, id="notifications"), mk_inp() +``` + +Line 4 +To use websockets in FastHTML, you must instantiate the app with `exts` +set to ‘ws’ + +Line 6 +As we want to use websockets to reset the form, we define the `mk_input` +function that can be called from multiple locations + +Line 12 +We create the form and mark it with the `ws_send` attribute, which is +documented here in the [HTMX websocket +specification](https://v1.htmx.org/extensions/web-sockets/). This tells +HTMX to send a message to the nearest websocket based on the trigger for +the form element, which for forms is pressing the `enter` key, an action +considered to be a form submission + +Line 13 +This is where the HTMX extension is loaded (`hx_ext='ws'`) and the +nearest websocket is defined (`ws_connect='/ws'`) + +Line 16 +When a websocket first connects we can optionally have it call a +function that accepts a `send` argument. The `send` argument will push a +message to the browser. + +Line 18 +Here we use the `send` function that was passed into the `on_connect` +function to send a `Div` with an `id` of `notifications` that HTMX +assigns to the element in the page that already has an `id` of +`notifications` + +Line 20 +When a websocket disconnects we can call a function which takes no +arguments. Typically the role of this function is to notify the server +to take an action. In this case, we print a simple message to the +console + +Line 23 +We use the `app.ws` decorator to mark that `/ws` is the route for our +websocket. We also pass in the two optional `conn` and `disconn` +parameters to this decorator. As a fun experiment, remove the `conn` and +`disconn` arguments and see what happens + +Line 24 +Define the `ws` function as async. This is necessary for ASGI to be able +to serve websockets. The function accepts two arguments, a `msg` that is +user input from the browser, and a `send` function for pushing data back +to the browser + +Line 25 +The `send` function is used here to send HTML back to the page. As the +HTML has an `id` of `notifications`, HTMX will overwrite what is already +on the page with the same ID + +Line 27 +The websocket function can also be used to return a value. In this case, +it is a tuple of two HTML elements. HTMX will take the elements and +replace them where appropriate. As both have `id` specified +(`notifications` and `msg` respectively), they will replace their +predecessor on the page. + +## File Uploads + +A common task in web development is uploading files. The examples below +are for uploading files to the hosting server, with information about +the uploaded file presented to the user. + +
    + +> **File uploads in production can be dangerous** +> +> File uploads can be the target of abuse, accidental or intentional. +> That means users may attempt to upload files that are too large or +> present a security risk. This is especially of concern for public +> facing apps. File upload security is outside the scope of this +> tutorial, for now we suggest reading the [OWASP File Upload Cheat +> Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html). + +
    + +### Single File Uploads + +``` python +from fasthtml.common import * +from pathlib import Path + +app, rt = fast_app() + +upload_dir = Path("filez") +upload_dir.mkdir(exist_ok=True) + +@rt('/') +def get(): + return Titled("File Upload Demo", + Article( + Form(hx_post=upload, hx_target="#result-one")( + Input(type="file", name="file"), + Button("Upload", type="submit", cls='secondary'), + ), + Div(id="result-one") + ) + ) + +def FileMetaDataCard(file): + return Article( + Header(H3(file.filename)), + Ul( + Li('Size: ', file.size), + Li('Content Type: ', file.content_type), + Li('Headers: ', file.headers), + ) + ) + +@rt +async def upload(file: UploadFile): + card = FileMetaDataCard(file) + filebuffer = await file.read() + (upload_dir / file.filename).write_bytes(filebuffer) + return card + +serve() +``` + +Line 13 +Every form rendered with the +[`Form`](https://www.fastht.ml/docs/api/xtend.html#form) FT component +defaults to `enctype="multipart/form-data"` + +Line 14 +Don’t forget to set the `Input` FT Component’s type to `file` + +Line 32 +The upload view should receive a [Starlette +UploadFile](https://www.starlette.io/requests/#request-files) type. You +can add other form variables + +Line 33 +We can access the metadata of the card (filename, size, content_type, +headers), a quick and safe process. We set that to the card variable + +Line 34 +In order to access the contents contained within a file we use the +`await` method to read() it. As files may be quite large or contain bad +data, this is a seperate step from accessing metadata + +Line 35 +This step shows how to use Python’s built-in +[`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) +library to write the file to disk. + +### Multiple File Uploads + +``` python +from fasthtml.common import * +from pathlib import Path + +app, rt = fast_app() + +upload_dir = Path("filez") +upload_dir.mkdir(exist_ok=True) + +@rt('/') +def get(): + return Titled("Multiple File Upload Demo", + Article( + Form(hx_post=upload_many, hx_target="#result-many")( + Input(type="file", name="files", multiple=True), + Button("Upload", type="submit", cls='secondary'), + ), + Div(id="result-many") + ) + ) + +def FileMetaDataCard(file): + return Article( + Header(H3(file.filename)), + Ul( + Li('Size: ', file.size), + Li('Content Type: ', file.content_type), + Li('Headers: ', file.headers), + ) + ) + +@rt +async def upload_many(files: list[UploadFile]): + cards = [] + for file in files: + cards.append(FileMetaDataCard(file)) + filebuffer = await file.read() + (upload_dir / file.filename).write_bytes(filebuffer) + return cards + +serve() +``` + +Line 13 +Every form rendered with the +[`Form`](https://www.fastht.ml/docs/api/xtend.html#form) FT component +defaults to `enctype="multipart/form-data"` + +Line 14 +Don’t forget to set the `Input` FT Component’s type to `file` and assign +the multiple attribute to `True` + +Line 32 +The upload view should receive a `list` containing the [Starlette +UploadFile](https://www.starlette.io/requests/#request-files) type. You +can add other form variables + +Line 34 +Iterate through the files + +Line 35 +We can access the metadata of the card (filename, size, content_type, +headers), a quick and safe process. We add that to the cards variable + +Line 36 +In order to access the contents contained within a file we use the +`await` method to read() it. As files may be quite large or contain bad +data, this is a seperate step from accessing metadata + +Line 37 +This step shows how to use Python’s built-in +[`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) +library to write the file to disk. diff --git a/docs/unpublished/tutorial_for_web_devs.html b/docs/unpublished/tutorial_for_web_devs.html new file mode 100644 index 0000000000000000000000000000000000000000..894d7980d7623c99e46c83a06556871ea2ffe575 --- /dev/null +++ b/docs/unpublished/tutorial_for_web_devs.html @@ -0,0 +1,1114 @@ + + + + + + + + + + +BYO Blog – fasthtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    + +
    + + +
    + + + +
    + +
    +
    +

    BYO Blog

    +
    + +
    +
    + Learn the foundations of FastHTML by creating your own blogging system from scratch. +
    +
    + + +
    + + + + +
    + + + +
    + + + +
    +
    +
    + +
    +
    +Caution +
    +
    +
    +

    This document is a work in progress.

    +
    +
    +

    In this tutorial we’re going to write a blog by example. Blogs are a good way to learn a web framework as they start simple yet can get surprisingly sophistated. The wikipedia definition of a blog is “an informational website consisting of discrete, often informal diary-style text entries (posts) informal diary-style text entries (posts)”, which means we need to provide these basic features:

    +
      +
    • A list of articles
    • +
    • A means to create/edit/delete the articles
    • +
    • An attractive but accessible layout
    • +
    +

    We’ll also add in these features, so the blog can become a working site:

    +
      +
    • RSS feed
    • +
    • Pages independent of the list of articles (about and contact come to mind)
    • +
    • Import and Export of articles
    • +
    • Tagging and categorization of data
    • +
    • Deployment
    • +
    • Ability to scale for large volumes of readers
    • +
    +
    +

    How to best use this tutorial

    +

    We could copy/paste every code example in sequence and have a finished blog at the end. However, it’s debatable how much we will learn through the copy/paste method. We’re not saying its impossible to learn through copy/paste, we’re just saying it’s not that of an efficient way to learn. It’s analogous to learning how to play a musical instrument or sport or video game by watching other people do it - you can learn some but its not the same as doing.

    +

    A better approach is to type out every line of code in this tutorial. This forces us to run the code through our brains, giving us actual practice in how to write FastHTML and Pythoncode and forcing us to debug our own mistakes. In some cases we’ll repeat similar tasks - a key component in achieving mastery in anything. Coming back to the instrument/sport/video game analogy, it’s exactly like actually practicing an instrument, sport, or video game. Through practice and repetition we eventually achieve mastery.

    +
    +
    +

    Installing FastHTML

    +

    FastHTML is just Python. Installation is often done with pip:

    +
    pip install python-fasthtml
    +
    +
    +

    A minimal FastHTML app

    +

    First, create the directory for our project using Python’s pathlib module:

    +
    import pathlib
    +pathlib.Path('blog-system').mkdir()
    +

    Now that we have our directory, let’s create a minimal FastHTML site in it.

    +
    +
    +
    blog-system/minimal.py
    +
    +
    from fasthtml.common import * 
    +
    +app, rt = fast_app()  
    +
    +@rt("/") 
    +def get():
    +    return Titled("FastHTML", P("Let's do this!")) 
    +
    +serve()
    +
    +

    Run that with python minimal.py and you should get something like this:

    +
    python minimal.py 
    +Link: http://localhost:5001
    +INFO:     Will watch for changes in these directories: ['/Users/pydanny/projects/blog-system']
    +INFO:     Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
    +INFO:     Started reloader process [46572] using WatchFiles
    +INFO:     Started server process [46576]
    +INFO:     Waiting for application startup.
    +INFO:     Application startup complete.
    +

    Confirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:

    +

    +
    +
    +
    + +
    +
    +What about the import *? +
    +
    +
    +

    For those worried about the use of import * rather than a PEP8-style declared namespace, understand that __all__ is defined in FastHTML’s common module. That means that only the symbols (functions, classes, and other things) the framework wants us to have will be brought into our own code via import *. Read importing from a package) for more information.

    +

    Nevertheless, if we want to use a defined namespace we can do so. Here’s an example:

    +
    from fasthtml import common as fh
    +
    +
    +app, rt = fh.fast_app()  
    +
    +@rt("/") 
    +def get():
    +    return fh.Titled("FastHTML", fh.P("Let's do this!")) 
    +
    +fh.serve()
    +
    +
    +
    +
    +

    Looking more closely at our app

    +

    Let’s look more closely at our application. Every line is packed with powerful features of FastHTML:

    +
    +
    +
    blog-system/minimal.py
    +
    +
    1from fasthtml.common import *
    +
    +2app, rt = fast_app()
    +
    +3@rt("/")
    +4def get():
    +5    return Titled("FastHTML", P("Let's do this!"))
    +
    +6serve()
    +
    +
    +
    1
    +
    +The top level namespace of Fast HTML (fasthtml.common) contains everything we need from FastHTML to build applications. A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience. +
    +
    2
    +
    +We instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll modify or take advantage of later in the tutorial. +
    +
    3
    +
    +We use the rt() decorator to tell FastHTML what to return when a user visits / in their browser. +
    +
    4
    +
    +We connect this route to HTTP GET requests by defining a view function called get(). +
    +
    5
    +
    +A tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach. +
    +
    6
    +
    +The serve() utility configures and runs FastHTML using a library called uvicorn. Any changes to this module will be reloaded into the browser. +
    +
    +
    +
    +

    Adding dynamic content to our minimal app

    +

    Our page is great, but we’ll make it better. Let’s add a randomized list of letters to the page. Every time the page reloads, a new list of varying length will be generated.

    +
    +
    +
    blog-system/random_letters.py
    +
    +
    from fasthtml.common import *
    +1import string, random
    +
    +app, rt = fast_app()
    +
    +@rt("/")
    +def get():
    +2    letters = random.choices(string.ascii_uppercase, k=random.randint(5, 20))
    +3    items = [Li(c) for c in letters]
    +    return Titled("Random lists of letters",
    +4        Ul(*items)
    +    ) 
    +
    +serve()
    +
    +
    +
    1
    +
    +The string and random libraries are part of Python’s standard library +
    +
    2
    +
    +We use these libraries to generate a random length list of random letters called letters +
    +
    3
    +
    +Using letters as the base we use list comprehension to generate a list of Li ft display components, each with their own letter and save that to the variable items +
    +
    4
    +
    +Inside a call to the Ul() ft component we use Python’s *args special syntax on the items variable. Therefore *list is treated not as one argument but rather a set of them. +
    +
    +

    When this is run, it will generate something like this with a different random list of letters for each page load:

    +

    +
    +
    +

    Storing the articles

    +

    The most basic component of a blog is a series of articles sorted by date authored. Rather than a database we’re going to use our computer’s harddrive to store a set of markdown files in a directory within our blog called posts. First, let’s create the directory and some test files we can use to search for:

    +
    +
    from fastcore.utils import *
    +
    +
    +
    # Create some dummy posts
    +posts = Path("posts")
    +posts.mkdir(exist_ok=True)
    +for i in range(10): (posts/f"article_{i}.md").write_text(f"This is article {i}")
    +
    +

    Searching for these files can be done with pathlib.

    +
    +
    import pathlib
    +posts.ls()
    +
    +
    (#10) [Path('posts/article_5.md'),Path('posts/article_1.md'),Path('posts/article_0.md'),Path('posts/article_4.md'),Path('posts/article_3.md'),Path('posts/article_7.md'),Path('posts/article_6.md'),Path('posts/article_2.md'),Path('posts/article_9.md'),Path('posts/article_8.md')]
    +
    +
    +
    +
    +
    + +
    +
    +Tip +
    +
    +
    +

    Python’s pathlib library is quite useful and makes file search and manipulation much easier. There’s many uses for it and is compatible across operating systems.

    +
    +
    +
    +
    +

    Creating the blog home page

    +

    We now have enough tools that we can create the home page. Let’s create a new Python file and write out our simple view to list the articles in our blog.

    +
    +
    +
    blog-system/main.py
    +
    +
    from fasthtml.common import *
    +import pathlib
    +
    +app, rt = fast_app()
    +
    +@rt("/")
    +def get():
    +    fnames = pathlib.Path("posts").rglob("*.md")
    +    items = [Li(A(fname, href=fname)) for fname in fnames]    
    +    return Titled("My Blog",
    +        Ul(*items)
    +    ) 
    +
    +serve()
    +
    +
    +
    for p in posts.ls(): p.unlink()
    +
    + + +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/docs/unpublished/tutorial_for_web_devs.html.md b/docs/unpublished/tutorial_for_web_devs.html.md new file mode 100644 index 0000000000000000000000000000000000000000..6d1791163712d4b21515511a85bf06c4e19c959b --- /dev/null +++ b/docs/unpublished/tutorial_for_web_devs.html.md @@ -0,0 +1,318 @@ +# BYO Blog + + + + +
    + +> **Caution** +> +> This document is a work in progress. + +
    + +In this tutorial we’re going to write a blog by example. Blogs are a +good way to learn a web framework as they start simple yet can get +surprisingly sophistated. The [wikipedia definition of a +blog](https://en.wikipedia.org/wiki/Blog) is “an informational website +consisting of discrete, often informal diary-style text entries (posts) +informal diary-style text entries (posts)”, which means we need to +provide these basic features: + +- A list of articles +- A means to create/edit/delete the articles +- An attractive but accessible layout + +We’ll also add in these features, so the blog can become a working site: + +- RSS feed +- Pages independent of the list of articles (about and contact come to + mind) +- Import and Export of articles +- Tagging and categorization of data +- Deployment +- Ability to scale for large volumes of readers + +## How to best use this tutorial + +We could copy/paste every code example in sequence and have a finished +blog at the end. However, it’s debatable how much we will learn through +the copy/paste method. We’re not saying its impossible to learn through +copy/paste, we’re just saying it’s not that of an efficient way to +learn. It’s analogous to learning how to play a musical instrument or +sport or video game by watching other people do it - you can learn some +but its not the same as doing. + +A better approach is to type out every line of code in this tutorial. +This forces us to run the code through our brains, giving us actual +practice in how to write FastHTML and Pythoncode and forcing us to debug +our own mistakes. In some cases we’ll repeat similar tasks - a key +component in achieving mastery in anything. Coming back to the +instrument/sport/video game analogy, it’s exactly like actually +practicing an instrument, sport, or video game. Through practice and +repetition we eventually achieve mastery. + +## Installing FastHTML + +FastHTML is *just Python*. Installation is often done with pip: + +``` shellscript +pip install python-fasthtml +``` + +## A minimal FastHTML app + +First, create the directory for our project using Python’s +[pathlib](https://docs.python.org/3/library/pathlib.html) module: + +``` python +import pathlib +pathlib.Path('blog-system').mkdir() +``` + +Now that we have our directory, let’s create a minimal FastHTML site in +it. + +
    + +**blog-system/minimal.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/") +def get(): + return Titled("FastHTML", P("Let's do this!")) + +serve() +``` + +
    + +Run that with `python minimal.py` and you should get something like +this: + +``` shellscript +python minimal.py +Link: http://localhost:5001 +INFO: Will watch for changes in these directories: ['/Users/pydanny/projects/blog-system'] +INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit) +INFO: Started reloader process [46572] using WatchFiles +INFO: Started server process [46576] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +Confirm FastHTML is running by opening your web browser to +[127.0.0.1:5001](http://127.0.0.1:5001). You should see something like +the image below: + +![](quickstart-web-dev/quickstart-fasthtml.png) + +
    + +> **What about the `import *`?** +> +> For those worried about the use of `import *` rather than a PEP8-style +> declared namespace, understand that `__all__` is defined in FastHTML’s +> common module. That means that only the symbols (functions, classes, +> and other things) the framework wants us to have will be brought into +> our own code via `import *`. Read [importing from a +> package](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package)) +> for more information. +> +> Nevertheless, if we want to use a defined namespace we can do so. +> Here’s an example: +> +> ``` python +> from fasthtml import common as fh +> +> +> app, rt = fh.fast_app() +> +> @rt("/") +> def get(): +> return fh.Titled("FastHTML", fh.P("Let's do this!")) +> +> fh.serve() +> ``` + +
    + +## Looking more closely at our app + +Let’s look more closely at our application. Every line is packed with +powerful features of FastHTML: + +
    + +**blog-system/minimal.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/") +def get(): + return Titled("FastHTML", P("Let's do this!")) + +serve() +``` + +
    + +Line 1 +The top level namespace of Fast HTML (fasthtml.common) contains +everything we need from FastHTML to build applications. A +carefully-curated set of FastHTML functions and other Python objects is +brought into our global namespace for convenience. + +Line 3 +We instantiate a FastHTML app with the `fast_app()` utility function. +This provides a number of really useful defaults that we’ll modify or +take advantage of later in the tutorial. + +Line 5 +We use the `rt()` decorator to tell FastHTML what to return when a user +visits `/` in their browser. + +Line 6 +We connect this route to HTTP GET requests by defining a view function +called `get()`. + +Line 7 +A tree of Python function calls that return all the HTML required to +write a properly formed web page. You’ll soon see the power of this +approach. + +Line 9 +The [`serve()`](https://www.fastht.ml/docs/api/core.html#serve) utility +configures and runs FastHTML using a library called `uvicorn`. Any +changes to this module will be reloaded into the browser. + +## Adding dynamic content to our minimal app + +Our page is great, but we’ll make it better. Let’s add a randomized list +of letters to the page. Every time the page reloads, a new list of +varying length will be generated. + +
    + +**blog-system/random_letters.py** + +``` python +from fasthtml.common import * +import string, random + +app, rt = fast_app() + +@rt("/") +def get(): + letters = random.choices(string.ascii_uppercase, k=random.randint(5, 20)) + items = [Li(c) for c in letters] + return Titled("Random lists of letters", + Ul(*items) + ) + +serve() +``` + +
    + +Line 2 +The `string` and `random` libraries are part of Python’s standard +library + +Line 8 +We use these libraries to generate a random length list of random +letters called `letters` + +Line 9 +Using `letters` as the base we use list comprehension to generate a list +of `Li` ft display components, each with their own letter and save that +to the variable `items` + +Line 11 +Inside a call to the `Ul()` ft component we use Python’s `*args` special +syntax on the `items` variable. Therefore `*list` is treated not as one +argument but rather a set of them. + +When this is run, it will generate something like this with a different +random list of letters for each page load: + +![](web-dev-tut/random-list-letters.png) + +## Storing the articles + +The most basic component of a blog is a series of articles sorted by +date authored. Rather than a database we’re going to use our computer’s +harddrive to store a set of markdown files in a directory within our +blog called `posts`. First, let’s create the directory and some test +files we can use to search for: + +``` python +from fastcore.utils import * +``` + +``` python +# Create some dummy posts +posts = Path("posts") +posts.mkdir(exist_ok=True) +for i in range(10): (posts/f"article_{i}.md").write_text(f"This is article {i}") +``` + +Searching for these files can be done with pathlib. + +``` python +import pathlib +posts.ls() +``` + + (#10) [Path('posts/article_5.md'),Path('posts/article_1.md'),Path('posts/article_0.md'),Path('posts/article_4.md'),Path('posts/article_3.md'),Path('posts/article_7.md'),Path('posts/article_6.md'),Path('posts/article_2.md'),Path('posts/article_9.md'),Path('posts/article_8.md')] + +
    + +> **Tip** +> +> Python’s [pathlib](https://docs.python.org/3/library/pathlib.html) +> library is quite useful and makes file search and manipulation much +> easier. There’s many uses for it and is compatible across operating +> systems. + +
    + +## Creating the blog home page + +We now have enough tools that we can create the home page. Let’s create +a new Python file and write out our simple view to list the articles in +our blog. + +
    + +**blog-system/main.py** + +``` python +from fasthtml.common import * +import pathlib + +app, rt = fast_app() + +@rt("/") +def get(): + fnames = pathlib.Path("posts").rglob("*.md") + items = [Li(A(fname, href=fname)) for fname in fnames] + return Titled("My Blog", + Ul(*items) + ) + +serve() +``` + +
    + +``` python +for p in posts.ls(): p.unlink() +``` diff --git a/docs/unpublished/web-dev-tut/random-list-letters.png b/docs/unpublished/web-dev-tut/random-list-letters.png new file mode 100644 index 0000000000000000000000000000000000000000..a63a7d59ae40916799bd92af434086fa4237c27f Binary files /dev/null and b/docs/unpublished/web-dev-tut/random-list-letters.png differ diff --git a/favicon-dark.ico b/favicon-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..24643a34df3ce986570c249f66eb4af03727d461 Binary files /dev/null and b/favicon-dark.ico differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..82ef3bab5996bc30a77e3fe0901081eed5eb8c61 Binary files /dev/null and b/favicon.ico differ diff --git a/home_components.py b/home_components.py new file mode 100644 index 0000000000000000000000000000000000000000..539361bfb66609ee641ed35d6c97cbdc1009d972 --- /dev/null +++ b/home_components.py @@ -0,0 +1,146 @@ +from fasthtml.common import * +from itertools import starmap + +icons = 'assets/icons' +col = "flex flex-col" +center = "flex items-center" +between = "flex justify-between" +gap2 = "flex gap-2" +# inset = "shadow-[0_3px_2px_rgba(255,255,255,0.9),0_5px_5px_rgba(0,0,0,0.3)]" +# bnset = "shadow-[0_3px_2px_rgba(255,255,255,0.1),0_4px_4px_rgba(0,0,0,0.7)]" +inset = "shadow-[0_2px_2px_rgba(255,255,255,0.5),0_3px_3px_rgba(0,0,0,0.2)]" +bnset = "shadow-[inset_0_2px_4px_rgba(255,255,255,0.1),0_4px_8px_rgba(0,0,0,0.5)]" + +section_base1= "pt-8 px-4 pb-24 gap-8 lg:gap-16 lg:pt-16 lg:px-16" +section_base =f"{col} {section_base1}" + +def maxpx (px ): return f"w-full max-w-[{px}px]" +def maxrem(rem): return f"w-full max-w-[{rem}rem]" + +def section_wrapper(content, bg_color, xtra="", flex=True): + return Section(content, cls=f"bg-{bg_color} {section_base1} {col if flex else ''} -mt-8 lg:-mt-16 items-center rounded-t-3xl lg:rounded-t-[2.5rem] relative {xtra}") + +def section_header(mono_text, heading, subheading, max_width=32, center=True): + pos = 'items-center text-center' if center else 'items-start text-start' + return Div( + P(mono_text, cls="mono-body text-opacity-60"), + H2(heading, cls=f"text-black heading-2 {maxrem(max_width)}"), + P(subheading, cls=f"l-body {maxrem(max_width)}"), + cls=f"{maxrem(50)} mx-auto {col} {pos} gap-6") + +def arrow(d): + return Button(Img(src=f"assets/icons/arrow-{d}.svg", alt="Arrow left"), + cls="disabled:opacity-40 transition-opacity", id=f"slide{d.capitalize()}", aria_label=f"Slide {d}") + +def carousel(items, id="carousel-container", extra_classes=""): + carousel_content = Div(*items, id=id, + cls=f"hide-scrollbar {col} lg:flex-row gap-4 lg:gap-6 rounded-l-3xl xl:rounded-3xl w-full lg:overflow-hidden xl:overflow-hidden whitespace-nowrap {extra_classes}") + + arrows = Div( + Div(arrow("left"), arrow("right"), + cls=f"w-[4.5rem] {between} ml-auto"), + cls=f"hidden lg:flex xl:flex justify-start {maxrem(41)} py-6 pl-6 pr-20") + return Div(carousel_content, arrows, cls=f"max-h-fit {col} items-start lg:-mr-16 {maxpx(1440)} overflow-hidden") + +def testimonial_card(idx, comment, name, role, company, image_src): + return Div( + P(comment, cls="m-body text-black"), + Div( + Div(Img(src=image_src, alt=f"Picture of {name}", width="112", height="112"), + cls="rounded-full w-11 h-11 lg:w-14 lg:h-14"), + Div( + P(name, cls=f"m-body text-black"), + Div( + P(role), + Img(src=f"{icons}/dot.svg", alt="Dot separator", width="4", height="4"), + P(company), + cls=f"{gap2} xs-mono-body w-full"), + cls="w-full"), + cls=f"{center} justify-start gap-2"), + id=f"testimonial-card-{idx+1}", + cls=f"testimonial-card {col} flex-none whitespace-normal flex justify-between h-96 rounded-3xl items-start bg-soft-pink p-4 lg:p-8 {maxrem(36)} lg:w-96") + +def stack_item(name, icon_src, href): + return A( + Img(src=f"./assets/icons/stack/{icon_src}", alt=name, width="24", height="24"), + P(name, cls="text-black/60"), + href=href, target="_blank", rel="noopener noreferrer", + cls=f"{gap2} items-center px-4 py-2 bg-white/60 rounded-full {inset}") + +def stacked_card(title, description, stacks, color): + return Div( + Div( + H3(title, cls="heading-3 mb-4"), + P(description, cls="mb-12"), + Div(*starmap(stack_item, stacks), + cls=f"{gap2} flex-wrap items-center"), + cls=f"rounded-3xl {color} lg:p-12 p-6 {col} m-body") + ) + +def accordion(id, question, answer, question_cls="", answer_cls="", container_cls=""): + return Div( + Input(id=f"collapsible-{id}", type="checkbox", cls=f"collapsible-checkbox peer/collapsible hidden"), + Label( + P(question, cls=f"flex-grow {question_cls}"), + Img(src=f"{icons}/plus-icon.svg", alt="Expand", cls=f"plus-icon w-6 h-6"), + Img(src=f"{icons}/minus-icon.svg", alt="Collapse", cls=f"minus-icon w-6 h-6"), + _for=f"collapsible-{id}", + cls="flex items-center cursor-pointer py-4 lg:py-6 pl-6 lg:pl-8 pr-4 lg:pr-6"), + P(answer, cls=f"overflow-hidden max-h-0 pl-6 lg:pl-8 pr-4 lg:pr-6 peer-checked/collapsible:max-h-[30rem] peer-checked/collapsible:pb-4 peer-checked/collapsible:lg:pb-6 transition-all duration-300 ease-in-out {answer_cls}"), + cls=container_cls) + +def video_player(txt): + return ( + # Video Popup container - TODO make pretty + Div( + Div( + # 'Pastel green top bar', + Div( + H3(txt, cls='text-green-800 font-semibold'), + # 'Close button', + Button('X', id='closePopup', cls='text-green-800 hover:text-green-950'), + cls='bg-green-200 p-2 flex justify-between items-center' + ), + # 'YouTube video iframe', + Div( + Iframe(id='youtubeVideo', width='560', height='315', src='', frameborder='0', allowfullscreen=''), + cls='p-4' + ), + cls='bg-white rounded-lg shadow-lg overflow-hidden' + ), + id='videoPopup', + cls='hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center' + ) + ) + +yt_frame = """""" +def video_button(txt, poster_src, video_duration, video_id, poster_alt="Video poster", youtube_icon_src="/assets/icons/youtube.svg", max_width="350px"): + return ( + A( + Img(src=poster_src, width='240', height='120', cls='rounded-full w-[7.5rem] h-auto', alt=poster_alt), + Span(txt, Span(video_duration, cls='s-body text-black/60'), cls=f'text-black {col}'), + P(Img(src=youtube_icon_src, width='41', height='30', alt='Youtube icon'), cls=f'flex-1 {center}'), + Script(f""" + me().on('click', (e) => {{ + e.preventDefault(); + me('#videoOverlay').classRemove('hidden').classAdd('flex'); + me('#youtube-player').setAttribute('src', 'https://www.youtube.com/embed/{video_id}'); + }});"""), + id="openVideo", href='#', + cls=f'{inset} p-2 rounded-full bg-white hover:bg-white/80 transition-colors duration-300 h-[76px] w-full max-w-[{max_width}] {center} gap-4' + ), + Div( + Iframe(id="youtube-player", src="", cls="w-full aspect-video", allowfullscreen=True, allow="autoplay; encrypted-media"), + Button("Close", + Script(""" + me().on('click', () => { + me('#videoOverlay').classRemove('flex').classAdd('hidden'); + me('#youtube-player').setAttribute('src', ''); + });"""), + id="closeVideo", cls="mt-4 bg-soft-pink text-black font-bold py-2 px-4 rounded" + ), + Script("document.addEventListener('keydown', (e) => { if (e.key === 'Escape') me('#closeVideo').send('click'); });"), + id="videoOverlay", cls="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50" + ) + ) + diff --git a/intermediate_todo.db b/intermediate_todo.db new file mode 100644 index 0000000000000000000000000000000000000000..1395515643fb4f209bfd23692593919f57bbf12a Binary files /dev/null and b/intermediate_todo.db differ diff --git a/js/carouselScroll.js b/js/carouselScroll.js new file mode 100644 index 0000000000000000000000000000000000000000..4b5cb38fb011731a9f2dbd2cb2e87dbbadcd7262 --- /dev/null +++ b/js/carouselScroll.js @@ -0,0 +1,86 @@ +const container = document.getElementById("carousel-container"); +const elements = container.querySelectorAll(".testimonial-card"); +const endicators = document.querySelectorAll(".endicator"); + +const options = { + root: container, + rootMargin: "0px", + threshold: 1, +}; + +// From specs, there's only ever gonna be one card fully visible at a time +let visibleCardNum = -1; +const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + visibleCardNum = Number(entry.target.id.split("-")[2]); + } + }); +}, options); + +const boundsObserver = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + const bound = entry.target.dataset.bound; + if (entry.isIntersecting) { + if (bound === "left") { + document.getElementById("slideLeft").disabled = true; + } + + if (bound === "right") { + document.getElementById("slideRight").disabled = true; + } + } else { + if (bound === "left") { + document.getElementById("slideLeft").disabled = false; + } + + if (bound === "right") { + document.getElementById("slideRight").disabled = false; + } + } + }); +}, options); + +endicators.forEach((element) => boundsObserver.observe(element)); + +elements.forEach((element) => { + observer.observe(element); +}); + +function updateButtonState() { + if (container.scrollLeft <= 0) { + document.getElementById("slideLeft").disabled = true; + } else { + document.getElementById("slideLeft").disabled = false; + } + + if (container.scrollLeft >= container.scrollWidth - container.clientWidth) { + document.getElementById("slideRight").disabled = true; + } else { + document.getElementById("slideRight").disabled = false; + } +} + +document.getElementById("slideRight").addEventListener("click", function () { + const scrollTo = document.getElementById( + `testimonial-card-${visibleCardNum + 1}` + ); + + if (scrollTo) { + const x = container.scrollLeft + scrollTo.offsetLeft - container.offsetLeft; + container.scrollBy({ left: x, behavior: "smooth" }); + // updateButtonState(); + } +}); + +document.getElementById("slideLeft").addEventListener("click", function () { + const scrollTo = document.getElementById( + `testimonial-card-${visibleCardNum - 1}` + ); + + if (scrollTo) { + const x = container.scrollLeft - scrollTo.offsetLeft + container.offsetLeft; + container.scrollBy({ left: -x, behavior: "smooth" }); + // updateButtonState(); + } +}); diff --git a/js/codeOverflow.js b/js/codeOverflow.js new file mode 100644 index 0000000000000000000000000000000000000000..ff32113ca01e54fad9e72360f0403679b652acf1 --- /dev/null +++ b/js/codeOverflow.js @@ -0,0 +1,20 @@ +function checkOverflow(codeContainer) { + const fadeOutElement = + codeContainer.parentElement.querySelector(".code-fade-out"); + let preElement = codeContainer.querySelector("code"); + if ( + Math.floor(codeContainer.getBoundingClientRect().bottom) < + Math.floor(preElement.getBoundingClientRect().bottom) + ) { + fadeOutElement.style.display = "block"; + } else { + fadeOutElement.style.display = "none"; + } +} + +checkOverflow(document.querySelector(".code-snippet")); + +const codeSnippets = document.querySelectorAll(".code-snippet"); +codeSnippets.forEach((container) => { + container.addEventListener("scroll", () => checkOverflow(container)); +}); diff --git a/js/copyCode.js b/js/copyCode.js new file mode 100644 index 0000000000000000000000000000000000000000..7c7dd7a68eaa9395ce25c7a59cb597b8594d7282 --- /dev/null +++ b/js/copyCode.js @@ -0,0 +1,36 @@ +document.querySelectorAll(".copy-button").forEach((button) => { + button.addEventListener("click", async () => { + const codeContainer = button.closest(".code-container"); + const codecontents = codeContainer.querySelector("code").innerText; + // const buttonContent = button.querySelector(".button-content"); + const copiedText = button.querySelector(".copied-text"); + + copiedText.parentElement.classList.add("rounded-r-[0.5rem]"); + copiedText.parentElement.classList.remove("rounded-[0.5rem]"); + copiedText.classList.add("w-fit"); + copiedText.classList.add("px-1"); + copiedText.classList.remove("w-0"); + + copiedText.style.transform = "translateX(-2rem)"; + + try { + button.disabled = true; + await navigator.clipboard.writeText(codecontents); + + setTimeout(() => { + copiedText.style.transform = "translateX(0rem)"; + + copiedText.addEventListener("transitionend", () => { + copiedText.classList.add("w-0"); + copiedText.classList.remove("w-fit"); + copiedText.classList.remove("px-1"); + copiedText.parentElement.classList.add("rounded-[0.5rem]"); + copiedText.parentElement.classList.remove("rounded-r-[0.5rem]"); + button.disabled = false; + }); + }, 2000); + } catch { + console.error("Failed to copy the code to the clipboard"); + } + }); +}); diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ac3e8a787dd6ff9806b5a544fda2065654c221a2 --- /dev/null +++ b/js/index.js @@ -0,0 +1,347 @@ +// Bundled JavaScript, generated via `npx vite build`, do not edit manually + +(function () { + const o = document.createElement("link").relList; + if (o && o.supports && o.supports("modulepreload")) return; + for (const n of document.querySelectorAll('link[rel="modulepreload"]')) e(n); + new MutationObserver((n) => { + for (const i of n) + if (i.type === "childList") + for (const c of i.addedNodes) + c.tagName === "LINK" && c.rel === "modulepreload" && e(c); + }).observe(document, { childList: !0, subtree: !0 }); + function s(n) { + const i = {}; + return ( + n.integrity && (i.integrity = n.integrity), + n.referrerPolicy && (i.referrerPolicy = n.referrerPolicy), + n.crossOrigin === "use-credentials" + ? (i.credentials = "include") + : n.crossOrigin === "anonymous" + ? (i.credentials = "omit") + : (i.credentials = "same-origin"), + i + ); + } + function e(n) { + if (n.ep) return; + n.ep = !0; + const i = s(n); + fetch(n.href, i); + } +})(); +(function () { + const t = document.getElementById("stacked-cards-section"), + o = t.children[0], + s = document.getElementById("stacked-cards"), + e = Array.from(s.children), + n = e.map((r) => r.children.item(0).getBoundingClientRect().height); + if ( + (function () { + let r = !1; + return ( + (function (a) { + (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( + a + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4) + )) && + (r = !0); + })(navigator.userAgent || navigator.vendor || window.opera), + r + ); + })() || + window.innerWidth < 1024 + ) { + (s.style.gap = "var(--cardMargin)"), + e.forEach((r) => (r.style.position = "relative")), + (o.style.position = "relative"); + return; + } + const c = (r, a) => { + const p = window.innerHeight, + u = window.scrollY, + f = r.offsetTop, + g = r.offsetHeight, + d = u + p - f - a, + v = Math.round(d / ((p + g) / 100)); + return Math.min(100, Math.max(0, v)); + }; + e.forEach((r) => { + r.style.top = `calc(${ + o.getBoundingClientRect().height + "px" + } + var(--cardsSectionGap) + calc(var(--index) * var(--cardTopPadding)))`; + }); + const l = +getComputedStyle(t) + .getPropertyValue("--cardsHeaderBottomStop") + .slice(0, -2); + document.addEventListener("scroll", function () { + const r = c(e[1], 64 + t.offsetTop), + a = c(e[2], 128 + t.offsetTop); + Math.abs( + o.getBoundingClientRect().bottom - t.getBoundingClientRect().bottom + ) < + l + n[2] + ? (o.children.item( + 0 + ).style.paddingBottom = `calc(var(--cardsHeaderBottomStop) + ${ + n[2] + "px" + })`) + : (o.children.item(0).style.paddingBottom = ""), + (e[0].children.item(0).style.scale = + 1 - (0.1 * r) / 100 - (0.1 * a) / 100), + (e[0].children.item(0).style.opacity = + 1 - (0.1 * r) / 100 - (0.1 * a) / 100), + (e[0].children.item(0).style.filter = `blur(${ + (10 * r) / 100 + (10 * a) / 100 + }px)`), + (e[0].style.paddingBottom = `calc( + var(--cardTopPadding) * 2 - ${n[0] - n[2]}px + )`), + (e[1].children.item(0).style.scale = 1 - (0.1 * a) / 100), + (e[1].children.item(0).style.opacity = 1 - (0.1 * a) / 100), + (e[1].children.item(0).style.filter = `blur(${(10 * a) / 100}px)`), + (e[1].style.paddingBottom = `calc( + var(--cardTopPadding) - ${n[1] - n[2]}px + )`); + }); +})(); +(function () { + const t = (n) => { + const i = document.getElementById("stacked-cards-component-demo"), + c = n.children.item(0), + l = i.getBoundingClientRect(), + r = c.getBoundingClientRect(); + return r.bottom < l.bottom + ? 100 + : r.top > l.bottom + ? 0 + : Math.round((Math.abs(l.bottom - r.top) / r.height) * 100); + }, + o = document.getElementById("stacked-cards-component-demo"), + s = document.getElementById("preview-stacked-cards"), + e = Array.from(s.children); + o.addEventListener("scroll", function () { + const n = t(e[1]), + i = t(e[2]); + (e[0].children.item(0).style.scale = 1 - (0.1 * n) / 100 - (0.1 * i) / 100), + (e[0].children.item(0).style.opacity = + 1 - (0.1 * n) / 100 - (0.1 * i) / 100), + (e[0].children.item(0).style.filter = `blur(${ + (10 * n) / 100 + (10 * i) / 100 + }px)`), + (e[1].children.item(0).style.scale = 1 - (0.1 * i) / 100), + (e[1].children.item(0).style.opacity = 1 - (0.1 * i) / 100), + (e[1].children.item(0).style.filter = `blur(${(10 * i) / 100}px)`); + }); +})(); +hljs.registerLanguage("python-custom", (t) => { + var o = t.getLanguage("python"); + (o.name = "Python Custom"), + o.contains.push({ + className: "decorator", + begin: "/@/", + end: "/$/", + contains: [ + { className: "symbol", match: "@" }, + { className: "name", begin: /[\w\.]+/ }, + { className: "params", begin: /\(/, end: /\)/ }, + ], + }); + var s = { className: "yellow-char", begin: /\*/ }, + e = { className: "white-char", begin: /[,.:()@]/ }, + n = { + className: "attribute", + begin: /\./, + end: /[\w]+/, + contains: [{ begin: /\w+/, className: "attr-name" }], + }; + return ( + (o.keywords.built_in = o.keywords.built_in.filter((i) => i !== "id")), + o.contains.push(e), + o.contains.push(n), + o.contains.push(s), + o + ); +}); +hljs.highlightAll(); +hljs.initLineNumbersOnLoad(); +document.addEventListener("DOMContentLoaded", function () { + const t = document.querySelectorAll(".toggle-button"), + o = document.querySelectorAll(".code-container"), + s = document.getElementById("tab-list"), + e = s.getBoundingClientRect(), + n = document.querySelector(".button-container"); + t.forEach((i) => { + i.addEventListener("click", () => { + const c = i.parentElement, + l = c.getBoundingClientRect(), + r = l.left - e.left + l.width / 2 - e.width / 2; + s.scrollBy({ left: r, behavior: "smooth" }); + const a = document.getElementById("highlighter"); + o.forEach((d) => { + d.style.display = "none"; + }), + t.forEach((d) => { + (d.parentElement.ariaSelected = !1), + d.parentElement.classList.remove("active"), + d.parentElement.classList.add("text-white/80"), + d.parentElement.classList.add("hover:text-white"), + d.parentElement.classList.remove("text-white"); + }); + const p = document.getElementById(i.dataset.target); + (p.style.display = "flex"), + (c.ariaSelected = !0), + c.classList.add("active"), + c.classList.add("text-white"), + c.classList.remove("text-white/80"); + const u = c.getBoundingClientRect(), + f = n.getBoundingClientRect(), + g = u.x - f.x; + a.style.transform = `translateX(${g}px)`; + }); + }), + t.forEach((i) => { + i.addEventListener("click", () => {}); + }); +}); +function y(t) { + const o = t.parentElement.querySelector(".code-fade-out"); + let s = t.querySelector("code"); + Math.floor(t.getBoundingClientRect().bottom) < + Math.floor(s.getBoundingClientRect().bottom) + ? (o.style.display = "block") + : (o.style.display = "none"); +} +y(document.querySelector(".code-snippet")); +const w = document.querySelectorAll(".code-snippet"); +w.forEach((t) => { + t.addEventListener("scroll", () => y(t)); +}); +const k = { + "button-code-snippet": ` +@rt("/") +async def get(): + add = Form(Group(mk_input(), Button("Add")), hx_post="/", target_id='todo-list', hx_swap="beforeend") + card = Card(Ul(*todos(), id='todo-list', header=add, footer=Div(id=id_curr)), title = 'Todo list') + return Title(title), Main(H1(title), card, cls='container') +`, + "card-code-snippet": ` +@rt("/") +async def put(todo: Todo): + return todos.upsert(todo), clr_details() + +@rt("/todos/{id}") +async def get(id:int): + todo = todos.get(id) + btn = Button('delete', hx_delete=f'/todos/{todo.id}', target_id=tid(todo.id), hx_swap="outerHTML") + return Div(Div(todo.title), btn) + +if __name__ == '__main__': + uvicorn.run("main:app", host='0.0.0.0', port=int(os.getenv("PORT", default=5001)) +`, + "accordion-code-snippet": ` +@rt("/") +async def post(todo:Todo): + return todos.insert(todo), mk_input(hx_swap_oob='true') + +@rt("/edit/{id}") +async def get(id:int): + res = Form(Group(Input(id="title"), + Button("Save")), + Hidden(id="id"), + Checkbox(id="done", label='Done'), + hx_put="/", + target_id=tid(id), + id="edit") + return fill_form(res, todos.get(id)) +`, + "stacked-cards-code-snippet": ` +@rt("/{fname:path}.{ext:static}") +async def get(fname:str, ext:str): + return FileResponse(f'{fname}.{ext}') + +def mk_input(**kw): + return Input(id="new-title", name="title", placeholder="New Todo", **kw) + +def clr_details(): + return Div(hx_swap_oob='innerHTML', id=id_curr) + +@rt("/") +async def get(): + add = Form(Group(mk_input(), Button("Add")), hx_post="/", target_id='todo-list', hx_swap="beforeend") + card = Card(Ul(*todos(), id='todo-list'), header=add, footer=Div(id=id_curr)), + title = 'Todo list' + return Title(title), Main(H1(title), card, cls='container') +`, +}; +document.querySelectorAll(".copy-button").forEach((t) => { + t.addEventListener("click", async () => { + const s = t.closest(".code-container").querySelector("pre").id, + e = t.querySelector(".copied-text"); + e.parentElement.classList.add("rounded-r-[0.5rem]"), + e.parentElement.classList.remove("rounded-[0.5rem]"), + e.classList.add("w-fit"), + e.classList.add("px-1"), + e.classList.remove("w-0"), + (e.style.transform = "translateX(-2rem)"); + try { + (t.disabled = !0), + await navigator.clipboard.writeText(k[s]), + setTimeout(() => { + (e.style.transform = "translateX(0rem)"), + e.addEventListener("transitionend", () => { + e.classList.add("w-0"), + e.classList.remove("w-fit"), + e.classList.remove("px-1"), + e.parentElement.classList.add("rounded-[0.5rem]"), + e.parentElement.classList.remove("rounded-r-[0.5rem]"), + (t.disabled = !1); + }); + }, 2e3); + } catch { + console.error("Failed to copy the code to the clipboard"); + } + }); +}); +const m = document.getElementById("carousel-container"), + x = m.querySelectorAll(".testimonial-card"), + E = document.querySelectorAll(".endicator"), + b = { root: m, rootMargin: "0px", threshold: 1 }; +let h = -1; +const L = new IntersectionObserver((t) => { + t.forEach((o) => { + o.isIntersecting && (h = Number(o.target.id.split("-")[2])); + }); + }, b), + B = new IntersectionObserver((t) => { + t.forEach((o) => { + const s = o.target.dataset.bound; + o.isIntersecting + ? (s === "left" && (document.getElementById("slideLeft").disabled = !0), + s === "right" && + (document.getElementById("slideRight").disabled = !0)) + : (s === "left" && (document.getElementById("slideLeft").disabled = !1), + s === "right" && + (document.getElementById("slideRight").disabled = !1)); + }); + }, b); +E.forEach((t) => B.observe(t)); +x.forEach((t) => { + L.observe(t); +}); +document.getElementById("slideRight").addEventListener("click", function () { + const t = document.getElementById(`testimonial-card-${h + 1}`); + if (t) { + const o = m.scrollLeft + t.offsetLeft - m.offsetLeft; + m.scrollBy({ left: o, behavior: "smooth" }); + } +}); +document.getElementById("slideLeft").addEventListener("click", function () { + const t = document.getElementById(`testimonial-card-${h - 1}`); + if (t) { + const o = m.scrollLeft - t.offsetLeft + m.offsetLeft; + m.scrollBy({ left: -o, behavior: "smooth" }); + } +}); diff --git a/js/previewStack.js b/js/previewStack.js new file mode 100644 index 0000000000000000000000000000000000000000..2a8cc412cf368df1aec7f94d783f785f6cd9632c --- /dev/null +++ b/js/previewStack.js @@ -0,0 +1,46 @@ +(function () { + const percentageSeen = (element) => { + const parent = document.getElementById("stacked-cards-component-demo"); + const child = element.children.item(0); + // Get the relevant measurements and positions + const parentRect = parent.getBoundingClientRect(); + + const childRect = child.getBoundingClientRect(); + + if (childRect.bottom < parentRect.bottom) return 100; + if (childRect.top > parentRect.bottom) return 0; + return Math.round( + (Math.abs(parentRect.bottom - childRect.top) / childRect.height) * 100 + ); + }; + const stackedCardsSection = document.getElementById( + "stacked-cards-component-demo" + ); + const stackedCards = document.getElementById("preview-stacked-cards"); + const cardsArray = Array.from(stackedCards.children); + + stackedCardsSection.addEventListener("scroll", function () { + const percentageOfSecondCardSeen = percentageSeen(cardsArray[1]); + const percentageOfThirdCardSeen = percentageSeen(cardsArray[2]); + cardsArray[0].children.item(0).style.scale = + 1 - + (0.1 * percentageOfSecondCardSeen) / 100 - + (0.1 * percentageOfThirdCardSeen) / 100; + cardsArray[0].children.item(0).style.opacity = + 1 - + (0.1 * percentageOfSecondCardSeen) / 100 - + (0.1 * percentageOfThirdCardSeen) / 100; + cardsArray[0].children.item(0).style.filter = `blur(${ + (10 * percentageOfSecondCardSeen) / 100 + + (10 * percentageOfThirdCardSeen) / 100 + }px)`; + + cardsArray[1].children.item(0).style.scale = + 1 - (0.1 * percentageOfThirdCardSeen) / 100; + cardsArray[1].children.item(0).style.opacity = + 1 - (0.1 * percentageOfThirdCardSeen) / 100; + cardsArray[1].children.item(0).style.filter = `blur(${ + (10 * percentageOfThirdCardSeen) / 100 + }px)`; + }); +})(); diff --git a/js/pythonHighlighter.js b/js/pythonHighlighter.js new file mode 100644 index 0000000000000000000000000000000000000000..c051b1ba9c71257a3869c836c278919ebdf1a6bd --- /dev/null +++ b/js/pythonHighlighter.js @@ -0,0 +1,61 @@ +hljs.registerLanguage("python-custom", (hljs) => { + var python = hljs.getLanguage("python"); + + python.name = "Python Custom"; + + python.contains.push({ + className: "decorator", + begin: "/@/", + end: "/$/", + contains: [ + { + className: "symbol", + match: "@", + }, + { + className: "name", + begin: /[\w\.]+/, + }, + { + className: "params", + begin: /\(/, + end: /\)/, + }, + ], + }); + + var yellowTokens = { + className: "yellow-char", + begin: /\*/, + }; + var whiteTokens = { + className: "white-char", + begin: /[,.:()@]/, + }; + var attributeTokens = { + className: "attribute", + begin: /\./, + end: /[\w]+/, + contains: [ + { + begin: /\w+/, + className: "attr-name", + }, + ], + }; + + // Remove id from built-in keywords + python.keywords.built_in = python.keywords.built_in.filter( + (el) => el !== `id` + ); + + // Ensure custom rules are applied at the beginning + python.contains.push(whiteTokens); + python.contains.push(attributeTokens); + python.contains.push(yellowTokens); + + return python; +}); + +hljs.highlightAll(); +hljs.initLineNumbersOnLoad(); diff --git a/js/stack.js b/js/stack.js new file mode 100644 index 0000000000000000000000000000000000000000..904baa9b73d78a3b923a3a1f0d42b5ca24ec5cf6 --- /dev/null +++ b/js/stack.js @@ -0,0 +1,113 @@ +(function () { + const stackedCardsSection = document.getElementById("stacked-cards-section"); + const stickyHeader = stackedCardsSection.children[0]; + const stackedCards = document.getElementById("stacked-cards"); + const cardsArray = Array.from(stackedCards.children); + const cardsHeights = cardsArray.map( + (card) => card.children.item(0).getBoundingClientRect().height + ); + + const mobileAndTabletCheck = function () { + let check = false; + (function (a) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( + a + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4) + ) + ) + check = true; + })(navigator.userAgent || navigator.vendor || window.opera); + return check; + }; + + if (mobileAndTabletCheck() || window.innerWidth < 1024) { + stackedCards.style.gap = "var(--cardMargin)"; + cardsArray.forEach((card) => (card.style.position = "relative")); + stickyHeader.style.position = "relative"; + return; + } + + const percentageSeen = (element, offset) => { + // Get the relevant measurements and positions + const viewportHeight = window.innerHeight; + const scrollTop = window.scrollY; + const elementOffsetTop = element.offsetTop; + const elementHeight = element.offsetHeight; + + // Calculate percentage of the element that's been seen + const distance = scrollTop + viewportHeight - elementOffsetTop - offset; + const percentage = Math.round( + distance / ((viewportHeight + elementHeight) / 100) + ); + + // Restrict the range to between 0 and 100 + return Math.min(100, Math.max(0, percentage)); + }; + + cardsArray.forEach((card) => { + card.style.top = `calc(${ + stickyHeader.getBoundingClientRect().height + "px" + } + var(--cardsSectionGap) + calc(var(--index) * var(--cardTopPadding)))`; + }); + + const headerBottomStop = +getComputedStyle(stackedCardsSection) + .getPropertyValue("--cardsHeaderBottomStop") + .slice(0, -2); + + document.addEventListener("scroll", function () { + const percentageOfSecondCardSeen = percentageSeen( + cardsArray[1], + 64 + stackedCardsSection.offsetTop + ); + const percentageOfThirdCardSeen = percentageSeen( + cardsArray[2], + 128 + stackedCardsSection.offsetTop + ); + + if ( + Math.abs( + stickyHeader.getBoundingClientRect().bottom - + stackedCardsSection.getBoundingClientRect().bottom + ) < + headerBottomStop + cardsHeights[2] + ) { + stickyHeader.children.item( + 0 + ).style.paddingBottom = `calc(var(--cardsHeaderBottomStop) + ${ + cardsHeights[2] + "px" + })`; + } else { + stickyHeader.children.item(0).style.paddingBottom = ""; + } + + cardsArray[0].children.item(0).style.scale = + 1 - + (0.1 * percentageOfSecondCardSeen) / 100 - + (0.1 * percentageOfThirdCardSeen) / 100; + cardsArray[0].children.item(0).style.opacity = + 1 - + (0.1 * percentageOfSecondCardSeen) / 100 - + (0.1 * percentageOfThirdCardSeen) / 100; + cardsArray[0].children.item(0).style.filter = `blur(${ + (10 * Math.max(percentageOfSecondCardSeen - 0.2, 0)) / 100 + + (10 * Math.max(percentageOfThirdCardSeen - 0.2, 0)) / 100 + }px)`; + cardsArray[0].style.paddingBottom = `calc( + var(--cardTopPadding) * 2 - ${cardsHeights[0] - cardsHeights[2]}px + )`; + + cardsArray[1].children.item(0).style.scale = + 1 - (0.1 * percentageOfThirdCardSeen) / 100; + cardsArray[1].children.item(0).style.opacity = + 1 - (0.1 * percentageOfThirdCardSeen) / 100; + cardsArray[1].children.item(0).style.filter = `blur(${ + (10 * Math.max(percentageOfThirdCardSeen - 0.2, 0)) / 100 + }px)`; + cardsArray[1].style.paddingBottom = `calc( + var(--cardTopPadding) - ${cardsHeights[1] - cardsHeights[2]}px + )`; + }); +})(); diff --git a/js/togglePreview.js b/js/togglePreview.js new file mode 100644 index 0000000000000000000000000000000000000000..362b190fdf70ae539fee1e20679d2f4db9e63b2d --- /dev/null +++ b/js/togglePreview.js @@ -0,0 +1,64 @@ +document.addEventListener("DOMContentLoaded", function () { + const tabButtons = document.querySelectorAll(".toggle-button"); + const codeContainers = document.querySelectorAll(".code-container"); + const tabList = document.getElementById("tab-list"); + const tabListRect = tabList.getBoundingClientRect(); + + // The first button container is our reference, since the highlighter + // is initialized there. Any translations happen over this position. + const firstButtonContainer = document.querySelector(".button-container"); + + tabButtons.forEach((button) => { + button.addEventListener("click", () => { + const currentButtonContainer = button.parentElement; + const currentButtonContainerRect = + currentButtonContainer.getBoundingClientRect(); + // Scroll the tab list to the clicked button + // Try to center it, go as far as possible + const offset = + currentButtonContainerRect.left - + tabListRect.left + + currentButtonContainerRect.width / 2 - + tabListRect.width / 2; + tabList.scrollBy({ left: offset, behavior: "smooth" }); + + // Make highlighter follow the clicked button + const highlighter = document.getElementById("highlighter"); + // Remove 'active' class from all button containers + codeContainers.forEach((container) => { + container.style.display = "none"; + }); + + tabButtons.forEach((btn) => { + btn.parentElement.ariaSelected = false; + btn.parentElement.classList.remove("active"); + btn.parentElement.classList.add("text-white/80"); + btn.parentElement.classList.add("hover:text-white"); + btn.parentElement.classList.remove("text-white"); + }); + + // Add 'active' class to the clicked button + const activeCodeContainer = document.getElementById( + button.dataset.target + ); + activeCodeContainer.style.display = "flex"; + currentButtonContainer.ariaSelected = true; + currentButtonContainer.classList.add("active"); + currentButtonContainer.classList.add("text-white"); + currentButtonContainer.classList.remove("text-white/80"); + + // Move highlighter to the clicked button + const buttonRect = currentButtonContainer.getBoundingClientRect(); + const firstButtonContainerRect = + firstButtonContainer.getBoundingClientRect(); + + const translateX = buttonRect.x - firstButtonContainerRect.x; + + highlighter.style.transform = `translateX(${translateX}px)`; + }); + }); + + tabButtons.forEach((button) => { + button.addEventListener("click", () => {}); + }); +}); diff --git a/js/videoPopup.js b/js/videoPopup.js new file mode 100644 index 0000000000000000000000000000000000000000..5009de2217327ba9e99fb51b6931b37de9e0d224 --- /dev/null +++ b/js/videoPopup.js @@ -0,0 +1,24 @@ +(function () { + const videoLink = document.getElementById('videoLink'); + const videoPopup = document.getElementById('videoPopup'); + const closePopup = document.getElementById('closePopup'); + const youtubeVideo = document.getElementById('youtubeVideo'); + + videoLink.addEventListener('click', (e) => { + e.preventDefault(); + youtubeVideo.src = 'https://www.youtube.com/embed/' + videoLink.getAttribute('video-id'); + videoPopup.classList.remove('hidden'); + }); + + closePopup.addEventListener('click', () => { + youtubeVideo.src = ''; + videoPopup.classList.add('hidden'); + }); + + videoPopup.addEventListener('click', (e) => { + if (e.target === videoPopup) { + youtubeVideo.src = ''; + videoPopup.classList.add('hidden'); + } + }) +})(); \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..cb843be5bc4eeec08266625efd3b1d2f16900d31 --- /dev/null +++ b/main.py @@ -0,0 +1,282 @@ +from fasthtml.common import * +from home_components import * +from content import * + +_blank = dict(target="_blank", rel="noopener noreferrer") +description = 'Modern web applications in pure Python' + +def benefit(title, content): + return Div( + H3(title, cls=f"text-black heading-3"), + P(content, cls=f"l-body mt-6 lg:mt-6"), + cls="w-full p-6 bg-soft-yellow rounded-2xl xl:p-12 lg:h-[22rem] lg:w-[26rem]") + +def faq_item(question, answer, id): + return accordion( + id=id, question=question, answer=answer, + question_cls="text-black s-body", + answer_cls="s-body text-black/80 col-span-full", + container_cls=f"{col} justify-between bg-soft-blue rounded-[1.25rem] {bnset}" + ) + +def hero_section(): + return ( + Header( + Nav( + A( + Img(src='/assets/logo.svg', alt='FastHTML', width='105', height='24'), + href='#'), + A('Read docs', href='/docs', **_blank, + cls=f'bg-black text-white py-2 px-4 s-body rounded-[62.5rem] hover:bg-black/80 transition-colors duration-300 px-4 py-1 h-10 {center} justify-center'), + cls=f'py-2 px-4 {between} items-center rounded-full w-full max-w-[400px] bg-white/50 backdrop-blur-lg border border-white/20'), + cls=f'fixed top-0 w-full left-0 p-4 {center} justify-center z-50'), + Section( + Div( + File('assets/hero-shapes.svg'), + cls='absolute z-0 lg:-top-[15%] top-0 left-1/2 -translate-x-1/2 grid grid-cols-1 grid-rows-1 w-[120%] aspect-square max-w-[2048px] min-w-[900px]'), + Div( + Div(cls='lg:flex-1 max-lg:basis-[152px]'), + Div( + H1(description, cls='heading-1 max-w-3xl'), + P('Built on solid web foundations, not the latest fads - with\nFastHTML you can get started on anything from simple dashboards to\nscalable web applications in minutes.', + cls='m-body max-w-[40rem] text-center'), + cls=f'flex-1 {col} items-center justify-center gap-6 text-center w-full text-black' + ), + Div( + A('Learn more', href='https://fastht.ml/about', **_blank, + cls=f'{bnset} m-body px-4 py-1 rounded-full bg-black hover:bg-black/80 transition-colors duration-300 text-white h-[76px] w-full max-w-[350px] flex items-center justify-center'), + video_button('Watch intro', '/assets/thumb.png', '7min 30sec', "QqZUzkPcU7A?si=lTtHuMT5HPC66-49"), + cls=f'flex-1 {center} justify-center content-center flex-wrap lg:gap-6 gap-4 m-body'), + video_player('Try now'), + cls=f'{col} flex-1 relative px-4 lg:px-16'), + cls=f'{col} relative w-full h-screen max-h-[1024px] min-h-[720px] overflow-hidden bg-grey') + ) + +def code_display(file_name, code_snippet, snippet_id): + return Div( + Div( + Div( + P(file_name, cls=f"xs-mono-body {center}"), + Button( + Div( + Img(cls="button-content w-4 h-4", src="assets/icons/copy-icon.svg", alt="Copy"), + Span("Copied!", cls=f"absolute bg-inherit rounded-l-[0.5rem] {center} py-2 h-full right-0 copied-text overflow-hidden w-0 peer-clicked:w-fit s-body text-white/80 transition-transform transition-all duration-600 ease-out"), + cls=f"relative bg-black/20 rounded-[0.5rem] {center} p-2 h-8 w-fit"), + cls="copy-button"), + cls=f"w-full {between}"), + cls="bg-black/20 text-white/80 w-full max-w-2xl lg:max-w-xl p-4 rounded-2xl"), + Pre( + Code(code_snippet, cls="python w-full mono-body"), + id=snippet_id, + cls="code-snippet relative max-h-[25rem] overflow-hidden hide-scrollbar"), + Div(cls="absolute code-fade-out bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-[#3a2234] pointer-events-none"), + cls=f"relative {col} gap-6 lg:max-w-[45rem] w-full overflow-hidden" + ) + +def code_demo(title, file_name, code_snippet, demo_content, is_active=False): + snippet_id = f"{title.lower().replace(' ', '-')}-code-snippet" + demo_cls = f"{center} my-11 p-4 flex-none whitespace-normal justify-center h-96 rounded-3xl bg-soft-purple lg:p-8 w-full max-w-2xl lg:max-w-md lg:mx-28 lg:my-8 overflow-y-auto {'lg:items-start hide-scrollbar' if snippet_id == 'reusable-code-snippet' else ''}" + return Div( + code_display(file_name, code_snippet, snippet_id), + Div(demo_content, cls=demo_cls), + aria_labelledby=f"tab-{title.lower().replace(' ', '-')}", role="tabpanel", + cls=f"code-container pt-8 lg:pt-16 tab-panel relative hide-scrollbar toggle-element {col} lg:flex-row lg:justify-between overflow-hidden w-full lg:max-w-[1440px] xl:mx-auto {'hidden' if not is_active else ''}", + id=f"{title.lower().replace(' ', '-')}-code-demo") + +def tab_button(title, is_active=False): + classes = f"z-10 button-container flex-none relative px-8 py-2 w-[10.59375rem] h-11 rounded-full {center}" + active = f"{classes} current active transition-all duration-300 text-white" + inactive = f"{classes} text-white/80 hover:text-white" + return Li( + Button( + Div(title, cls="m-body w-max mx-auto"), + id=f"tab-{title.lower().replace(' ', '-')}", + cls="toggle-button w-full cursor-pointer", + data_target=f"{title.lower().replace(' ', '-')}-code-demo", + tabindex="-1" if not is_active else None, + name=f"tab-{title.lower().replace(' ', '-')}"), + role="tab", aria_selected="true" if is_active else "false", + cls=active if is_active else inactive) + +async def component_preview_section(): + cs = await components() + return Section( + Div( + Div( + H2("This home page is a FastHTML app.", cls="text-white heading-2 pt-8"), + P("Click the buttons below to see four small, live components in action.", cls=f"text-white pt-8 l-body {maxrem(32)}"), + cls=f"{maxrem(50)} mx-auto {col} {center} text-center gap-6" + ), + *[code_demo(title, file_name, code_snippet, demo_content, i == 0) for i, (title, file_name, code_snippet, demo_content) in enumerate(cs)], + Ul( + *[tab_button(title, i == 0) for i, (title, _, _, _) in enumerate(cs)], + Div(id="highlighter", cls=f"{inset} z-0 highlighter w-[10.59375rem] absolute bg-white/20 h-[2.75rem] rounded-[62.5rem] transition-transform duration-300"), + role="tablist", id="tab-list", + cls=f"relative mt-12 text-white/80 flex-none rounded-[62.5rem] bg-black/20 p-2 max-w-full overflow-x-auto lg:mx-auto {center} hide-scrollbar lg:max-w-[43.375rem]"), + cls=f"{col} {center}"), + cls="relative bg-purple px-4 lg:px-16 pb-24 -mt-8 lg:-mt-10 flex-col xl:items-center items-start gap-6 lg:gap-16 rounded-t-3xl lg:rounded-t-[2.5rem] overflow-x-hidden") + +def stacked_cards_section(): + return Section( + Div( + Div( + Div( + P("TECH STACK", cls="mono-body text-opacity-60"), + H2("FastHTML scales up and scales down.", cls="text-black heading-2"), + P("Read more about our ", + A("design philosophy here", href="https://fastht.ml/about/vision", **_blank, + cls="border-b-2 border-b-black/30 hover:border-b-black/80"), + ", or click a button below:", + cls=f"l-body {maxrem(32)}"), + cls=f"{maxrem(50)} mx-auto {col} {center} text-center gap-6"), + cls="py-8 lg:pt-16 px-4 lg:px-16 rounded-t-3xl lg:rounded-t-[2.5rem] bg-green"), + cls="bg-yellow sticky top-0 bottom-[calc(100%-300px)] w-full"), + Div( + Div( + *[stacked_card(title, description, tech_stack, "bg-soft-green") for title, description, tech_stack in stacked], + id="stacked-cards", cls=f"{maxrem(50)} mx-auto"), + cls="px-4 lg:px-16 w-full bg-green pt-8"), + id="stacked-cards-section", cls="relative") + +def samples_section(): + text = "FastHTML can be used for everything from collaborative games to multi-modal UI. We've selected small self-contained examples for you to learn from." + return section_wrapper( + (section_header("SAMPLES", "See FastHTML in action", text, max_width=40), + Div( + *[Div( + A( + File(f"assets/{svg}"), + Div( + P(name, cls="border-b-2 border-b-black/30 hover:border-b-black/80 regular-body"), + Img(src=f"{icons}/arrow-up-right.svg", alt="Arrow right icon", cls="group-hover:translate-y-[-0.1rem] transition-all ease-in-out duration-300"), + cls=f"{gap2} transition-transform transform relative items-center mt-4 lg:mt-6"), + href=url, cls=f"{col} items-center"), + cls="group px-2" + ) for name, svg, url in samples], + cls="grid max-w-5xl lg:grid-cols-4 lg:max-w-7xl lg:gap-x-12 grid-cols-2 gap-x-4 gap-y-8 w-full mx-auto"), + A("Discover all", href=eg_url, + cls=f"{bnset} bg-black text-white py-2 px-4 s-body rounded-full hover:bg-black/70 transition-colors duration-300")), + bg_color="grey") + +def how_it_works_section(): + msg = "With FastHTML you create good-looking modern web applications in pure Python and deploy them in minutes." + return section_wrapper( + (Div( + section_header( "GET STARTED IN MINUTES", "The fastest way to create a real web application.", msg), + cls="max-w-3xl w-full mx-auto flex-col items-center text-center gap-6 mb-8 lg:mb-8"), + Div(*[benefit(title, content) for title, content in benefits], + cls=f"{col} w-full lg:flex-row gap-4 items-center lg:gap-8 max-w-7xl mx-auto justify-center"),), + bg_color="yellow", flex=False) + +def faq_section(): + return section_wrapper( + Div( + section_header( "FAQ", "Questions? Answers.", "Your top FastHTML questions clarified.", + max_width=21, center=False), + Div( + *[faq_item(question, answer, i+3) for i, (question, answer) in enumerate(faqs)], + cls=f"{col} gap-4 {maxrem(32)} transition ease-out delay-[300ms]"), + cls=f"{section_base} w-full mx-auto lg:flex-row items-start max-w-7xl"), + bg_color="blue") + +def testimonials_section(): + testimonial_cards = [testimonial_card(i, *args) for i, args in enumerate(testimonials)] + return section_wrapper( + Div( + section_header( + "LOVE IS IN THE AIR", "What the experts say", + "Top web programmers tell us that they love working with FastHTML.", + max_width=21, center=False), + carousel(testimonial_cards), + cls=f"{section_base} {maxrem(90)} mx-auto lg:flex-row items-start"), + bg_color="pink") + +def footer_link(text, href, **kw): + return Li(A( + Span(text, cls="border-b border-b-transparent border-b-white/50"), + Img(src=f"{icons}/arrow-up-right-white.svg", alt="Arrow right icon", width="16", height="16", cls="w-4 h-4"), + href=href, cls=f"{gap2} items-center hover:text-white border-b border-b-transparent hover:border-b-white", **kw)) + +def footer(): + return Section( + Div( + Div( + P("© 2024 onwards AnswerDotAI. All rights reserved.", cls="mr-auto"), + Nav( + Ul( + footer_link("Github", "https://github.com/AnswerDotAI/fasthtml", **_blank), + footer_link("Join Discord", "https://discord.gg/qcXvcxMhdP", **_blank), + footer_link("Docs", "/docs", **_blank), + footer_link("Site design", "https://tinloof.com/", **_blank), + cls="flex max-lg:flex-col max-lg:items-start gap-4 lg:gap-6 flex-wrap")), + cls="relative z-[1] mono-s flex max-lg:flex-col gap-6 text-white/80 px-4 lg:px-16 pb-16"), + Div( + Div( + File("assets/footer-shapes.svg"), + cls=f"absolute z-0 lg:-top-[15%] top-0 left-1/2 -translate-x-1/2 grid grid-cols-1 grid-rows-1 lg:w-[150%] w-[200%] aspect-square max-w-none min-w-max"), + File("assets/footer-path.svg"), + cls=f"relative z-0 w-full px-4 lg:px-16 pb-1 {col} flex-1 justify-end"), + cls=f"relative w-full h-[420px] lg:h-[600px] {col} pt-8 lg:pt-12 rounded-t-3xl lg:rounded-t-[2.5rem] bg-black overflow-hidden -mt-8 lg:-mt-10")) + +hdrs = [ + Meta(charset='UTF-8'), + Meta(name='viewport', content='width=device-width, initial-scale=1.0, maximum-scale=1.0'), + Meta(name='description', content=description), + *Favicon('favicon.ico', 'favicon-dark.ico'), + *Socials(title='FastHTML', + description=description, + site_name='fastht.ml', + twitter_site='@answerdotai', + image=f'/assets/og-sq.png', + url=''), + # surrsrc, + Script(src='https://cdn.jsdelivr.net/gh/gnat/surreal@main/surreal.js'), + scopesrc, + Link(href='css/main.css', rel='stylesheet'), + Link(href='css/tailwind.css', rel='stylesheet'), + Link(href='css/stack.css', rel='stylesheet'), + Link(href='css/preview-stack.css', rel='stylesheet'), + Link(href='css/highlighter-theme.css', rel='stylesheet')] + +from about.main import app as about_app +routes = [Mount(f"/about", about_app), + Mount('/docs', StaticFiles(directory='docs', html=True)), + ] + +bodykw = {"class": "relative bg-black font-geist text-black/80 font-details-off"} +app,rt = fast_app(hdrs=hdrs, default_hdrs=False, bodykw=bodykw, on_startup=[startup], routes=routes) + + +scripts = ( + Script(src='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js'), + Script(src='https://cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js'), + # Script(src='js/videoPopup.js'), + Script(src='js/pythonHighlighter.js'), + Script(src='js/carouselScroll.js'), + Script(src='js/stack.js'), + Script(src='js/togglePreview.js'), + Script(src='js/codeOverflow.js'), + Script(src='js/copyCode.js'), + ) + +from fastcore.xtras import timed_cache + +@timed_cache(seconds=60) +async def home(): + return (Title(f"FastHTML - {description}"), + Main( + hero_section(), + await component_preview_section(), + how_it_works_section(), + stacked_cards_section(), + samples_section(), + faq_section(), + testimonials_section(), + footer()), + *scripts) + +@rt("/") +async def get(): return await home() + +serve() + diff --git a/requirements.txt b/requirements.txt index e0a2242db3565b8be160e4dd8090e98ec8f9fe82..6b2776a6e615ee10b5168c1f0b9b83a6cc212c07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,4 @@ monsterui mistletoe lxml fa6_icons -altair -pandas -streamlit \ No newline at end of file + diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000000000000000000000000000000000000..55c44f1a8a8d320737bd76c9e66c863bcf644a0e --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,159 @@ + + + + https://www.fastht.ml/docs/index.html + 2025-06-05T05:56:08.709Z + + + https://www.fastht.ml/docs/tutorials/index.html + 2025-05-18T23:30:33.077Z + + + https://www.fastht.ml/docs/tutorials/jupyter_and_fasthtml.html + 2025-06-05T05:56:08.861Z + + + https://www.fastht.ml/docs/tutorials/quickstart_for_web_devs.html + 2025-06-05T05:56:08.783Z + + + https://www.fastht.ml/docs/api/components.html + 2025-06-05T05:56:08.793Z + + + https://www.fastht.ml/docs/api/js.html + 2025-06-05T05:56:08.839Z + + + https://www.fastht.ml/docs/api/cli.html + 2025-06-05T05:56:08.764Z + + + https://www.fastht.ml/docs/api/xtend.html + 2025-06-05T05:56:08.766Z + + + https://www.fastht.ml/docs/explains/websockets.html + 2025-06-05T05:56:08.712Z + + + https://www.fastht.ml/docs/explains/explaining_xt_components.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/explains/stripe.html + 2025-06-05T05:56:08.712Z + + + https://www.fastht.ml/docs/explains/faq.html + 2025-06-05T05:56:08.709Z + + + https://www.fastht.ml/docs/unpublished/tutorial_for_web_devs.html + 2025-06-05T05:56:08.709Z + + + https://www.fastht.ml/docs/ref/response_types.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/ref/live_reload.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/ref/best_practice.html + 2025-06-05T06:06:30.727Z + + + https://www.fastht.ml/docs/ref/concise_guide.html + 2025-06-05T05:56:08.713Z + + + https://www.fastht.ml/docs/ref/handlers.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/ref/defining_xt_component.html + 2025-06-05T05:56:08.709Z + + + https://www.fastht.ml/docs/explains/minidataapi.html + 2025-06-05T05:56:08.711Z + + + https://www.fastht.ml/docs/explains/routes.html + 2025-06-05T05:56:08.710Z + + + https://www.fastht.ml/docs/explains/background_tasks.html + 2025-06-05T05:56:08.708Z + + + https://www.fastht.ml/docs/explains/oauth.html + 2025-06-05T05:56:08.713Z + + + https://www.fastht.ml/docs/api/core.html + 2025-06-05T05:56:08.840Z + + + https://www.fastht.ml/docs/api/pico.html + 2025-06-05T05:56:08.760Z + + + https://www.fastht.ml/docs/api/oauth.html + 2025-06-05T05:56:08.843Z + + + https://www.fastht.ml/docs/api/svg.html + 2025-06-05T05:56:08.858Z + + + https://www.fastht.ml/docs/api/jupyter.html + 2025-06-05T05:56:08.808Z + + + https://www.fastht.ml/docs/tutorials/e2e.html + 2025-06-05T05:56:08.851Z + + + https://www.fastht.ml/docs/tutorials/best_practice.html + 2025-06-05T06:03:10.577Z + + + https://www.fastht.ml/docs/tutorials/by_example.html + 2025-06-05T05:56:08.854Z + + + https://www.fastht.ml/ + 2025-06-05T06:18:45+00:00 + + + https://www.fastht.ml/about + 2025-06-05T06:18:45+00:00 + + + https://www.fastht.ml/about/vision + 2025-06-05T06:18:45+00:00 + + + https://www.fastht.ml/about/foundation + 2025-06-05T06:18:45+00:00 + + + https://www.fastht.ml/about/tech + 2025-06-05T06:18:45+00:00 + + + https://www.fastht.ml/about/components + 2025-06-05T06:18:45+00:00 + + + https://www.fastht.ml/docs/llms.txt + 2025-06-05T06:18:45+00:00 + + + https://www.fastht.ml/docs/llms.html + 2025-06-05T06:18:45+00:00 + + diff --git a/sqlite.db b/sqlite.db new file mode 100644 index 0000000000000000000000000000000000000000..4e86411b5803e34b1e4767ce981907694f15c1ed Binary files /dev/null and b/sqlite.db differ diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..7002b82d7cbf6e64eac02435e2a64e56cb96e4bb --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["*.py"], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/texts.db b/texts.db new file mode 100644 index 0000000000000000000000000000000000000000..cd3f4e8d7568f4a1c52b8875dae8857952cf774f --- /dev/null +++ b/texts.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d5c46d9672dcca696e52209d18961244a0bd9beae566dbfbc8064bfbee35d63 +size 8196096 diff --git a/tools/docs.sh b/tools/docs.sh new file mode 100644 index 0000000000000000000000000000000000000000..82d0217d4d5171f37f9bc06fdc861ffe489783a8 --- /dev/null +++ b/tools/docs.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -eux + +pushd ../fasthtml +nbdev_docs +popd +cp -r ../fasthtml/_docs/* docs/ +tools/sitemap.py +git add -A +git status + diff --git a/tools/pages b/tools/pages new file mode 100644 index 0000000000000000000000000000000000000000..847f348b64599c1ed1040507f39b0a1c8cd014a0 --- /dev/null +++ b/tools/pages @@ -0,0 +1,9 @@ +/ +/about +/about/vision +/about/foundation +/about/tech +/about/components +/docs/llms.txt +/docs/llms.html + diff --git a/tools/sitemap.py b/tools/sitemap.py new file mode 100644 index 0000000000000000000000000000000000000000..88f118d38df16b32a18383c1b36fce19b36c66ba --- /dev/null +++ b/tools/sitemap.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +from fastcore.utils import * +import fastcore.xtras +from lxml import etree +from datetime import datetime, timezone + +my_path = Path(__file__).parent.parent.absolute() +git_path = my_path.parent.absolute() +assert git_path.exists() +fhome_path = git_path/'home-fasthtml' +about_path = fhome_path/'about' +docs_path = fhome_path/'docs' +tools_path = fhome_path/'tools' + +pages = (tools_path/'pages').read_text().strip().splitlines() +sitemap = (docs_path/'sitemap.xml').read_text() + +parser = etree.XMLParser(remove_blank_text=True) +root = etree.fromstring(sitemap.encode('utf-8'), parser) + +ns = root.nsmap[None] +current_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00") +for page in pages: + url = etree.SubElement(root, f"{{{ns}}}url") + loc = etree.SubElement(url, f"{{{ns}}}loc") + loc.text = f"https://www.fastht.ml{page}" + lastmod = etree.SubElement(url, f"{{{ns}}}lastmod") + lastmod.text = current_time + +res = etree.tostring(root, xml_declaration=True, encoding='UTF-8', pretty_print=True).decode('utf-8') +(my_path/'sitemap.xml').write_text(res) + diff --git a/weather.py b/weather.py new file mode 100644 index 0000000000000000000000000000000000000000..f33a7e5e86e30f43e31ccf249c118b0c2525d029 --- /dev/null +++ b/weather.py @@ -0,0 +1,32 @@ +import asyncio, httpx + +coordinates = { + "New York": (40.7128, -74.0060), +# "Los Angeles": (34.0500, -118.2500), + "San Francisco": (37.7749, -122.4194), + "Chicago": (41.8333, -87.6167), + "Houston": (29.7500, -95.3500), + "Washington": (38.8833, -77.0333) +} + +async def weather(client, city): + lat,lon = coordinates[city] + point = (await client.get(f"https://api.weather.gov/points/{lat},{lon}")).json()['properties'] + station_url = point['observationStations'] + stations = (await client.get(station_url)).json() + first_url = stations['features'][0]['id'] + try: + obs = (await client.get(f"{first_url}/observations/latest")).json() + obs = obs['properties'] + except: return city, dict(temp='NA',wind='NA',humid='NA') + def val(x): return round(x['value'], 1) if x['value'] else 'NA' + return city, dict(temp=val(obs['temperature']), + wind=val(obs['windSpeed']), + humid=val(obs['relativeHumidity'])) + +async def all_weather(): + async with httpx.AsyncClient() as client: + tasks = [weather(client, city) for city in coordinates] + results = await asyncio.gather(*tasks) + return dict(results) +