fh-matui 0.9.4__py3-none-any.whl → 0.9.6__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 +9 -1
- fh_matui/datatable.py +247 -95
- fh_matui-0.9.6.dist-info/METADATA +243 -0
- fh_matui-0.9.6.dist-info/RECORD +14 -0
- fh_matui-0.9.4.dist-info/METADATA +0 -187
- fh_matui-0.9.4.dist-info/RECORD +0 -14
- {fh_matui-0.9.4.dist-info → fh_matui-0.9.6.dist-info}/WHEEL +0 -0
- {fh_matui-0.9.4.dist-info → fh_matui-0.9.6.dist-info}/entry_points.txt +0 -0
- {fh_matui-0.9.4.dist-info → fh_matui-0.9.6.dist-info}/licenses/LICENSE +0 -0
- {fh_matui-0.9.4.dist-info → fh_matui-0.9.6.dist-info}/top_level.txt +0 -0
fh_matui/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.9.
|
|
1
|
+
__version__ = "0.9.5"
|
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',
|
|
@@ -152,7 +157,10 @@ d = { 'settings': { 'branch': 'master',
|
|
|
152
157
|
'fh_matui/datatable.py'),
|
|
153
158
|
'fh_matui.datatable.DataTableResource._wrap_modal': ( 'datatable.html#datatableresource._wrap_modal',
|
|
154
159
|
'fh_matui/datatable.py'),
|
|
160
|
+
'fh_matui.datatable.DataTableResource._wrap_response': ( 'datatable.html#datatableresource._wrap_response',
|
|
161
|
+
'fh_matui/datatable.py'),
|
|
155
162
|
'fh_matui.datatable._action_menu': ('datatable.html#_action_menu', 'fh_matui/datatable.py'),
|
|
163
|
+
'fh_matui.datatable._is_htmx_request': ('datatable.html#_is_htmx_request', 'fh_matui/datatable.py'),
|
|
156
164
|
'fh_matui.datatable._page_size_select': ('datatable.html#_page_size_select', 'fh_matui/datatable.py'),
|
|
157
165
|
'fh_matui.datatable._safe_int': ('datatable.html#_safe_int', 'fh_matui/datatable.py'),
|
|
158
166
|
'fh_matui.datatable._to_dict': ('datatable.html#_to_dict', 'fh_matui/datatable.py'),
|
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
|
|
@@ -308,19 +403,41 @@ def _to_dict(obj: Any) -> dict:
|
|
|
308
403
|
return dict(obj)
|
|
309
404
|
|
|
310
405
|
|
|
406
|
+
def _is_htmx_request(req) -> bool:
|
|
407
|
+
"""Check if request is an HTMX partial request."""
|
|
408
|
+
headers = getattr(req, 'headers', {})
|
|
409
|
+
return headers.get('HX-Request') == 'true'
|
|
410
|
+
|
|
411
|
+
|
|
311
412
|
class DataTableResource:
|
|
312
|
-
"
|
|
413
|
+
"""
|
|
414
|
+
🔧 High-level resource that auto-registers all routes for a data table.
|
|
415
|
+
|
|
416
|
+
**Features:**
|
|
417
|
+
- All callbacks receive `request` for multi-tenant support
|
|
418
|
+
- Custom CRUD hooks with `CrudContext` for rich business logic
|
|
419
|
+
- Async/sync hook support for external API integration
|
|
420
|
+
- Auto-refresh table via HX-Trigger after mutations
|
|
421
|
+
- Layout wrapper for full-page (non-HTMX) responses
|
|
422
|
+
|
|
423
|
+
**Auto-registers 3 routes:**
|
|
424
|
+
- `GET {base_route}` → DataTable list view
|
|
425
|
+
- `GET {base_route}/action` → FormModal for create/edit/view/delete
|
|
426
|
+
- `POST {base_route}/save` → Save handler with hooks
|
|
427
|
+
"""
|
|
313
428
|
|
|
314
429
|
def __init__(
|
|
315
430
|
self,
|
|
316
431
|
app,
|
|
317
432
|
base_route: str,
|
|
318
433
|
columns: list[dict],
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
434
|
+
# Data callbacks - ALL receive request as first param
|
|
435
|
+
get_all: Callable[[Any], list], # (req) -> list
|
|
436
|
+
get_by_id: Callable[[Any, Any], Any], # (req, id) -> record
|
|
437
|
+
create: Callable[[Any, dict], Any] = None, # (req, data) -> record
|
|
438
|
+
update: Callable[[Any, Any, dict], Any] = None, # (req, id, data) -> record
|
|
439
|
+
delete: Callable[[Any, Any], bool] = None, # (req, id) -> bool
|
|
440
|
+
# Display options
|
|
324
441
|
title: str = "Records",
|
|
325
442
|
row_id_field: str = "id",
|
|
326
443
|
crud_ops: dict = None,
|
|
@@ -328,15 +445,12 @@ class DataTableResource:
|
|
|
328
445
|
search_placeholder: str = "Search...",
|
|
329
446
|
create_label: str = "New Record",
|
|
330
447
|
empty_message: str = "No records found.",
|
|
331
|
-
#
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
on_after_delete: Callable[[Any], None] = None,
|
|
338
|
-
# Multi-tenant
|
|
339
|
-
user_filter: Callable = None,
|
|
448
|
+
# CRUD Hooks (with CrudContext)
|
|
449
|
+
on_create: Callable[[CrudContext], dict] = None,
|
|
450
|
+
on_update: Callable[[CrudContext], dict] = None,
|
|
451
|
+
on_delete: Callable[[CrudContext], None] = None,
|
|
452
|
+
# Layout wrapper for full-page responses
|
|
453
|
+
layout_wrapper: Callable[[Any, Any], Any] = None, # (content, req) -> wrapped
|
|
340
454
|
# Custom generators
|
|
341
455
|
id_generator: Callable[[], Any] = None,
|
|
342
456
|
timestamp_fields: dict = None
|
|
@@ -359,23 +473,20 @@ class DataTableResource:
|
|
|
359
473
|
# Determine CRUD ops from provided functions
|
|
360
474
|
if crud_ops is None:
|
|
361
475
|
self.crud_ops = {
|
|
362
|
-
"create": create is not None,
|
|
363
|
-
"update": update is not None,
|
|
364
|
-
"delete": delete is not None
|
|
476
|
+
"create": create is not None or on_create is not None,
|
|
477
|
+
"update": update is not None or on_update is not None,
|
|
478
|
+
"delete": delete is not None or on_delete is not None
|
|
365
479
|
}
|
|
366
480
|
else:
|
|
367
481
|
self.crud_ops = crud_ops
|
|
368
482
|
|
|
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
|
|
483
|
+
# CRUD hooks
|
|
484
|
+
self.on_create_hook = on_create
|
|
485
|
+
self.on_update_hook = on_update
|
|
486
|
+
self.on_delete_hook = on_delete
|
|
376
487
|
|
|
377
|
-
#
|
|
378
|
-
self.
|
|
488
|
+
# Layout wrapper
|
|
489
|
+
self.layout_wrapper = layout_wrapper
|
|
379
490
|
|
|
380
491
|
# Generators
|
|
381
492
|
self.id_generator = id_generator
|
|
@@ -385,46 +496,74 @@ 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."""
|
|
514
|
+
user = None
|
|
515
|
+
db = None
|
|
516
|
+
tbl = None
|
|
517
|
+
|
|
518
|
+
# Extract user from request.state if available
|
|
519
|
+
try:
|
|
520
|
+
if hasattr(req, 'state') and hasattr(req.state, 'user'):
|
|
521
|
+
user = req.state.user
|
|
522
|
+
except AttributeError:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
# Extract db from request.state if available
|
|
526
|
+
try:
|
|
527
|
+
if hasattr(req, 'state') and hasattr(req.state, 'tenant_db'):
|
|
528
|
+
db = req.state.tenant_db
|
|
529
|
+
except AttributeError:
|
|
530
|
+
pass
|
|
531
|
+
|
|
532
|
+
return CrudContext(
|
|
533
|
+
request=req,
|
|
534
|
+
user=user,
|
|
535
|
+
db=db,
|
|
536
|
+
tbl=tbl,
|
|
537
|
+
record=record or {},
|
|
538
|
+
record_id=record_id
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def _wrap_response(self, content, req):
|
|
542
|
+
"""Wrap content with layout_wrapper if not an HTMX request."""
|
|
543
|
+
if self.layout_wrapper and not _is_htmx_request(req):
|
|
544
|
+
return self.layout_wrapper(content, req)
|
|
545
|
+
return content
|
|
546
|
+
|
|
392
547
|
def _register_routes(self):
|
|
393
548
|
"""Register all data table routes with the app."""
|
|
394
549
|
rt = self.app.route
|
|
395
550
|
|
|
396
|
-
# Main table route
|
|
397
551
|
@rt(self.base_route)
|
|
398
552
|
def _table_handler(req):
|
|
399
553
|
return self._handle_table(req)
|
|
400
554
|
|
|
401
|
-
# Action route (view/edit/create/delete)
|
|
402
555
|
@rt(f"{self.base_route}/action")
|
|
403
556
|
def _action_handler(req):
|
|
404
557
|
return self._handle_action(req)
|
|
405
558
|
|
|
406
|
-
# Save route (form submission)
|
|
407
559
|
@rt(f"{self.base_route}/save")
|
|
408
560
|
async def _save_handler(req):
|
|
409
561
|
return await self._handle_save(req)
|
|
410
562
|
|
|
411
563
|
def _get_filtered_data(self, req) -> list:
|
|
412
|
-
"""Get all data
|
|
413
|
-
all_data = self.get_all()
|
|
414
|
-
|
|
415
|
-
# Convert to list of dicts
|
|
416
|
-
data = [_to_dict(item) for item in all_data]
|
|
417
|
-
|
|
418
|
-
# Apply user filter if provided
|
|
419
|
-
if self.user_filter and callable(self.user_filter):
|
|
420
|
-
filter_criteria = self.user_filter(req)
|
|
421
|
-
if filter_criteria:
|
|
422
|
-
data = [
|
|
423
|
-
row for row in data
|
|
424
|
-
if all(row.get(k) == v for k, v in filter_criteria.items())
|
|
425
|
-
]
|
|
426
|
-
|
|
427
|
-
return data
|
|
564
|
+
"""Get all data from user's callback."""
|
|
565
|
+
all_data = self.get_all(req)
|
|
566
|
+
return [_to_dict(item) for item in all_data]
|
|
428
567
|
|
|
429
568
|
def _filter_by_search(self, data: list, search: str) -> list:
|
|
430
569
|
"""Filter data by search term across searchable columns."""
|
|
@@ -437,7 +576,6 @@ class DataTableResource:
|
|
|
437
576
|
if col.get("searchable", False)
|
|
438
577
|
]
|
|
439
578
|
|
|
440
|
-
# If no columns marked searchable, search all
|
|
441
579
|
if not searchable_keys:
|
|
442
580
|
searchable_keys = [col["key"] for col in self.columns]
|
|
443
581
|
|
|
@@ -451,26 +589,19 @@ class DataTableResource:
|
|
|
451
589
|
total = len(data)
|
|
452
590
|
total_pages = max(1, ceil(total / page_size))
|
|
453
591
|
page = min(max(1, page), total_pages)
|
|
454
|
-
|
|
455
592
|
start = (page - 1) * page_size
|
|
456
593
|
end = start + page_size
|
|
457
|
-
|
|
458
594
|
return data[start:end], total, page
|
|
459
595
|
|
|
460
596
|
def _handle_table(self, req):
|
|
461
597
|
"""Handle main table route."""
|
|
462
|
-
# Extract pagination state
|
|
463
598
|
state = table_state_from_request(req, page_sizes=self.page_sizes)
|
|
464
599
|
search, page, page_size = state["search"], state["page"], state["page_size"]
|
|
465
600
|
|
|
466
|
-
# Get and filter data
|
|
467
601
|
data = self._get_filtered_data(req)
|
|
468
602
|
filtered = self._filter_by_search(data, search)
|
|
469
|
-
|
|
470
|
-
# Paginate
|
|
471
603
|
page_data, total, page = self._paginate(filtered, page, page_size)
|
|
472
604
|
|
|
473
|
-
# Build table
|
|
474
605
|
table = DataTable(
|
|
475
606
|
data=page_data,
|
|
476
607
|
total=total,
|
|
@@ -489,31 +620,43 @@ class DataTableResource:
|
|
|
489
620
|
empty_message=self.empty_message
|
|
490
621
|
)
|
|
491
622
|
|
|
492
|
-
|
|
623
|
+
# Wrap table in auto-refresh container
|
|
624
|
+
params = urlencode({"search": search, "page": page, "page_size": page_size})
|
|
625
|
+
table_container = Div(
|
|
626
|
+
table,
|
|
627
|
+
id=self.container_id,
|
|
628
|
+
hx_trigger=f"{self.refresh_trigger} from:body",
|
|
629
|
+
hx_get=f"{self.base_route}?{params}",
|
|
630
|
+
hx_target=f"#{self.container_id}",
|
|
631
|
+
hx_swap="outerHTML"
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
feedback = Div(id=self.feedback_id)
|
|
635
|
+
content = Div(feedback, table_container)
|
|
636
|
+
|
|
637
|
+
# Wrap with layout if full-page request
|
|
638
|
+
return self._wrap_response(content, req)
|
|
493
639
|
|
|
494
640
|
def _handle_action(self, req):
|
|
495
641
|
"""Handle action route (view/edit/create/delete)."""
|
|
496
642
|
params = getattr(req, "query_params", {})
|
|
497
643
|
getter = params.get if hasattr(params, "get") else (lambda k, d=None: params[k] if k in params else d)
|
|
498
644
|
|
|
499
|
-
# Handle dismiss
|
|
500
645
|
if getter("dismiss") is not None:
|
|
501
646
|
return Div(id=self.feedback_id)
|
|
502
647
|
|
|
503
648
|
record_id = getter("id")
|
|
504
|
-
# Try to convert to int if numeric
|
|
505
649
|
if record_id:
|
|
506
650
|
try:
|
|
507
651
|
record_id = int(record_id)
|
|
508
652
|
except (TypeError, ValueError):
|
|
509
|
-
pass
|
|
653
|
+
pass
|
|
510
654
|
|
|
511
655
|
action = (getter("action", "view") or "view").lower()
|
|
512
656
|
search = getter("search", "") or ""
|
|
513
657
|
page = _safe_int(getter("page", 1), 1)
|
|
514
658
|
page_size = _safe_int(getter("page_size", 10), 10)
|
|
515
659
|
|
|
516
|
-
# URLs
|
|
517
660
|
return_params = urlencode({"search": search, "page": page, "page_size": page_size})
|
|
518
661
|
cancel_url = f"{self.base_route}/action?dismiss=1"
|
|
519
662
|
save_url = f"{self.base_route}/save?{return_params}"
|
|
@@ -539,7 +682,7 @@ class DataTableResource:
|
|
|
539
682
|
# Get record for view/edit/delete
|
|
540
683
|
record = None
|
|
541
684
|
if record_id:
|
|
542
|
-
raw_record = self.get_by_id(record_id)
|
|
685
|
+
raw_record = self.get_by_id(req, record_id)
|
|
543
686
|
record = _to_dict(raw_record) if raw_record else None
|
|
544
687
|
|
|
545
688
|
if not record:
|
|
@@ -581,21 +724,23 @@ class DataTableResource:
|
|
|
581
724
|
if not self.crud_ops.get("delete"):
|
|
582
725
|
return self._error_toast("Delete operation not enabled.")
|
|
583
726
|
|
|
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
727
|
try:
|
|
591
|
-
self.
|
|
728
|
+
ctx = self._build_context(req, record=record, record_id=record_id)
|
|
592
729
|
|
|
593
|
-
#
|
|
594
|
-
if self.
|
|
595
|
-
self.
|
|
730
|
+
# Use on_delete hook if provided
|
|
731
|
+
if self.on_delete_hook:
|
|
732
|
+
if asyncio.iscoroutinefunction(self.on_delete_hook):
|
|
733
|
+
loop = asyncio.get_event_loop()
|
|
734
|
+
loop.run_until_complete(self.on_delete_hook(ctx))
|
|
735
|
+
else:
|
|
736
|
+
self.on_delete_hook(ctx)
|
|
737
|
+
else:
|
|
738
|
+
# Default: use delete_fn
|
|
739
|
+
self.delete_fn(req, record_id)
|
|
596
740
|
|
|
597
|
-
return self._success_toast(
|
|
741
|
+
return self._success_toast("Record deleted successfully.")
|
|
598
742
|
except Exception as e:
|
|
743
|
+
logger.error(f"Delete failed: {e}", exc_info=True)
|
|
599
744
|
return self._error_toast(f"Delete failed: {str(e)}")
|
|
600
745
|
|
|
601
746
|
return Div(id=self.feedback_id)
|
|
@@ -606,14 +751,13 @@ class DataTableResource:
|
|
|
606
751
|
form_data = await req.form()
|
|
607
752
|
record_id = form_data.get(self.row_id_field)
|
|
608
753
|
|
|
609
|
-
# Try to convert ID
|
|
610
754
|
if record_id:
|
|
611
755
|
try:
|
|
612
756
|
record_id = int(record_id)
|
|
613
757
|
except (TypeError, ValueError):
|
|
614
758
|
pass
|
|
615
759
|
|
|
616
|
-
# Build data dict from form
|
|
760
|
+
# Build data dict from form
|
|
617
761
|
data = {}
|
|
618
762
|
for col in self.columns:
|
|
619
763
|
key = col["key"]
|
|
@@ -626,7 +770,6 @@ class DataTableResource:
|
|
|
626
770
|
|
|
627
771
|
value = form_data.get(key)
|
|
628
772
|
|
|
629
|
-
# Type conversion based on form config
|
|
630
773
|
field_type = form_cfg.get("type", "text")
|
|
631
774
|
if field_type == "number" and value:
|
|
632
775
|
try:
|
|
@@ -648,33 +791,36 @@ class DataTableResource:
|
|
|
648
791
|
|
|
649
792
|
# CREATE or UPDATE
|
|
650
793
|
if record_id:
|
|
651
|
-
# UPDATE
|
|
652
|
-
|
|
653
|
-
data = self.on_before_update(record_id, data)
|
|
654
|
-
|
|
655
|
-
result = self.update_fn(record_id, data)
|
|
794
|
+
# ===== UPDATE =====
|
|
795
|
+
ctx = self._build_context(req, record=data, record_id=record_id)
|
|
656
796
|
|
|
657
|
-
if self.
|
|
658
|
-
self.
|
|
797
|
+
if self.on_update_hook:
|
|
798
|
+
data = await self._call_hook(self.on_update_hook, ctx)
|
|
799
|
+
if data is not None:
|
|
800
|
+
self.update_fn(req, record_id, data)
|
|
801
|
+
else:
|
|
802
|
+
self.update_fn(req, record_id, data)
|
|
659
803
|
|
|
660
804
|
return self._success_toast("Record updated successfully.")
|
|
661
805
|
else:
|
|
662
|
-
# CREATE
|
|
663
|
-
if self.on_before_create:
|
|
664
|
-
data = self.on_before_create(data)
|
|
665
|
-
|
|
806
|
+
# ===== CREATE =====
|
|
666
807
|
# Generate ID if needed
|
|
667
|
-
if self.id_generator:
|
|
808
|
+
if self.id_generator and self.row_id_field not in data:
|
|
668
809
|
data[self.row_id_field] = self.id_generator()
|
|
669
810
|
|
|
670
|
-
|
|
811
|
+
ctx = self._build_context(req, record=data, record_id=None)
|
|
671
812
|
|
|
672
|
-
if self.
|
|
673
|
-
self.
|
|
813
|
+
if self.on_create_hook:
|
|
814
|
+
data = await self._call_hook(self.on_create_hook, ctx)
|
|
815
|
+
if data is not None:
|
|
816
|
+
self.create_fn(req, data)
|
|
817
|
+
else:
|
|
818
|
+
self.create_fn(req, data)
|
|
674
819
|
|
|
675
820
|
return self._success_toast("Record created successfully.")
|
|
676
821
|
|
|
677
822
|
except Exception as e:
|
|
823
|
+
logger.error(f"Save failed: {e}", exc_info=True)
|
|
678
824
|
return self._error_toast(f"Save failed: {str(e)}")
|
|
679
825
|
|
|
680
826
|
def _wrap_modal(self, modal):
|
|
@@ -684,7 +830,7 @@ class DataTableResource:
|
|
|
684
830
|
return Div(modal, id=self.feedback_id)
|
|
685
831
|
|
|
686
832
|
def _success_toast(self, message: str):
|
|
687
|
-
"""Return a success toast
|
|
833
|
+
"""✅ Return a success toast with auto-refresh trigger."""
|
|
688
834
|
toast = Toast(
|
|
689
835
|
message,
|
|
690
836
|
variant="success",
|
|
@@ -698,10 +844,16 @@ class DataTableResource:
|
|
|
698
844
|
),
|
|
699
845
|
active=True
|
|
700
846
|
)
|
|
701
|
-
|
|
847
|
+
|
|
848
|
+
# Return as HTMLResponse with HX-Trigger for auto-refresh
|
|
849
|
+
from fasthtml.common import to_xml
|
|
850
|
+
html_content = to_xml(Div(toast, id=self.feedback_id))
|
|
851
|
+
response = HTMLResponse(content=html_content)
|
|
852
|
+
response.headers['HX-Trigger'] = self.refresh_trigger
|
|
853
|
+
return response
|
|
702
854
|
|
|
703
855
|
def _error_toast(self, message: str):
|
|
704
|
-
"""Return an error toast
|
|
856
|
+
"""❌ Return an error toast (no auto-refresh on errors)."""
|
|
705
857
|
toast = Toast(
|
|
706
858
|
message,
|
|
707
859
|
variant="error",
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fh-matui
|
|
3
|
+
Version: 0.9.6
|
|
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=UvB3IZAWAs7vV6H9dpZvZPryrSNIyF5_bowVS4_YGFA,23
|
|
2
|
+
fh_matui/_modidx.py,sha256=naHwPQ4kCo-5saE_uozmdPA881kp1gnqvKhmaG-Ya-4,23914
|
|
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=x5HgBWksvBiJyRE0Ux7Ht7n1hy0AZttTVXU0mMiZ8Vo,31714
|
|
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.6.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
|
|
10
|
+
fh_matui-0.9.6.dist-info/METADATA,sha256=-tOPC56fiu90DdvaLQt1C7TOoThZyTJ4m3owELT0BUk,10490
|
|
11
|
+
fh_matui-0.9.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
fh_matui-0.9.6.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
|
|
13
|
+
fh_matui-0.9.6.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
|
|
14
|
+
fh_matui-0.9.6.dist-info/RECORD,,
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fh-matui
|
|
3
|
-
Version: 0.9.4
|
|
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
|
-
Button("Get Started", cls="primary"),
|
|
116
|
-
),
|
|
117
|
-
cls="padding"
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
serve()
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## 📦 Installation
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
pip install fh-matui
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Dependencies
|
|
130
|
-
|
|
131
|
-
fh-matui automatically includes:
|
|
132
|
-
- **python-fasthtml** - The core FastHTML framework
|
|
133
|
-
- **BeerCSS** - Loaded via CDN for Material Design 3 styling
|
|
134
|
-
|
|
135
|
-
### What This Code Does
|
|
136
|
-
|
|
137
|
-
1. **`MatTheme.indigo.headers()`** - Loads BeerCSS with the indigo color scheme
|
|
138
|
-
2. **[`Card`](https://abhisheksreesaila.github.io/fh-matui/components.html#card)** - Creates a Material Design card component with elevation
|
|
139
|
-
3. **[`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield)** - Generates a styled input with floating label
|
|
140
|
-
4. **`Button`** - Renders a Material Design button with ripple effects
|
|
141
|
-
|
|
142
|
-
## 📚 Module Reference
|
|
143
|
-
|
|
144
|
-
| Module | Description | Key Components |
|
|
145
|
-
|--------|-------------|----------------|
|
|
146
|
-
| [Foundations](https://abhisheksreesaila.github.io/fh-matui/foundations.html) | Base utilities and helper functions | `BeerHeaders`, `display`, styling helpers |
|
|
147
|
-
| [Core](https://abhisheksreesaila.github.io/fh-matui/core.html) | Theme system and styling | `MatTheme`, color presets, theme configuration |
|
|
148
|
-
| [Components](https://abhisheksreesaila.github.io/fh-matui/components.html) | UI component library | `Button`, `Card`, `FormField`, `FormModal`, `Grid` |
|
|
149
|
-
| [App Pages](https://abhisheksreesaila.github.io/fh-matui/app_pages.html) | Application layouts | Navigation, sidebars, full-page layouts |
|
|
150
|
-
| [Data Tables](https://abhisheksreesaila.github.io/fh-matui/datatable.html) | Data management components | `DataTable`, `DataTableResource`, CRUD operations |
|
|
151
|
-
| [Web Pages](https://abhisheksreesaila.github.io/fh-matui/web_pages.html) | Marketing/landing pages | Hero sections, feature grids, testimonials |
|
|
152
|
-
|
|
153
|
-
## 🛠️ Development
|
|
154
|
-
|
|
155
|
-
### Install in Development Mode
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
# Clone the repository
|
|
159
|
-
git clone https://github.com/user/fh-matui.git
|
|
160
|
-
cd fh-matui
|
|
161
|
-
|
|
162
|
-
# Install in editable mode
|
|
163
|
-
pip install -e .
|
|
164
|
-
|
|
165
|
-
# Make changes under nbs/ directory, then compile
|
|
166
|
-
nbdev_prepare
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
## 🤝 Why fh-matui?
|
|
170
|
-
|
|
171
|
-
| Challenge | fh-matui Solution |
|
|
172
|
-
|-----------|-------------------|
|
|
173
|
-
| **CSS complexity** | Pre-built Material Design 3 components via BeerCSS |
|
|
174
|
-
| **JavaScript fatigue** | FastHTML handles interactivity declaratively |
|
|
175
|
-
| **Component consistency** | Unified API across all components |
|
|
176
|
-
| **Dark mode support** | Built-in with automatic system preference detection |
|
|
177
|
-
| **Responsive design** | Mobile-first grid system and responsive utilities |
|
|
178
|
-
| **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 |
|
|
179
|
-
| **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 |
|
|
180
|
-
|
|
181
|
-
## 📄 License
|
|
182
|
-
|
|
183
|
-
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.
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
**Built with ❤️ using FastHTML and nbdev**
|
fh_matui-0.9.4.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
fh_matui/__init__.py,sha256=iPcoATf7BiWjSu-KocRdM5zFTR4wx4ktCHlGGpvdc1M,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.4.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
|
|
10
|
-
fh_matui-0.9.4.dist-info/METADATA,sha256=CBK48Mce-W9Lgll_Pj6XvYGDG_GFkqVor8aCqwI84fk,8369
|
|
11
|
-
fh_matui-0.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
fh_matui-0.9.4.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
|
|
13
|
-
fh_matui-0.9.4.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
|
|
14
|
-
fh_matui-0.9.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|