- Add PLAN.md with 40-step checklist across 10 phases - Add CLAUDE.md with project-specific instructions - Set up nix flake with FastHTML/MonsterUI dependencies - Create Python package skeleton (src/animaltrack) - Vendor FastHTML and MonsterUI documentation - Add Docker build configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
112 lines
5.1 KiB
Markdown
112 lines
5.1 KiB
Markdown
"""FrankenUI Mail Example built with MonsterUI (original design by ShadCN)"""
|
|
|
|
from fasthtml.common import *
|
|
from monsterui.all import *
|
|
from fasthtml.svg import *
|
|
import pathlib, json
|
|
from datetime import datetime
|
|
|
|
app, rt = fast_app(hdrs=Theme.blue.headers())
|
|
|
|
sidebar_group1 = (('home', 'Inbox', '128'), ('file-text', 'Drafts', '9'), (' arrow-up-right', 'Sent', ''),
|
|
('ban', 'Junk', '23'), ('trash', 'Trash', ''), ('folder', 'Archive', ''))
|
|
|
|
sidebar_group2 = (('globe','Social','972'),('info','Updates','342'),('messages-square','Forums','128'),
|
|
('shopping-cart','Shopping','8'),('shopping-bag','Promotions','21'),)
|
|
|
|
def MailSbLi(icon, title, cnt):
|
|
return Li(A(DivLAligned(Span(UkIcon(icon)),Span(title),P(cnt, cls=TextPresets.muted_sm)),href='#', cls='hover:bg-secondary p-4'))
|
|
|
|
sidebar = NavContainer(
|
|
NavHeaderLi(H3("Email"), cls='p-3'),
|
|
Li(Select(map(Option, ('alicia@example.com','alicia@gmail.com', 'alicia@yahoo.com')))),
|
|
*[MailSbLi(i, t, c) for i, t, c in sidebar_group1],
|
|
Li(Hr()),
|
|
*[MailSbLi(i, t, c) for i, t, c in sidebar_group2],
|
|
cls='mt-3')
|
|
|
|
mail_data = json.load(open(pathlib.Path('data_/mail.json')))
|
|
|
|
def format_date(date_str):
|
|
date_obj = datetime.fromisoformat(date_str)
|
|
return date_obj.strftime("%Y-%m-%d %I:%M %p")
|
|
|
|
def MailItem(mail):
|
|
cls_base = 'relative rounded-lg border border-border p-3 text-sm hover:bg-secondary space-y-2'
|
|
cls = f"{cls_base} {'bg-muted' if mail == mail_data[0] else ''} {'tag-unread' if not mail['read'] else 'tag-mail'}"
|
|
|
|
return Li(
|
|
DivFullySpaced(
|
|
DivLAligned(
|
|
Strong(mail['name']),
|
|
Span(cls='flex h-2 w-2 rounded-full bg-blue-600') if not mail['read'] else ''),
|
|
Time(format_date(mail['date']), cls='text-xs')),
|
|
Small(mail['subject'], href=f"#mail-{mail['id']}"),
|
|
Div(mail['text'][:100] + '...', cls=TextPresets.muted_sm),
|
|
DivLAligned(
|
|
*[Label(A(label, href='#'), cls='uk-label-primary' if label == 'work' else '') for label in mail['labels']]),
|
|
cls=cls)
|
|
|
|
def MailList(mails): return Ul(cls='js-filter space-y-2 p-4 pt-0')(*[MailItem(mail) for mail in mails])
|
|
|
|
def MailContent():
|
|
return Div(cls='flex flex-col',uk_filter="target: .js-filter")(
|
|
Div(cls='flex px-4 py-2 ')(
|
|
H3('Inbox'),
|
|
TabContainer(Li(A("All Mail",href='#', role='button'),cls='uk-active', uk_filter_control="filter: .tag-mail"),
|
|
Li(A("Unread",href='#', role='button'), uk_filter_control="filter: .tag-unread"),
|
|
alt=True, cls='ml-auto max-w-40', )),
|
|
Div(cls='flex flex-1 flex-col')(
|
|
Div(cls='p-4')(
|
|
Div(cls='uk-inline w-full')(
|
|
Span(cls='uk-form-icon text-muted-foreground')(UkIcon('search')),
|
|
Input(placeholder='Search'))),
|
|
Div(cls='flex-1 overflow-y-auto max-h-[600px]')(MailList(mail_data))))
|
|
|
|
def IconNavItem(*d): return [Li(A(UkIcon(o[0],uk_tooltip=o[1]))) for o in d]
|
|
def IconNav(*c,cls=''): return Ul(cls=f'uk-iconnav {cls}')(*c)
|
|
|
|
def MailDetailView(mail):
|
|
top_icons = [('folder','Archive'), ('ban','Move to junk'), ('trash','Move to trash')]
|
|
reply_icons = [('reply','Reply'), ('reply','Reply all'), ('forward','Forward')]
|
|
dropdown_items = ['Mark as unread', 'Star read', 'Add Label', 'Mute Thread']
|
|
|
|
return Container(
|
|
DivFullySpaced(
|
|
DivLAligned(
|
|
DivLAligned(*[UkIcon(o[0],uk_tooltip=o[1]) for o in top_icons]),
|
|
Div(UkIcon('clock', uk_tooltip='Snooze'), cls='pl-2'),
|
|
cls='space-x-2 divide-x divide-border'),
|
|
DivLAligned(
|
|
*[UkIcon(o[0],uk_tooltip=o[1]) for o in reply_icons],
|
|
Div(UkIcon('ellipsis-vertical',button=True)),
|
|
DropDownNavContainer(*map(lambda x: Li(A(x)), dropdown_items)))),
|
|
DivLAligned(
|
|
Span(mail['name'][:2], cls='flex h-10 w-10 items-center justify-center rounded-full bg-muted'),
|
|
Div(Strong(mail['name']),
|
|
Div(mail['subject']),
|
|
DivLAligned(P('Reply-To:'), A(mail['email'], href=f"mailto:{mail['email']}"), cls='space-x-1'),
|
|
P(Time(format_date(mail['date']))),
|
|
cls='space-y-1'+TextT.sm),
|
|
cls='m-4 space-x-4'),
|
|
DividerLine(),
|
|
P(mail['text'], cls=TextT.sm +'p-4'),
|
|
DividerLine(),
|
|
Div(TextArea(id='message', placeholder=f"Reply {mail['name']}"),
|
|
DivFullySpaced(
|
|
LabelSwitch('Mute this thread',id='mute'),
|
|
Button('Send', cls=ButtonT.primary)),
|
|
cls='space-y-4'))
|
|
|
|
@rt
|
|
def index():
|
|
return Title("Mail Example"),Container(
|
|
Grid(Div(sidebar, cls='col-span-1'),
|
|
Div(MailContent(), cls='col-span-2'),
|
|
Div(MailDetailView(mail_data[0]), cls='col-span-2'),
|
|
cols_sm=1, cols_md=1, cols_lg=5, cols_xl=5,
|
|
gap=0, cls='flex-1'),
|
|
cls=('flex', ContainerT.xl))
|
|
|
|
serve()
|