Skip to main content

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

fhswiftui-0.0.4.tar.gz (19.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fhswiftui-0.0.4-py3-none-any.whl (14.8 kB view details)

Uploaded Python 3

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

Hashes for fhswiftui-0.0.4.tar.gz
Algorithm Hash digest
SHA256 10e65b7dacf0c3a3d0502385c7c1c511770e462fc6a1c8d9b82dce52f233e501
MD5 daa1a6d87a2e3092ec79ede53f5e938a
BLAKE2b-256 a03ed8d3c9d1bbfcd084fe4b6b6804cbf11d44ff1b06eb163b75449e12016876

See more details on using hashes here.

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

Hashes for fhswiftui-0.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 ee6cbbb119e88d2a4b09a8b3c594cd1bbbf077e2270962e29f10ce53ed921afd
MD5 66e2517e835859194aaf090359be8dc4
BLAKE2b-256 8db5ffff8b5b5734c4f1e576e80a053ce457feb4556ca6ba42948f910ad99128

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page