fh-matui 0.9.3__py3-none-any.whl → 0.9.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fh_matui/__init__.py +1 -1
- fh_matui/_modidx.py +6 -1
- fh_matui/datatable.py +233 -73
- fh_matui-0.9.5.dist-info/METADATA +243 -0
- fh_matui-0.9.5.dist-info/RECORD +14 -0
- fh_matui-0.9.3.dist-info/METADATA +0 -188
- fh_matui-0.9.3.dist-info/RECORD +0 -14
- {fh_matui-0.9.3.dist-info → fh_matui-0.9.5.dist-info}/WHEEL +0 -0
- {fh_matui-0.9.3.dist-info → fh_matui-0.9.5.dist-info}/entry_points.txt +0 -0
- {fh_matui-0.9.3.dist-info → fh_matui-0.9.5.dist-info}/licenses/LICENSE +0 -0
- {fh_matui-0.9.3.dist-info → fh_matui-0.9.5.dist-info}/top_level.txt +0 -0
fh_matui/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.9.
|
|
1
|
+
__version__ = "0.9.4"
|
fh_matui/_modidx.py
CHANGED
|
@@ -128,10 +128,15 @@ d = { 'settings': { 'branch': 'master',
|
|
|
128
128
|
'fh_matui.core._ThemeNamespace.violet': ('core.html#_themenamespace.violet', 'fh_matui/core.py'),
|
|
129
129
|
'fh_matui.core._ThemeNamespace.yellow': ('core.html#_themenamespace.yellow', 'fh_matui/core.py'),
|
|
130
130
|
'fh_matui.core._ThemeNamespace.zinc': ('core.html#_themenamespace.zinc', 'fh_matui/core.py')},
|
|
131
|
-
'fh_matui.datatable': { 'fh_matui.datatable.
|
|
131
|
+
'fh_matui.datatable': { 'fh_matui.datatable.CrudContext': ('datatable.html#crudcontext', 'fh_matui/datatable.py'),
|
|
132
|
+
'fh_matui.datatable.DataTable': ('datatable.html#datatable', 'fh_matui/datatable.py'),
|
|
132
133
|
'fh_matui.datatable.DataTableResource': ('datatable.html#datatableresource', 'fh_matui/datatable.py'),
|
|
133
134
|
'fh_matui.datatable.DataTableResource.__init__': ( 'datatable.html#datatableresource.__init__',
|
|
134
135
|
'fh_matui/datatable.py'),
|
|
136
|
+
'fh_matui.datatable.DataTableResource._build_context': ( 'datatable.html#datatableresource._build_context',
|
|
137
|
+
'fh_matui/datatable.py'),
|
|
138
|
+
'fh_matui.datatable.DataTableResource._call_hook': ( 'datatable.html#datatableresource._call_hook',
|
|
139
|
+
'fh_matui/datatable.py'),
|
|
135
140
|
'fh_matui.datatable.DataTableResource._error_toast': ( 'datatable.html#datatableresource._error_toast',
|
|
136
141
|
'fh_matui/datatable.py'),
|
|
137
142
|
'fh_matui.datatable.DataTableResource._filter_by_search': ( 'datatable.html#datatableresource._filter_by_search',
|
fh_matui/datatable.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_datatable.ipynb.
|
|
4
4
|
|
|
5
5
|
# %% auto 0
|
|
6
|
-
__all__ = ['PAGE_SIZES', 'table_state_from_request', 'DataTable', 'DataTableResource']
|
|
6
|
+
__all__ = ['PAGE_SIZES', 'logger', 'table_state_from_request', 'DataTable', 'CrudContext', 'DataTableResource']
|
|
7
7
|
|
|
8
8
|
# %% ../nbs/05_datatable.ipynb 2
|
|
9
9
|
from fastcore.utils import *
|
|
@@ -17,7 +17,7 @@ from .core import *
|
|
|
17
17
|
from nbdev.showdoc import show_doc
|
|
18
18
|
from .components import *
|
|
19
19
|
|
|
20
|
-
# %% ../nbs/05_datatable.ipynb
|
|
20
|
+
# %% ../nbs/05_datatable.ipynb 7
|
|
21
21
|
#| code-fold: true
|
|
22
22
|
from math import ceil
|
|
23
23
|
from urllib.parse import urlencode
|
|
@@ -288,7 +288,102 @@ def DataTable(
|
|
|
288
288
|
|
|
289
289
|
return Div(card, Div(id=feedback_id), id=container_id)
|
|
290
290
|
|
|
291
|
-
# %% ../nbs/05_datatable.ipynb
|
|
291
|
+
# %% ../nbs/05_datatable.ipynb 9
|
|
292
|
+
import asyncio
|
|
293
|
+
import logging
|
|
294
|
+
from dataclasses import dataclass
|
|
295
|
+
from typing import Callable, Any, Optional
|
|
296
|
+
from starlette.responses import HTMLResponse
|
|
297
|
+
|
|
298
|
+
logger = logging.getLogger(__name__)
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class CrudContext:
|
|
302
|
+
"""
|
|
303
|
+
🎯 Context object passed to enhanced CRUD operation hooks.
|
|
304
|
+
|
|
305
|
+
Provides rich access to request state, user info, database, and record data.
|
|
306
|
+
Perfect for implementing complex business logic and external API integration.
|
|
307
|
+
|
|
308
|
+
## 📦 Fields
|
|
309
|
+
|
|
310
|
+
- `request`: Full Starlette request object (headers, query params, session, state)
|
|
311
|
+
- `user`: Current user dict from `request.state.user` (if available)
|
|
312
|
+
- `db`: Database instance from `request.state.tenant_db` (if available)
|
|
313
|
+
- `tbl`: Table instance from `request.state.tables[table_name]` (if available)
|
|
314
|
+
- `record`: Form data dict with field values
|
|
315
|
+
- `record_id`: Record ID for update/delete operations (None for create)
|
|
316
|
+
|
|
317
|
+
## 💡 Usage in Hooks
|
|
318
|
+
|
|
319
|
+
### Example: Create with External API
|
|
320
|
+
```python
|
|
321
|
+
async def quiltt_create_connection(ctx: CrudContext) -> dict:
|
|
322
|
+
# Access user info
|
|
323
|
+
user_id = ctx.user['user_id']
|
|
324
|
+
|
|
325
|
+
# Call external API
|
|
326
|
+
api = QuilttClient()
|
|
327
|
+
response = await api.create_connection(
|
|
328
|
+
institution=ctx.record['institution_name'],
|
|
329
|
+
user_id=user_id
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Enrich record with API response
|
|
333
|
+
ctx.record['connection_id'] = response['id']
|
|
334
|
+
ctx.record['account_id'] = response['account_id']
|
|
335
|
+
ctx.record['status'] = 'pending'
|
|
336
|
+
|
|
337
|
+
return ctx.record # DataTableResource will insert this
|
|
338
|
+
|
|
339
|
+
DataTableResource(
|
|
340
|
+
...,
|
|
341
|
+
on_create=quiltt_create_connection # 🆕 Enhanced hook
|
|
342
|
+
)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Example: Soft Delete
|
|
346
|
+
```python
|
|
347
|
+
def soft_delete_budget(ctx: CrudContext) -> None:
|
|
348
|
+
# Access table directly
|
|
349
|
+
ctx.tbl.update({
|
|
350
|
+
'id': ctx.record_id,
|
|
351
|
+
'is_deleted': True,
|
|
352
|
+
'deleted_at': datetime.now().isoformat(),
|
|
353
|
+
'deleted_by': ctx.user['user_id']
|
|
354
|
+
})
|
|
355
|
+
# No return needed for delete hooks
|
|
356
|
+
|
|
357
|
+
DataTableResource(
|
|
358
|
+
...,
|
|
359
|
+
on_delete=soft_delete_budget, # 🆕 Custom delete logic
|
|
360
|
+
get_table=lambda req: req.state.tables['budgets']
|
|
361
|
+
)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Example: Update with Sync
|
|
365
|
+
```python
|
|
366
|
+
async def sync_transaction_update(ctx: CrudContext) -> dict:
|
|
367
|
+
# Update external API first
|
|
368
|
+
api = TransactionAPI()
|
|
369
|
+
await api.update_transaction(
|
|
370
|
+
transaction_id=ctx.record_id,
|
|
371
|
+
data=ctx.record
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Add sync timestamp
|
|
375
|
+
ctx.record['last_synced'] = datetime.now().isoformat()
|
|
376
|
+
return ctx.record
|
|
377
|
+
```
|
|
378
|
+
"""
|
|
379
|
+
request: Any # Full Starlette request
|
|
380
|
+
user: Optional[dict] = None # request.state.user (if available)
|
|
381
|
+
db: Optional[Any] = None # request.state.tenant_db (if available)
|
|
382
|
+
tbl: Optional[Any] = None # request.state.tables[table_name] (if available)
|
|
383
|
+
record: dict = None # Form data dict
|
|
384
|
+
record_id: Optional[Any] = None # ID for update/delete (None for create)
|
|
385
|
+
|
|
386
|
+
# %% ../nbs/05_datatable.ipynb 12
|
|
292
387
|
from typing import Callable, Optional, Any, Union
|
|
293
388
|
from dataclasses import asdict, is_dataclass
|
|
294
389
|
from datetime import datetime
|
|
@@ -309,7 +404,20 @@ def _to_dict(obj: Any) -> dict:
|
|
|
309
404
|
|
|
310
405
|
|
|
311
406
|
class DataTableResource:
|
|
312
|
-
"
|
|
407
|
+
"""
|
|
408
|
+
🔧 High-level resource that auto-registers all routes for a data table.
|
|
409
|
+
|
|
410
|
+
**Features:**
|
|
411
|
+
- Custom CRUD hooks with `CrudContext` for rich business logic
|
|
412
|
+
- Async/sync hook support for external API integration
|
|
413
|
+
- Auto-refresh table via HX-Trigger after mutations
|
|
414
|
+
- Request state accessors for multi-tenant apps
|
|
415
|
+
|
|
416
|
+
**Auto-registers 3 routes:**
|
|
417
|
+
- `GET {base_route}` → DataTable list view
|
|
418
|
+
- `GET {base_route}/action` → FormModal for create/edit/view/delete
|
|
419
|
+
- `POST {base_route}/save` → Save handler with hooks
|
|
420
|
+
"""
|
|
313
421
|
|
|
314
422
|
def __init__(
|
|
315
423
|
self,
|
|
@@ -328,15 +436,16 @@ class DataTableResource:
|
|
|
328
436
|
search_placeholder: str = "Search...",
|
|
329
437
|
create_label: str = "New Record",
|
|
330
438
|
empty_message: str = "No records found.",
|
|
331
|
-
#
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
on_after_update: Callable[[Any], None] = None,
|
|
336
|
-
on_before_delete: Callable[[Any], bool] = None,
|
|
337
|
-
on_after_delete: Callable[[Any], None] = None,
|
|
439
|
+
# CRUD Hooks (with CrudContext)
|
|
440
|
+
on_create: Callable[[CrudContext], dict] = None,
|
|
441
|
+
on_update: Callable[[CrudContext], dict] = None,
|
|
442
|
+
on_delete: Callable[[CrudContext], None] = None,
|
|
338
443
|
# Multi-tenant
|
|
339
444
|
user_filter: Callable = None,
|
|
445
|
+
# Request state accessors (for CrudContext)
|
|
446
|
+
get_user: Callable = None,
|
|
447
|
+
get_db: Callable = None,
|
|
448
|
+
get_table: Callable = None,
|
|
340
449
|
# Custom generators
|
|
341
450
|
id_generator: Callable[[], Any] = None,
|
|
342
451
|
timestamp_fields: dict = None
|
|
@@ -359,24 +468,26 @@ class DataTableResource:
|
|
|
359
468
|
# Determine CRUD ops from provided functions
|
|
360
469
|
if crud_ops is None:
|
|
361
470
|
self.crud_ops = {
|
|
362
|
-
"create": create is not None,
|
|
363
|
-
"update": update is not None,
|
|
364
|
-
"delete": delete is not None
|
|
471
|
+
"create": create is not None or on_create is not None,
|
|
472
|
+
"update": update is not None or on_update is not None,
|
|
473
|
+
"delete": delete is not None or on_delete is not None
|
|
365
474
|
}
|
|
366
475
|
else:
|
|
367
476
|
self.crud_ops = crud_ops
|
|
368
477
|
|
|
369
|
-
#
|
|
370
|
-
self.
|
|
371
|
-
self.
|
|
372
|
-
self.
|
|
373
|
-
self.on_after_update = on_after_update
|
|
374
|
-
self.on_before_delete = on_before_delete
|
|
375
|
-
self.on_after_delete = on_after_delete
|
|
478
|
+
# CRUD hooks
|
|
479
|
+
self.on_create_hook = on_create
|
|
480
|
+
self.on_update_hook = on_update
|
|
481
|
+
self.on_delete_hook = on_delete
|
|
376
482
|
|
|
377
483
|
# Multi-tenant
|
|
378
484
|
self.user_filter = user_filter
|
|
379
485
|
|
|
486
|
+
# Request state accessors
|
|
487
|
+
self.get_user = get_user
|
|
488
|
+
self.get_db = get_db
|
|
489
|
+
self.get_table = get_table
|
|
490
|
+
|
|
380
491
|
# Generators
|
|
381
492
|
self.id_generator = id_generator
|
|
382
493
|
self.timestamp_fields = timestamp_fields or {}
|
|
@@ -385,25 +496,68 @@ class DataTableResource:
|
|
|
385
496
|
self.container_id = f"crud-table-{base_route.replace('/', '-').strip('-')}"
|
|
386
497
|
self.feedback_id = f"{self.container_id}-feedback"
|
|
387
498
|
self.modal_id = f"{self.container_id}-modal"
|
|
499
|
+
self.refresh_trigger = f"{self.container_id}-refresh"
|
|
388
500
|
|
|
389
501
|
# Register routes
|
|
390
502
|
self._register_routes()
|
|
391
503
|
|
|
504
|
+
async def _call_hook(self, hook, ctx: CrudContext):
|
|
505
|
+
"""🔄 Call hook function, handling both sync and async."""
|
|
506
|
+
if hook is None:
|
|
507
|
+
return None
|
|
508
|
+
if asyncio.iscoroutinefunction(hook):
|
|
509
|
+
return await hook(ctx)
|
|
510
|
+
return hook(ctx)
|
|
511
|
+
|
|
512
|
+
def _build_context(self, req, record: dict = None, record_id: Any = None) -> CrudContext:
|
|
513
|
+
"""🏗️ Build CrudContext from request state."""
|
|
514
|
+
user = None
|
|
515
|
+
db = None
|
|
516
|
+
tbl = None
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
if self.get_user and callable(self.get_user):
|
|
520
|
+
user = self.get_user(req)
|
|
521
|
+
elif hasattr(req, 'state') and hasattr(req.state, 'user'):
|
|
522
|
+
user = req.state.user
|
|
523
|
+
except AttributeError:
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
if self.get_db and callable(self.get_db):
|
|
528
|
+
db = self.get_db(req)
|
|
529
|
+
elif hasattr(req, 'state') and hasattr(req.state, 'tenant_db'):
|
|
530
|
+
db = req.state.tenant_db
|
|
531
|
+
except AttributeError:
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
if self.get_table and callable(self.get_table):
|
|
536
|
+
tbl = self.get_table(req)
|
|
537
|
+
except AttributeError:
|
|
538
|
+
pass
|
|
539
|
+
|
|
540
|
+
return CrudContext(
|
|
541
|
+
request=req,
|
|
542
|
+
user=user,
|
|
543
|
+
db=db,
|
|
544
|
+
tbl=tbl,
|
|
545
|
+
record=record or {},
|
|
546
|
+
record_id=record_id
|
|
547
|
+
)
|
|
548
|
+
|
|
392
549
|
def _register_routes(self):
|
|
393
550
|
"""Register all data table routes with the app."""
|
|
394
551
|
rt = self.app.route
|
|
395
552
|
|
|
396
|
-
# Main table route
|
|
397
553
|
@rt(self.base_route)
|
|
398
554
|
def _table_handler(req):
|
|
399
555
|
return self._handle_table(req)
|
|
400
556
|
|
|
401
|
-
# Action route (view/edit/create/delete)
|
|
402
557
|
@rt(f"{self.base_route}/action")
|
|
403
558
|
def _action_handler(req):
|
|
404
559
|
return self._handle_action(req)
|
|
405
560
|
|
|
406
|
-
# Save route (form submission)
|
|
407
561
|
@rt(f"{self.base_route}/save")
|
|
408
562
|
async def _save_handler(req):
|
|
409
563
|
return await self._handle_save(req)
|
|
@@ -411,11 +565,8 @@ class DataTableResource:
|
|
|
411
565
|
def _get_filtered_data(self, req) -> list:
|
|
412
566
|
"""Get all data, optionally filtered by user_filter."""
|
|
413
567
|
all_data = self.get_all()
|
|
414
|
-
|
|
415
|
-
# Convert to list of dicts
|
|
416
568
|
data = [_to_dict(item) for item in all_data]
|
|
417
569
|
|
|
418
|
-
# Apply user filter if provided
|
|
419
570
|
if self.user_filter and callable(self.user_filter):
|
|
420
571
|
filter_criteria = self.user_filter(req)
|
|
421
572
|
if filter_criteria:
|
|
@@ -423,7 +574,6 @@ class DataTableResource:
|
|
|
423
574
|
row for row in data
|
|
424
575
|
if all(row.get(k) == v for k, v in filter_criteria.items())
|
|
425
576
|
]
|
|
426
|
-
|
|
427
577
|
return data
|
|
428
578
|
|
|
429
579
|
def _filter_by_search(self, data: list, search: str) -> list:
|
|
@@ -437,7 +587,6 @@ class DataTableResource:
|
|
|
437
587
|
if col.get("searchable", False)
|
|
438
588
|
]
|
|
439
589
|
|
|
440
|
-
# If no columns marked searchable, search all
|
|
441
590
|
if not searchable_keys:
|
|
442
591
|
searchable_keys = [col["key"] for col in self.columns]
|
|
443
592
|
|
|
@@ -451,26 +600,19 @@ class DataTableResource:
|
|
|
451
600
|
total = len(data)
|
|
452
601
|
total_pages = max(1, ceil(total / page_size))
|
|
453
602
|
page = min(max(1, page), total_pages)
|
|
454
|
-
|
|
455
603
|
start = (page - 1) * page_size
|
|
456
604
|
end = start + page_size
|
|
457
|
-
|
|
458
605
|
return data[start:end], total, page
|
|
459
606
|
|
|
460
607
|
def _handle_table(self, req):
|
|
461
608
|
"""Handle main table route."""
|
|
462
|
-
# Extract pagination state
|
|
463
609
|
state = table_state_from_request(req, page_sizes=self.page_sizes)
|
|
464
610
|
search, page, page_size = state["search"], state["page"], state["page_size"]
|
|
465
611
|
|
|
466
|
-
# Get and filter data
|
|
467
612
|
data = self._get_filtered_data(req)
|
|
468
613
|
filtered = self._filter_by_search(data, search)
|
|
469
|
-
|
|
470
|
-
# Paginate
|
|
471
614
|
page_data, total, page = self._paginate(filtered, page, page_size)
|
|
472
615
|
|
|
473
|
-
# Build table
|
|
474
616
|
table = DataTable(
|
|
475
617
|
data=page_data,
|
|
476
618
|
total=total,
|
|
@@ -489,31 +631,40 @@ class DataTableResource:
|
|
|
489
631
|
empty_message=self.empty_message
|
|
490
632
|
)
|
|
491
633
|
|
|
492
|
-
|
|
634
|
+
# Wrap table in auto-refresh container
|
|
635
|
+
params = urlencode({"search": search, "page": page, "page_size": page_size})
|
|
636
|
+
table_container = Div(
|
|
637
|
+
table,
|
|
638
|
+
id=self.container_id,
|
|
639
|
+
hx_trigger=f"{self.refresh_trigger} from:body",
|
|
640
|
+
hx_get=f"{self.base_route}?{params}",
|
|
641
|
+
hx_target=f"#{self.container_id}",
|
|
642
|
+
hx_swap="outerHTML"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
feedback = Div(id=self.feedback_id)
|
|
646
|
+
return Div(feedback, table_container)
|
|
493
647
|
|
|
494
648
|
def _handle_action(self, req):
|
|
495
649
|
"""Handle action route (view/edit/create/delete)."""
|
|
496
650
|
params = getattr(req, "query_params", {})
|
|
497
651
|
getter = params.get if hasattr(params, "get") else (lambda k, d=None: params[k] if k in params else d)
|
|
498
652
|
|
|
499
|
-
# Handle dismiss
|
|
500
653
|
if getter("dismiss") is not None:
|
|
501
654
|
return Div(id=self.feedback_id)
|
|
502
655
|
|
|
503
656
|
record_id = getter("id")
|
|
504
|
-
# Try to convert to int if numeric
|
|
505
657
|
if record_id:
|
|
506
658
|
try:
|
|
507
659
|
record_id = int(record_id)
|
|
508
660
|
except (TypeError, ValueError):
|
|
509
|
-
pass
|
|
661
|
+
pass
|
|
510
662
|
|
|
511
663
|
action = (getter("action", "view") or "view").lower()
|
|
512
664
|
search = getter("search", "") or ""
|
|
513
665
|
page = _safe_int(getter("page", 1), 1)
|
|
514
666
|
page_size = _safe_int(getter("page_size", 10), 10)
|
|
515
667
|
|
|
516
|
-
# URLs
|
|
517
668
|
return_params = urlencode({"search": search, "page": page, "page_size": page_size})
|
|
518
669
|
cancel_url = f"{self.base_route}/action?dismiss=1"
|
|
519
670
|
save_url = f"{self.base_route}/save?{return_params}"
|
|
@@ -581,21 +732,23 @@ class DataTableResource:
|
|
|
581
732
|
if not self.crud_ops.get("delete"):
|
|
582
733
|
return self._error_toast("Delete operation not enabled.")
|
|
583
734
|
|
|
584
|
-
# Check before_delete hook
|
|
585
|
-
if self.on_before_delete:
|
|
586
|
-
if not self.on_before_delete(record_id):
|
|
587
|
-
return self._error_toast("Delete cancelled by validation.")
|
|
588
|
-
|
|
589
|
-
# Perform delete
|
|
590
735
|
try:
|
|
591
|
-
self.
|
|
736
|
+
ctx = self._build_context(req, record=record, record_id=record_id)
|
|
592
737
|
|
|
593
|
-
#
|
|
594
|
-
if self.
|
|
595
|
-
self.
|
|
738
|
+
# Use on_delete hook if provided
|
|
739
|
+
if self.on_delete_hook:
|
|
740
|
+
if asyncio.iscoroutinefunction(self.on_delete_hook):
|
|
741
|
+
loop = asyncio.get_event_loop()
|
|
742
|
+
loop.run_until_complete(self.on_delete_hook(ctx))
|
|
743
|
+
else:
|
|
744
|
+
self.on_delete_hook(ctx)
|
|
745
|
+
else:
|
|
746
|
+
# Default: use delete_fn
|
|
747
|
+
self.delete_fn(record_id)
|
|
596
748
|
|
|
597
|
-
return self._success_toast(
|
|
749
|
+
return self._success_toast("Record deleted successfully.")
|
|
598
750
|
except Exception as e:
|
|
751
|
+
logger.error(f"Delete failed: {e}", exc_info=True)
|
|
599
752
|
return self._error_toast(f"Delete failed: {str(e)}")
|
|
600
753
|
|
|
601
754
|
return Div(id=self.feedback_id)
|
|
@@ -606,14 +759,13 @@ class DataTableResource:
|
|
|
606
759
|
form_data = await req.form()
|
|
607
760
|
record_id = form_data.get(self.row_id_field)
|
|
608
761
|
|
|
609
|
-
# Try to convert ID
|
|
610
762
|
if record_id:
|
|
611
763
|
try:
|
|
612
764
|
record_id = int(record_id)
|
|
613
765
|
except (TypeError, ValueError):
|
|
614
766
|
pass
|
|
615
767
|
|
|
616
|
-
# Build data dict from form
|
|
768
|
+
# Build data dict from form
|
|
617
769
|
data = {}
|
|
618
770
|
for col in self.columns:
|
|
619
771
|
key = col["key"]
|
|
@@ -626,7 +778,6 @@ class DataTableResource:
|
|
|
626
778
|
|
|
627
779
|
value = form_data.get(key)
|
|
628
780
|
|
|
629
|
-
# Type conversion based on form config
|
|
630
781
|
field_type = form_cfg.get("type", "text")
|
|
631
782
|
if field_type == "number" and value:
|
|
632
783
|
try:
|
|
@@ -648,33 +799,36 @@ class DataTableResource:
|
|
|
648
799
|
|
|
649
800
|
# CREATE or UPDATE
|
|
650
801
|
if record_id:
|
|
651
|
-
# UPDATE
|
|
652
|
-
|
|
653
|
-
data = self.on_before_update(record_id, data)
|
|
654
|
-
|
|
655
|
-
result = self.update_fn(record_id, data)
|
|
802
|
+
# ===== UPDATE =====
|
|
803
|
+
ctx = self._build_context(req, record=data, record_id=record_id)
|
|
656
804
|
|
|
657
|
-
if self.
|
|
658
|
-
self.
|
|
805
|
+
if self.on_update_hook:
|
|
806
|
+
data = await self._call_hook(self.on_update_hook, ctx)
|
|
807
|
+
if data is not None:
|
|
808
|
+
self.update_fn(record_id, data)
|
|
809
|
+
else:
|
|
810
|
+
self.update_fn(record_id, data)
|
|
659
811
|
|
|
660
812
|
return self._success_toast("Record updated successfully.")
|
|
661
813
|
else:
|
|
662
|
-
# CREATE
|
|
663
|
-
if self.on_before_create:
|
|
664
|
-
data = self.on_before_create(data)
|
|
665
|
-
|
|
814
|
+
# ===== CREATE =====
|
|
666
815
|
# Generate ID if needed
|
|
667
|
-
if self.id_generator:
|
|
816
|
+
if self.id_generator and self.row_id_field not in data:
|
|
668
817
|
data[self.row_id_field] = self.id_generator()
|
|
669
818
|
|
|
670
|
-
|
|
819
|
+
ctx = self._build_context(req, record=data, record_id=None)
|
|
671
820
|
|
|
672
|
-
if self.
|
|
673
|
-
self.
|
|
821
|
+
if self.on_create_hook:
|
|
822
|
+
data = await self._call_hook(self.on_create_hook, ctx)
|
|
823
|
+
if data is not None:
|
|
824
|
+
self.create_fn(data)
|
|
825
|
+
else:
|
|
826
|
+
self.create_fn(data)
|
|
674
827
|
|
|
675
828
|
return self._success_toast("Record created successfully.")
|
|
676
829
|
|
|
677
830
|
except Exception as e:
|
|
831
|
+
logger.error(f"Save failed: {e}", exc_info=True)
|
|
678
832
|
return self._error_toast(f"Save failed: {str(e)}")
|
|
679
833
|
|
|
680
834
|
def _wrap_modal(self, modal):
|
|
@@ -684,7 +838,7 @@ class DataTableResource:
|
|
|
684
838
|
return Div(modal, id=self.feedback_id)
|
|
685
839
|
|
|
686
840
|
def _success_toast(self, message: str):
|
|
687
|
-
"""Return a success toast
|
|
841
|
+
"""✅ Return a success toast with auto-refresh trigger."""
|
|
688
842
|
toast = Toast(
|
|
689
843
|
message,
|
|
690
844
|
variant="success",
|
|
@@ -698,10 +852,16 @@ class DataTableResource:
|
|
|
698
852
|
),
|
|
699
853
|
active=True
|
|
700
854
|
)
|
|
701
|
-
|
|
855
|
+
|
|
856
|
+
# Return as HTMLResponse with HX-Trigger for auto-refresh
|
|
857
|
+
from fasthtml.common import to_xml
|
|
858
|
+
html_content = to_xml(Div(toast, id=self.feedback_id))
|
|
859
|
+
response = HTMLResponse(content=html_content)
|
|
860
|
+
response.headers['HX-Trigger'] = self.refresh_trigger
|
|
861
|
+
return response
|
|
702
862
|
|
|
703
863
|
def _error_toast(self, message: str):
|
|
704
|
-
"""Return an error toast
|
|
864
|
+
"""❌ Return an error toast (no auto-refresh on errors)."""
|
|
705
865
|
toast = Toast(
|
|
706
866
|
message,
|
|
707
867
|
variant="error",
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fh-matui
|
|
3
|
+
Version: 0.9.5
|
|
4
|
+
Summary: material-ui for fasthtml
|
|
5
|
+
Home-page: https://github.com/abhisheksreesaila/fh-matui
|
|
6
|
+
Author: abhishek sreesaila
|
|
7
|
+
Author-email: abhishek.sreesaila@gmail.com
|
|
8
|
+
License: Apache Software License 2.0
|
|
9
|
+
Keywords: nbdev jupyter notebook python
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: python-fasthtml
|
|
22
|
+
Requires-Dist: fastcore
|
|
23
|
+
Requires-Dist: markdown
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Dynamic: author
|
|
26
|
+
Dynamic: author-email
|
|
27
|
+
Dynamic: classifier
|
|
28
|
+
Dynamic: description
|
|
29
|
+
Dynamic: description-content-type
|
|
30
|
+
Dynamic: home-page
|
|
31
|
+
Dynamic: keywords
|
|
32
|
+
Dynamic: license
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
Dynamic: provides-extra
|
|
35
|
+
Dynamic: requires-dist
|
|
36
|
+
Dynamic: requires-python
|
|
37
|
+
Dynamic: summary
|
|
38
|
+
|
|
39
|
+
# fh-matui
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
43
|
+
|
|
44
|
+
## What is fh-matui?
|
|
45
|
+
|
|
46
|
+
**fh-matui** is a Python library that brings Google’s Material Design to
|
|
47
|
+
[FastHTML](https://fastht.ml/) applications. It provides a comprehensive
|
|
48
|
+
set of pre-built UI components that integrate seamlessly with FastHTML’s
|
|
49
|
+
hypermedia-driven architecture.
|
|
50
|
+
|
|
51
|
+
Built on top of [BeerCSS](https://www.beercss.com/) (a lightweight
|
|
52
|
+
Material Design 3 CSS framework), fh-matui enables you to create modern,
|
|
53
|
+
responsive web interfaces entirely in Python — no JavaScript required.
|
|
54
|
+
|
|
55
|
+
## ✨ Key Features
|
|
56
|
+
|
|
57
|
+
| Feature | Description |
|
|
58
|
+
|----|----|
|
|
59
|
+
| 🎨 **Material Design 3** | Modern, beautiful components following Google’s latest design language |
|
|
60
|
+
| ⚡ **Zero JavaScript** | Build interactive UIs entirely in Python with FastHTML |
|
|
61
|
+
| 📱 **Responsive** | Mobile-first design with automatic breakpoint handling |
|
|
62
|
+
| 🌙 **Dark Mode** | Built-in light/dark theme support with 20+ color schemes |
|
|
63
|
+
| 🧩 **Composable** | Chainable styling APIs inspired by MonsterUI |
|
|
64
|
+
| 📊 **Data Tables** | Full-featured tables with pagination, search, sorting, and CRUD |
|
|
65
|
+
| 🔧 **nbdev-powered** | Literate programming with documentation built from notebooks |
|
|
66
|
+
|
|
67
|
+
## 🏗️ Architecture
|
|
68
|
+
|
|
69
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
70
|
+
│ fh-matui │
|
|
71
|
+
├─────────────────────────────────────────────────────────────┤
|
|
72
|
+
│ Foundations │ Core styling utilities, helpers, enums │
|
|
73
|
+
│ Core │ Theme system, MatTheme color presets │
|
|
74
|
+
│ Components │ Buttons, Cards, Modals, Forms, Tables │
|
|
75
|
+
│ App Pages │ Full-page layouts, navigation patterns │
|
|
76
|
+
│ Data Tables │ DataTable, DataTableResource for CRUD │
|
|
77
|
+
│ Web Pages │ Landing pages, marketing components │
|
|
78
|
+
├─────────────────────────────────────────────────────────────┤
|
|
79
|
+
│ BeerCSS │ Material Design 3 CSS framework │
|
|
80
|
+
│ FastHTML │ Python hypermedia web framework │
|
|
81
|
+
└─────────────────────────────────────────────────────────────┘
|
|
82
|
+
|
|
83
|
+
## 🎨 Available Themes
|
|
84
|
+
|
|
85
|
+
fh-matui includes 15+ pre-configured Material Design 3 color themes:
|
|
86
|
+
|
|
87
|
+
| Theme | Preview | Theme | Preview |
|
|
88
|
+
|-----------------------|---------|-----------------------|---------|
|
|
89
|
+
| `MatTheme.red` | 🔴 | `MatTheme.pink` | 🩷 |
|
|
90
|
+
| `MatTheme.purple` | 🟣 | `MatTheme.deepPurple` | 💜 |
|
|
91
|
+
| `MatTheme.indigo` | 🔵 | `MatTheme.blue` | 💙 |
|
|
92
|
+
| `MatTheme.lightBlue` | 🩵 | `MatTheme.cyan` | 🌊 |
|
|
93
|
+
| `MatTheme.teal` | 🩶 | `MatTheme.green` | 💚 |
|
|
94
|
+
| `MatTheme.lightGreen` | 🍀 | `MatTheme.lime` | 💛 |
|
|
95
|
+
| `MatTheme.yellow` | 🌟 | `MatTheme.amber` | 🧡 |
|
|
96
|
+
| `MatTheme.orange` | 🟠 | `MatTheme.deepOrange` | 🔶 |
|
|
97
|
+
|
|
98
|
+
**Usage:**
|
|
99
|
+
|
|
100
|
+
``` python
|
|
101
|
+
# Choose your theme
|
|
102
|
+
app, rt = fast_app(hdrs=[MatTheme.deepPurple.headers()])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 🚀 Quick Start
|
|
106
|
+
|
|
107
|
+
Here’s a minimal example to get you started:
|
|
108
|
+
|
|
109
|
+
``` python
|
|
110
|
+
from fasthtml.common import *
|
|
111
|
+
from fh_matui.core import MatTheme
|
|
112
|
+
from fh_matui.components import Button, Card, FormField
|
|
113
|
+
|
|
114
|
+
# Create a themed FastHTML app
|
|
115
|
+
app, rt = fast_app(hdrs=[MatTheme.indigo.headers()])
|
|
116
|
+
|
|
117
|
+
@rt('/')
|
|
118
|
+
def home():
|
|
119
|
+
return Div(
|
|
120
|
+
Card(
|
|
121
|
+
H3("Welcome to fh-matui!"),
|
|
122
|
+
P("Build beautiful Material Design apps with Python."),
|
|
123
|
+
FormField("email", label="Email", type="email"),
|
|
124
|
+
Button("Get Started", cls="primary"),
|
|
125
|
+
),
|
|
126
|
+
cls="padding"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
serve()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 📦 Installation
|
|
133
|
+
|
|
134
|
+
``` bash
|
|
135
|
+
pip install fh-matui
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Dependencies
|
|
139
|
+
|
|
140
|
+
fh-matui automatically includes: - **python-fasthtml** - The core
|
|
141
|
+
FastHTML framework - **BeerCSS** - Loaded via CDN for Material Design 3
|
|
142
|
+
styling
|
|
143
|
+
|
|
144
|
+
### What This Code Does
|
|
145
|
+
|
|
146
|
+
1. **`MatTheme.indigo.headers()`** - Loads BeerCSS with the indigo
|
|
147
|
+
color scheme
|
|
148
|
+
2. **[`Card`](https://abhisheksreesaila.github.io/fh-matui/components.html#card)** -
|
|
149
|
+
Creates a Material Design card component with elevation
|
|
150
|
+
3. **[`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield)** -
|
|
151
|
+
Generates a styled input with floating label
|
|
152
|
+
4. **`Button`** - Renders a Material Design button with ripple effects
|
|
153
|
+
|
|
154
|
+
## 📚 Module Reference
|
|
155
|
+
|
|
156
|
+
| Module | Description | Key Components |
|
|
157
|
+
|----|----|----|
|
|
158
|
+
| [Foundations](foundations.html) | Base utilities and helper functions | `BeerHeaders`, `display`, styling helpers |
|
|
159
|
+
| [Core](core.html) | Theme system and styling | `MatTheme`, color presets, theme configuration |
|
|
160
|
+
| [Components](components.html) | UI component library | `Button`, [`Card`](https://abhisheksreesaila.github.io/fh-matui/components.html#card), [`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield), [`FormModal`](https://abhisheksreesaila.github.io/fh-matui/components.html#formmodal), [`Grid`](https://abhisheksreesaila.github.io/fh-matui/components.html#grid) |
|
|
161
|
+
| [App Pages](app_pages.html) | Application layouts | Navigation, sidebars, full-page layouts |
|
|
162
|
+
| [Data Tables](05_table.html) | Data management components | [`DataTable`](https://abhisheksreesaila.github.io/fh-matui/datatable.html#datatable), [`DataTableResource`](https://abhisheksreesaila.github.io/fh-matui/datatable.html#datatableresource), CRUD operations |
|
|
163
|
+
| [Web Pages](web_pages.html) | Marketing/landing pages | Hero sections, feature grids, testimonials |
|
|
164
|
+
|
|
165
|
+
## 🛠️ Development
|
|
166
|
+
|
|
167
|
+
### Install in Development Mode
|
|
168
|
+
|
|
169
|
+
``` bash
|
|
170
|
+
# Clone the repository
|
|
171
|
+
git clone https://github.com/user/fh-matui.git
|
|
172
|
+
cd fh-matui
|
|
173
|
+
|
|
174
|
+
# Install in editable mode
|
|
175
|
+
pip install -e .
|
|
176
|
+
|
|
177
|
+
# Make changes under nbs/ directory, then compile
|
|
178
|
+
nbdev_prepare
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 🤝 Why fh-matui?
|
|
182
|
+
|
|
183
|
+
| Challenge | fh-matui Solution |
|
|
184
|
+
|----|----|
|
|
185
|
+
| **CSS complexity** | Pre-built Material Design 3 components via BeerCSS |
|
|
186
|
+
| **JavaScript fatigue** | FastHTML handles interactivity declaratively |
|
|
187
|
+
| **Component consistency** | Unified API across all components |
|
|
188
|
+
| **Dark mode support** | Built-in with automatic system preference detection |
|
|
189
|
+
| **Responsive design** | Mobile-first grid system and responsive utilities |
|
|
190
|
+
| **Form handling** | [`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield), [`FormGrid`](https://abhisheksreesaila.github.io/fh-matui/components.html#formgrid), [`FormModal`](https://abhisheksreesaila.github.io/fh-matui/components.html#formmodal) for rapid form building |
|
|
191
|
+
| **Data management** | [`DataTable`](https://abhisheksreesaila.github.io/fh-matui/datatable.html#datatable) and [`DataTableResource`](https://abhisheksreesaila.github.io/fh-matui/datatable.html#datatableresource) for CRUD operations |
|
|
192
|
+
|
|
193
|
+
## 🤖 For LLM Users
|
|
194
|
+
|
|
195
|
+
fh-matui includes **comprehensive documentation bundles** for Large
|
|
196
|
+
Language Models, enabling AI assistants (like Claude, ChatGPT, or GitHub
|
|
197
|
+
Copilot) to help you build FastHTML apps with complete knowledge of the
|
|
198
|
+
component APIs.
|
|
199
|
+
|
|
200
|
+
### 📥 Download Context File
|
|
201
|
+
|
|
202
|
+
**[📄
|
|
203
|
+
llms-ctx.txt](https://raw.githubusercontent.com/abhisheksreesaila/fh-matui/main/llms-ctx.txt)**
|
|
204
|
+
— Complete API documentation in LLM-optimized format
|
|
205
|
+
|
|
206
|
+
### 💡 How to Use
|
|
207
|
+
|
|
208
|
+
1. **Download the context file** from the link above
|
|
209
|
+
2. **Attach it to your LLM conversation** (drag & drop or paste
|
|
210
|
+
contents)
|
|
211
|
+
3. **Ask for implementation** using natural language
|
|
212
|
+
|
|
213
|
+
**Example Prompt:**
|
|
214
|
+
|
|
215
|
+
I'm using fh-matui (context attached). Create a dashboard with:
|
|
216
|
+
- A sidebar navigation with 5 menu items
|
|
217
|
+
- A DataTable showing products with pagination
|
|
218
|
+
- A modal form to add/edit products
|
|
219
|
+
- Use the deep purple theme
|
|
220
|
+
|
|
221
|
+
The LLM will generate production-ready FastHTML code using the exact
|
|
222
|
+
component APIs from the documentation.
|
|
223
|
+
|
|
224
|
+
### 🔄 Staying Up to Date
|
|
225
|
+
|
|
226
|
+
The `llms-ctx.txt` file is automatically regenerated with each release
|
|
227
|
+
to ensure it stays synchronized with the latest API changes. Always
|
|
228
|
+
download the version matching your installed package version for the
|
|
229
|
+
most accurate results.
|
|
230
|
+
|
|
231
|
+
> **📌 Note:** The context file is generated from the same literate
|
|
232
|
+
> programming notebooks that build the library itself, ensuring 100%
|
|
233
|
+
> accuracy with the actual implementation.
|
|
234
|
+
|
|
235
|
+
## 📄 License
|
|
236
|
+
|
|
237
|
+
This project is licensed under the Apache 2.0 License - see the
|
|
238
|
+
[LICENSE](https://github.com/user/fh-matui/blob/main/LICENSE) file for
|
|
239
|
+
details.
|
|
240
|
+
|
|
241
|
+
------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
**Built with ❤️ using FastHTML and nbdev**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
fh_matui/__init__.py,sha256=iPcoATf7BiWjSu-KocRdM5zFTR4wx4ktCHlGGpvdc1M,23
|
|
2
|
+
fh_matui/_modidx.py,sha256=RTGErPb_KBkO8z8KjcFH3rczKntoTLkHKAK-4a0oax0,23511
|
|
3
|
+
fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
|
|
4
|
+
fh_matui/components.py,sha256=KjdTHzWRXpVWBEIGskW1HfhjPpzRYzi6UA_yRjZyMWM,48254
|
|
5
|
+
fh_matui/core.py,sha256=xtVBN8CtC50ZJ4Iu7o-mUhaA87tWdnz8gBfKRk63Zhs,10680
|
|
6
|
+
fh_matui/datatable.py,sha256=DsK73GhB18s7KRk5n_2QU54AuRtfFGTckIVOQ_a8n1w,31632
|
|
7
|
+
fh_matui/foundations.py,sha256=b7PnObJpKN8ZAU9NzCm9xpfnHzFjjAROU7E2YvA_tj4,1820
|
|
8
|
+
fh_matui/web_pages.py,sha256=4mF-jpfVcZTVepfQ-aMGgIUp-nBp0YCkvcdsWhUYeaA,34879
|
|
9
|
+
fh_matui-0.9.5.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
|
|
10
|
+
fh_matui-0.9.5.dist-info/METADATA,sha256=dUxJJcfK4MX0RbEdlchOVr2KbHPmn03NVm-nUvlgdcE,10490
|
|
11
|
+
fh_matui-0.9.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
fh_matui-0.9.5.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
|
|
13
|
+
fh_matui-0.9.5.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
|
|
14
|
+
fh_matui-0.9.5.dist-info/RECORD,,
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fh-matui
|
|
3
|
-
Version: 0.9.3
|
|
4
|
-
Summary: material-ui for fasthtml
|
|
5
|
-
Home-page: https://github.com/abhisheksreesaila/fh-matui
|
|
6
|
-
Author: abhishek sreesaila
|
|
7
|
-
Author-email: abhishek.sreesaila@gmail.com
|
|
8
|
-
License: Apache Software License 2.0
|
|
9
|
-
Keywords: nbdev jupyter notebook python
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Natural Language :: English
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
|
-
Requires-Python: >=3.9
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
License-File: LICENSE
|
|
21
|
-
Requires-Dist: python-fasthtml
|
|
22
|
-
Requires-Dist: fastcore
|
|
23
|
-
Requires-Dist: markdown
|
|
24
|
-
Provides-Extra: dev
|
|
25
|
-
Dynamic: author
|
|
26
|
-
Dynamic: author-email
|
|
27
|
-
Dynamic: classifier
|
|
28
|
-
Dynamic: description
|
|
29
|
-
Dynamic: description-content-type
|
|
30
|
-
Dynamic: home-page
|
|
31
|
-
Dynamic: keywords
|
|
32
|
-
Dynamic: license
|
|
33
|
-
Dynamic: license-file
|
|
34
|
-
Dynamic: provides-extra
|
|
35
|
-
Dynamic: requires-dist
|
|
36
|
-
Dynamic: requires-python
|
|
37
|
-
Dynamic: summary
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## What is fh-matui?
|
|
41
|
-
|
|
42
|
-
**fh-matui** is a Python library that brings Google's Material Design to [FastHTML](https://fastht.ml/) applications. It provides a comprehensive set of pre-built UI components that integrate seamlessly with FastHTML's hypermedia-driven architecture.
|
|
43
|
-
|
|
44
|
-
Built on top of [BeerCSS](https://www.beercss.com/) (a lightweight Material Design 3 CSS framework), fh-matui enables you to create modern, responsive web interfaces entirely in Python — no JavaScript required.
|
|
45
|
-
|
|
46
|
-
## ✨ Key Features
|
|
47
|
-
|
|
48
|
-
| Feature | Description |
|
|
49
|
-
|---------|-------------|
|
|
50
|
-
| 🎨 **Material Design 3** | Modern, beautiful components following Google's latest design language |
|
|
51
|
-
| ⚡ **Zero JavaScript** | Build interactive UIs entirely in Python with FastHTML |
|
|
52
|
-
| 📱 **Responsive** | Mobile-first design with automatic breakpoint handling |
|
|
53
|
-
| 🌙 **Dark Mode** | Built-in light/dark theme support with 20+ color schemes |
|
|
54
|
-
| 🧩 **Composable** | Chainable styling APIs inspired by MonsterUI |
|
|
55
|
-
| 📊 **Data Tables** | Full-featured tables with pagination, search, sorting, and CRUD |
|
|
56
|
-
| 🔧 **nbdev-powered** | Literate programming with documentation built from notebooks |
|
|
57
|
-
|
|
58
|
-
## 🏗️ Architecture
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
62
|
-
│ fh-matui │
|
|
63
|
-
├─────────────────────────────────────────────────────────────┤
|
|
64
|
-
│ Foundations │ Core styling utilities, helpers, enums │
|
|
65
|
-
│ Core │ Theme system, MatTheme color presets │
|
|
66
|
-
│ Components │ Buttons, Cards, Modals, Forms, Tables │
|
|
67
|
-
│ App Pages │ Full-page layouts, navigation patterns │
|
|
68
|
-
│ Data Tables │ DataTable, DataTableResource for CRUD │
|
|
69
|
-
│ Web Pages │ Landing pages, marketing components │
|
|
70
|
-
├─────────────────────────────────────────────────────────────┤
|
|
71
|
-
│ BeerCSS │ Material Design 3 CSS framework │
|
|
72
|
-
│ FastHTML │ Python hypermedia web framework │
|
|
73
|
-
└─────────────────────────────────────────────────────────────┘
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## 🎨 Available Themes
|
|
77
|
-
|
|
78
|
-
fh-matui includes 15+ pre-configured Material Design 3 color themes:
|
|
79
|
-
|
|
80
|
-
| Theme | Preview | Theme | Preview |
|
|
81
|
-
|-------|---------|-------|---------|
|
|
82
|
-
| `MatTheme.red` | 🔴 | `MatTheme.pink` | 🩷 |
|
|
83
|
-
| `MatTheme.purple` | 🟣 | `MatTheme.deepPurple` | 💜 |
|
|
84
|
-
| `MatTheme.indigo` | 🔵 | `MatTheme.blue` | 💙 |
|
|
85
|
-
| `MatTheme.lightBlue` | 🩵 | `MatTheme.cyan` | 🌊 |
|
|
86
|
-
| `MatTheme.teal` | 🩶 | `MatTheme.green` | 💚 |
|
|
87
|
-
| `MatTheme.lightGreen` | 🍀 | `MatTheme.lime` | 💛 |
|
|
88
|
-
| `MatTheme.yellow` | 🌟 | `MatTheme.amber` | 🧡 |
|
|
89
|
-
| `MatTheme.orange` | 🟠 | `MatTheme.deepOrange` | 🔶 |
|
|
90
|
-
|
|
91
|
-
**Usage:**
|
|
92
|
-
```python
|
|
93
|
-
# Choose your theme
|
|
94
|
-
app, rt = fast_app(hdrs=[MatTheme.deepPurple.headers()])
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## 🚀 Quick Start
|
|
98
|
-
|
|
99
|
-
Here's a minimal example to get you started:
|
|
100
|
-
|
|
101
|
-
```python
|
|
102
|
-
from fasthtml.common import *
|
|
103
|
-
from fh_matui.core import MatTheme
|
|
104
|
-
from fh_matui.components import Button, Card, FormField
|
|
105
|
-
|
|
106
|
-
# Create a themed FastHTML app
|
|
107
|
-
app, rt = fast_app(hdrs=[MatTheme.indigo.headers()])
|
|
108
|
-
|
|
109
|
-
@rt('/')
|
|
110
|
-
def home():
|
|
111
|
-
return Div(
|
|
112
|
-
Card(
|
|
113
|
-
H3("Welcome to fh-matui!"),
|
|
114
|
-
P("Build beautiful Material Design apps with Python."),
|
|
115
|
-
FormField("email", label="Email", type="email"),
|
|
116
|
-
Button("Get Started", cls="primary"),
|
|
117
|
-
),
|
|
118
|
-
cls="padding"
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
serve()
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## 📦 Installation
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
pip install fh-matui
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Dependencies
|
|
131
|
-
|
|
132
|
-
fh-matui automatically includes:
|
|
133
|
-
- **python-fasthtml** - The core FastHTML framework
|
|
134
|
-
- **BeerCSS** - Loaded via CDN for Material Design 3 styling
|
|
135
|
-
|
|
136
|
-
### What This Code Does
|
|
137
|
-
|
|
138
|
-
1. **`MatTheme.indigo.headers()`** - Loads BeerCSS with the indigo color scheme
|
|
139
|
-
2. **[`Card`](https://abhisheksreesaila.github.io/fh-matui/components.html#card)** - Creates a Material Design card component with elevation
|
|
140
|
-
3. **[`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield)** - Generates a styled input with floating label
|
|
141
|
-
4. **`Button`** - Renders a Material Design button with ripple effects
|
|
142
|
-
|
|
143
|
-
## 📚 Module Reference
|
|
144
|
-
|
|
145
|
-
| Module | Description | Key Components |
|
|
146
|
-
|--------|-------------|----------------|
|
|
147
|
-
| [Foundations](https://abhisheksreesaila.github.io/fh-matui/foundations.html) | Base utilities and helper functions | `BeerHeaders`, `display`, styling helpers |
|
|
148
|
-
| [Core](https://abhisheksreesaila.github.io/fh-matui/core.html) | Theme system and styling | `MatTheme`, color presets, theme configuration |
|
|
149
|
-
| [Components](https://abhisheksreesaila.github.io/fh-matui/components.html) | UI component library | `Button`, `Card`, `FormField`, `FormModal`, `Grid` |
|
|
150
|
-
| [App Pages](https://abhisheksreesaila.github.io/fh-matui/app_pages.html) | Application layouts | Navigation, sidebars, full-page layouts |
|
|
151
|
-
| [Data Tables](https://abhisheksreesaila.github.io/fh-matui/datatable.html) | Data management components | `DataTable`, `DataTableResource`, CRUD operations |
|
|
152
|
-
| [Web Pages](https://abhisheksreesaila.github.io/fh-matui/web_pages.html) | Marketing/landing pages | Hero sections, feature grids, testimonials |
|
|
153
|
-
|
|
154
|
-
## 🛠️ Development
|
|
155
|
-
|
|
156
|
-
### Install in Development Mode
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
# Clone the repository
|
|
160
|
-
git clone https://github.com/user/fh-matui.git
|
|
161
|
-
cd fh-matui
|
|
162
|
-
|
|
163
|
-
# Install in editable mode
|
|
164
|
-
pip install -e .
|
|
165
|
-
|
|
166
|
-
# Make changes under nbs/ directory, then compile
|
|
167
|
-
nbdev_prepare
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
## 🤝 Why fh-matui?
|
|
171
|
-
|
|
172
|
-
| Challenge | fh-matui Solution |
|
|
173
|
-
|-----------|-------------------|
|
|
174
|
-
| **CSS complexity** | Pre-built Material Design 3 components via BeerCSS |
|
|
175
|
-
| **JavaScript fatigue** | FastHTML handles interactivity declaratively |
|
|
176
|
-
| **Component consistency** | Unified API across all components |
|
|
177
|
-
| **Dark mode support** | Built-in with automatic system preference detection |
|
|
178
|
-
| **Responsive design** | Mobile-first grid system and responsive utilities |
|
|
179
|
-
| **Form handling** | [`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield), [`FormGrid`](https://abhisheksreesaila.github.io/fh-matui/components.html#formgrid), [`FormModal`](https://abhisheksreesaila.github.io/fh-matui/components.html#formmodal) for rapid form building |
|
|
180
|
-
| **Data management** | [`DataTable`](https://abhisheksreesaila.github.io/fh-matui/table.html#datatable) and [`DataTableResource`](https://abhisheksreesaila.github.io/fh-matui/table.html#datatableresource) for CRUD operations |
|
|
181
|
-
|
|
182
|
-
## 📄 License
|
|
183
|
-
|
|
184
|
-
This project is licensed under the Apache 2.0 License - see the [LICENSE](https://github.com/user/fh-matui/blob/main/LICENSE) file for details.
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
**Built with ❤️ using FastHTML and nbdev**
|
fh_matui-0.9.3.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
fh_matui/__init__.py,sha256=5uBZ3sUaocnyIWuJeW9M94Z77vA1kKmwKxiblrnbKlc,23
|
|
2
|
-
fh_matui/_modidx.py,sha256=hLZ_V7aRsAvavJ82m02XyvImfzYB5_DnOce8eKRD3xU,22865
|
|
3
|
-
fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
|
|
4
|
-
fh_matui/components.py,sha256=KjdTHzWRXpVWBEIGskW1HfhjPpzRYzi6UA_yRjZyMWM,48254
|
|
5
|
-
fh_matui/core.py,sha256=xtVBN8CtC50ZJ4Iu7o-mUhaA87tWdnz8gBfKRk63Zhs,10680
|
|
6
|
-
fh_matui/datatable.py,sha256=MAEibyjRwYlBLMD9dIIocufuWn84jTvrgRt1XKFAN9U,25506
|
|
7
|
-
fh_matui/foundations.py,sha256=b7PnObJpKN8ZAU9NzCm9xpfnHzFjjAROU7E2YvA_tj4,1820
|
|
8
|
-
fh_matui/web_pages.py,sha256=4mF-jpfVcZTVepfQ-aMGgIUp-nBp0YCkvcdsWhUYeaA,34879
|
|
9
|
-
fh_matui-0.9.3.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
|
|
10
|
-
fh_matui-0.9.3.dist-info/METADATA,sha256=KX2VQz5wOPRHaSwtc86l3-p3TTjn-08pjuS7kAHbta8,8431
|
|
11
|
-
fh_matui-0.9.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
fh_matui-0.9.3.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
|
|
13
|
-
fh_matui-0.9.3.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
|
|
14
|
-
fh_matui-0.9.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|