# fhswiftui


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

`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](https://pypi.org/project/fhswiftui/)

``` sh
$ 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.

``` html
<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.

``` python
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=" &lt;!doctype html&gt;
 &lt;html&gt;
   &lt;head&gt;
     &lt;title&gt;FastHTML page&lt;/title&gt;
     &lt;link rel=&quot;canonical&quot; href=&quot;https://testserver/_QhS4wSqvSwqrbJvdz7wwjQ&quot;&gt;
     &lt;meta charset=&quot;utf-8&quot;&gt;
     &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, viewport-fit=cover&quot;&gt;
&lt;script src=&quot;https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js&quot;&gt;&lt;/script&gt;     &lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css&quot;&gt;
&lt;script src=&quot;https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4&quot;&gt;&lt;/script&gt;     &lt;div class=&quot;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&quot;&gt;&lt;/div&gt;
&lt;script&gt;
    function sendmsg() {
        window.parent.postMessage({height: document.documentElement.offsetHeight}, &#x27;*&#x27;);
    }
    window.onload = function() {
        sendmsg();
        document.body.addEventListener(&#x27;htmx:afterSettle&#x27;,    sendmsg);
        document.body.addEventListener(&#x27;htmx:wsAfterMessage&#x27;, sendmsg);
    };&lt;/script&gt;   &lt;/head&gt;
   &lt;body&gt;
     &lt;div class=&quot;max-w-lg &quot;&gt;
       &lt;div class=&quot;flex flex-col items-start gap-6 p-6&quot;&gt;
         &lt;div class=&quot;flex flex-row items-start gap-4 p-4 bg-gray-50 rounded shadow&quot;&gt;
           &lt;div class=&quot;flex flex-col items-start gap-1&quot;&gt;
&lt;span class=&quot;text-2xl font-bold&quot;&gt;fhswiftui Demo&lt;/span&gt;&lt;span class=&quot;text-sm font-normal text-gray-500&quot;&gt;SwiftUI-inspired layouts with Basecoat styling&lt;/span&gt;           &lt;/div&gt;
           &lt;div class=&quot;flex-grow&quot;&gt;&lt;/div&gt;
&lt;button class=&quot;btn btn-outline btn-sm&quot;&gt;Settings&lt;/button&gt;         &lt;/div&gt;
         &lt;div class=&quot;flex flex-row items-start gap-4&quot;&gt;
           &lt;article class=&quot;card&quot;&gt;
             &lt;header&gt;
               &lt;h3 class=&quot;font-semibold&quot;&gt;Typography&lt;/h3&gt;
             &lt;/header&gt;
             &lt;section&gt;
               &lt;div class=&quot;flex flex-col items-start gap-2&quot;&gt;
&lt;span class=&quot;text-2xl font-bold&quot;&gt;Title&lt;/span&gt;&lt;span class=&quot;text-xl font-semibold&quot;&gt;Headline&lt;/span&gt;&lt;span class=&quot;text-base font-normal&quot;&gt;Body text goes here&lt;/span&gt;&lt;span class=&quot;text-sm font-normal text-gray-300&quot;&gt;Caption for details&lt;/span&gt;&lt;span class=&quot;text-xs font-normal text-gray-500&quot;&gt;Footnote&lt;/span&gt;               &lt;/div&gt;
             &lt;/section&gt;
           &lt;/article&gt;
           &lt;article class=&quot;card&quot;&gt;
             &lt;header&gt;
               &lt;h3 class=&quot;font-semibold&quot;&gt;Form Controls&lt;/h3&gt;
             &lt;/header&gt;
             &lt;section&gt;
               &lt;div class=&quot;flex flex-col items-start gap-3&quot;&gt;
                 &lt;div class=&quot;field&quot;&gt;
&lt;label class=&quot;label&quot;&gt;Email&lt;/label&gt;                   &lt;input name=&quot;email&quot; placeholder=&quot;you@example.com&quot; class=&quot;input&quot;&gt;
                 &lt;/div&gt;
                 &lt;div class=&quot;field&quot;&gt;
&lt;label class=&quot;label&quot;&gt;Password&lt;/label&gt;                   &lt;input name=&quot;password&quot; placeholder=&quot;••••••••&quot; type=&quot;password&quot; class=&quot;input&quot;&gt;
                 &lt;/div&gt;
                 &lt;div class=&quot;flex flex-row items-start gap-2&quot;&gt;
&lt;button class=&quot;btn btn-outline&quot;&gt;Cancel&lt;/button&gt;&lt;button class=&quot;btn btn-primary&quot;&gt;Submit&lt;/button&gt;                 &lt;/div&gt;
               &lt;/div&gt;
             &lt;/section&gt;
           &lt;/article&gt;
         &lt;/div&gt;
         &lt;div class=&quot;flex flex-row items-start gap-3&quot;&gt;
&lt;span class=&quot;badge&quot;&gt;Active&lt;/span&gt;&lt;span class=&quot;badge badge-destructive&quot;&gt;Error&lt;/span&gt;         &lt;/div&gt;
         &lt;div class=&quot;border-t w-full my-2&quot;&gt;&lt;/div&gt;
&lt;span class=&quot;text-xl font-semibold&quot;&gt;Semantic Colors&lt;/span&gt;         &lt;div class=&quot;flex flex-row items-start gap-2&quot;&gt;
&lt;span class=&quot;text-gray-900&quot;&gt;Primary&lt;/span&gt;&lt;span class=&quot;text-gray-500&quot;&gt;Secondary&lt;/span&gt;&lt;span class=&quot;text-blue-600&quot;&gt;Accent&lt;/span&gt;&lt;span class=&quot;text-green-600&quot;&gt;Success&lt;/span&gt;&lt;span class=&quot;text-amber-500&quot;&gt;Warning&lt;/span&gt;&lt;span class=&quot;text-red-600&quot;&gt;Danger&lt;/span&gt;         &lt;/div&gt;
       &lt;/div&gt;
     &lt;/div&gt;
   &lt;/body&gt;
 &lt;/html&gt;
" 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> 
