SwiftUI inspired wrapper for FastHTML
Project description
fhswiftui
fhswiftui is a SwiftUI inspired wrapper for FastHTML and tailwind.
fhswiftui is built on top of FastHTML and tailwind and will work with
all FT components. Tailwind classes and FastHTML features work
seamlessly.
FastHTML enables composing HTML apps by nested Python objects and tailwind is a CSS framework that provides standard CSS components. Since tailwind relies heavily on CSS classes to define components as well as include style information, this can quickly become complex and difficult to read.
For example, let’s consider a simple example of a turn in a chat which displays a user’s message with a timestamp. In FastHTML + tailwind, this would look like something:
Section(
Div(
Span('04:30:56', cls='text-xs text-muted-foreground'),
Div('What is the meaning of life?', cls='flex w-max max-w-[75%] flex-col gap-2 rounded-lg px-3 py-2 text-sm border-l-4 border-primary italic'),
cls='flex gap-2 items-center'
),
cls='space-y-6'
)
fhswiftui provides layout and UI components. It also provides property
setters that enable specific styling. The same example above using
fhswiftui:
HStack(alignment="center")(
Text('04:30:56').fg("muted-foreground").font(size="xs"),
VDivider(border_color),
Div(t.m).padding(left=3, right=3, top=2, bottom=2).font(size="sm")
)
fhswiftui is built on top of FastHTML and tailwind and will work with
all FT components. Tailwind classes and FastHTML features work
seamlessly.
Installation
Install latest from pypi
$ pip install fhswiftui
Why build yet another toolkit
Tailwind CSS gets complicated quickly
This slightly more comprhensive demo illustrates how per element classes can explode quickly and become difficult to read.
<div class="flex flex-col items-start gap-6 p-6">
<div class="flex flex-row items-start gap-4 p-4 bg-gray-50 rounded shadow">
<div class="flex flex-col items-start gap-1">
<span class="text-2xl font-bold">fhswiftui Demo</span><span class="text-sm font-normal text-gray-500">SwiftUI-inspired layouts with Basecoat styling</span> </div>
<div class="flex-grow"></div>
<button class="btn btn-outline btn-sm">Settings</button> </div>
<div class="flex flex-row items-start gap-4">
<article class="card">
<header>
<h3 class="font-semibold">Typography</h3>
</header>
<section>
<div class="flex flex-col items-start gap-2">
<span class="text-2xl font-bold">Title</span>
<span class="text-xl font-semibold">Headline</span>
<span class="text-base font-normal">Body text goes here</span>
<span class="text-sm font-normal text-gray-300">Caption for details</span>
<span class="text-xs font-normal text-gray-500">Footnote</span>
</div>
</section>
</article>
<article class="card">
<header>
<h3 class="font-semibold">Form Controls</h3>
</header>
<section>
<div class="flex flex-col items-start gap-3">
<div class="field">
<label class="label">Email</label>
<input name="email" placeholder="you@example.com" class="input">
</div>
<div class="field">
<label class="label">Password</label>
<input name="password" placeholder="••••••••" type="password" class="input">
</div>
<div class="flex flex-row items-start gap-2">
<button class="btn btn-outline">Cancel</button><button class="btn btn-primary">Submit</button>
</div>
</div>
</section>
</article>
</div>
<div class="flex flex-row items-start gap-3">
<span class="badge">Active</span><span class="badge badge-destructive">Error</span>
</div>
<div class="border-t w-full my-2"></div>
<span class="text-xl font-semibold">Semantic Colors</span>
<div class="flex flex-row items-start gap-2">
<span class="text-gray-900">Primary</span>
<span class="text-gray-500">Secondary</span>
<span class="text-blue-600">Accent</span>
<span class="text-green-600">Success</span>
<span class="text-amber-500">Warning</span>
<span class="text-red-600">Danger</span>
</div>
</div>
Using semantic expressions preserves readablity
By using semantic expressions, this preserves the intent of the designer. Changes become safer.
Notice, that we coexist with FastHTML so it is always possible to dive
into raw Tailwind CSS if needed.
We believe, this encourages composiblity. Real world applications would define components that could be composed together in a light scaffolding.
from fasthtml.components import *
from fhswiftui import *
p = mk_previewer()
html = VStack(spacing=6)(
HStack(spacing=4)(
VStack(spacing=1)(
Text("fhswiftui Demo").font(Font.Title),
Text("SwiftUI-inspired layouts with Basecoat styling").font(Font.Caption).fg(Color.Secondary),
),
Spacer(),
Button("Settings", cls="btn btn-outline btn-sm"),
).padding().bg(Color.Surface).corner_radius().shadow(),
# Feature showcase in cards
HStack(spacing=4)(
Article(cls="card")(
Header(H3("Typography", cls="font-semibold")),
Section(
VStack(spacing=2)(
Text("Title").font(Font.Title),
Text("Headline").font(Font.Headline),
Text("Body text goes here").font(Font.Body),
Text("Caption for details").font(Font.Caption).fg(Color.Muted),
Text("Footnote").font(Font.Footnote).fg(Color.Secondary),
)
)
),
# Card 2: Form elements
Article(cls="card")(
Header(H3("Form Controls", cls="font-semibold")),
Section(
VStack(spacing=3)(
Div(cls="field")(
Label("Email", cls="label"),
TextField("email", placeholder="you@example.com", cls="input"),
),
Div(cls="field")(
Label("Password", cls="label"),
TextField("password", placeholder="••••••••", type="password", cls="input"),
),
HStack(spacing=2)(
Button("Cancel", cls="btn btn-outline"),
Button("Submit", cls="btn btn-primary"),
),
)
)
),
),
# Status row
HStack(spacing=3)(
Span("Active", cls="badge"),
Span("Error", cls="badge badge-destructive"),
),
# Divider
HDivider(),
# Color palette showcase
Text("Semantic Colors").font(Font.Headline),
HStack(spacing=2)(
Text("Primary").fg(Color.Primary),
Text("Secondary").fg(Color.Secondary),
Text("Accent").fg(Color.Accent),
Text("Success").fg(Color.Success),
Text("Warning").fg(Color.Warning),
Text("Danger").fg(Color.Danger),
),
).padding(6)
p(html)
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_QhS4wSqvSwqrbJvdz7wwjQ">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.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> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <div class="hidden border-l-primary border-l-secondary border-l-accent border-l-muted border-l-card border-l-popover border-l-primary-foreground border-l-secondary-foreground border-l-accent-foreground border-l-muted-foreground border-l-card-foreground border-l-popover-foreground border-l-background border-l-foreground border-l-destructive border-l-ring border-l-input border-l-border border-r-primary border-r-secondary border-r-accent border-r-muted border-r-card border-r-popover border-r-primary-foreground border-r-secondary-foreground border-r-accent-foreground border-r-muted-foreground border-r-card-foreground border-r-popover-foreground border-r-background border-r-foreground border-r-destructive border-r-ring border-r-input border-r-border border-t-primary border-t-secondary border-t-accent border-t-muted border-t-card border-t-popover border-t-primary-foreground border-t-secondary-foreground border-t-accent-foreground border-t-muted-foreground border-t-card-foreground border-t-popover-foreground border-t-background border-t-foreground border-t-destructive border-t-ring border-t-input border-t-border border-b-primary border-b-secondary border-b-accent border-b-muted border-b-card border-b-popover border-b-primary-foreground border-b-secondary-foreground border-b-accent-foreground border-b-muted-foreground border-b-card-foreground border-b-popover-foreground border-b-background border-b-foreground border-b-destructive border-b-ring border-b-input border-b-border bg-primary bg-secondary bg-accent bg-muted bg-card bg-popover bg-primary-foreground bg-secondary-foreground bg-accent-foreground bg-muted-foreground bg-card-foreground bg-popover-foreground bg-background bg-foreground bg-destructive bg-ring bg-input bg-border text-primary text-secondary text-accent text-muted text-card text-popover text-primary-foreground text-secondary-foreground text-accent-foreground text-muted-foreground text-card-foreground text-popover-foreground text-background text-foreground text-destructive text-ring text-input text-border border-primary border-secondary border-accent border-muted border-card border-popover border-primary-foreground border-secondary-foreground border-accent-foreground border-muted-foreground border-card-foreground border-popover-foreground border-background border-foreground border-destructive border-ring border-input border-border"></div>
<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 class="max-w-lg ">
<div class="flex flex-col items-start gap-6 p-6">
<div class="flex flex-row items-start gap-4 p-4 bg-gray-50 rounded shadow">
<div class="flex flex-col items-start gap-1">
<span class="text-2xl font-bold">fhswiftui Demo</span><span class="text-sm font-normal text-gray-500">SwiftUI-inspired layouts with Basecoat styling</span> </div>
<div class="flex-grow"></div>
<button class="btn btn-outline btn-sm">Settings</button> </div>
<div class="flex flex-row items-start gap-4">
<article class="card">
<header>
<h3 class="font-semibold">Typography</h3>
</header>
<section>
<div class="flex flex-col items-start gap-2">
<span class="text-2xl font-bold">Title</span><span class="text-xl font-semibold">Headline</span><span class="text-base font-normal">Body text goes here</span><span class="text-sm font-normal text-gray-300">Caption for details</span><span class="text-xs font-normal text-gray-500">Footnote</span> </div>
</section>
</article>
<article class="card">
<header>
<h3 class="font-semibold">Form Controls</h3>
</header>
<section>
<div class="flex flex-col items-start gap-3">
<div class="field">
<label class="label">Email</label> <input name="email" placeholder="you@example.com" class="input">
</div>
<div class="field">
<label class="label">Password</label> <input name="password" placeholder="••••••••" type="password" class="input">
</div>
<div class="flex flex-row items-start gap-2">
<button class="btn btn-outline">Cancel</button><button class="btn btn-primary">Submit</button> </div>
</div>
</section>
</article>
</div>
<div class="flex flex-row items-start gap-3">
<span class="badge">Active</span><span class="badge badge-destructive">Error</span> </div>
<div class="border-t w-full my-2"></div>
<span class="text-xl font-semibold">Semantic Colors</span> <div class="flex flex-row items-start gap-2">
<span class="text-gray-900">Primary</span><span class="text-gray-500">Secondary</span><span class="text-blue-600">Accent</span><span class="text-green-600">Success</span><span class="text-amber-500">Warning</span><span class="text-red-600">Danger</span> </div>
</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fhswiftui-0.0.4.tar.gz.
File metadata
- Download URL: fhswiftui-0.0.4.tar.gz
- Upload date:
- Size: 19.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10e65b7dacf0c3a3d0502385c7c1c511770e462fc6a1c8d9b82dce52f233e501
|
|
| MD5 |
daa1a6d87a2e3092ec79ede53f5e938a
|
|
| BLAKE2b-256 |
a03ed8d3c9d1bbfcd084fe4b6b6804cbf11d44ff1b06eb163b75449e12016876
|
File details
Details for the file fhswiftui-0.0.4-py3-none-any.whl.
File metadata
- Download URL: fhswiftui-0.0.4-py3-none-any.whl
- Upload date:
- Size: 14.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee6cbbb119e88d2a4b09a8b3c594cd1bbbf077e2270962e29f10ce53ed921afd
|
|
| MD5 |
66e2517e835859194aaf090359be8dc4
|
|
| BLAKE2b-256 |
8db5ffff8b5b5734c4f1e576e80a053ce457feb4556ca6ba42948f910ad99128
|