fh-matui 0.9.16__py3-none-any.whl → 0.9.18__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.9.16"
1
+ __version__ = "0.9.17"
fh_matui/_modidx.py CHANGED
@@ -162,6 +162,7 @@ d = { 'settings': { 'branch': 'master',
162
162
  'fh_matui.datatable.DataTableResource._wrap_response': ( 'datatable.html#datatableresource._wrap_response',
163
163
  'fh_matui/datatable.py'),
164
164
  'fh_matui.datatable._action_menu': ('datatable.html#_action_menu', 'fh_matui/datatable.py'),
165
+ 'fh_matui.datatable._group_data': ('datatable.html#_group_data', 'fh_matui/datatable.py'),
165
166
  'fh_matui.datatable._is_htmx_request': ('datatable.html#_is_htmx_request', 'fh_matui/datatable.py'),
166
167
  'fh_matui.datatable._page_size_select': ('datatable.html#_page_size_select', 'fh_matui/datatable.py'),
167
168
  'fh_matui.datatable._safe_int': ('datatable.html#_safe_int', 'fh_matui/datatable.py'),
fh_matui/components.py CHANGED
@@ -685,8 +685,8 @@ def Select(*options, label=None, value='', name='', id=None, placeholder=None, c
685
685
 
686
686
  select_el = fc.Select(*opt_elements, **select_attrs)
687
687
 
688
- # Build field wrapper
689
- field_cls = ['field', 'border', 'round']
688
+ # Build field wrapper - 'max' ensures full width like LabelInput
689
+ field_cls = ['field', 'border', 'round', 'max']
690
690
  if label: field_cls.append('label')
691
691
  if cls: field_cls.extend(normalize_tokens(cls))
692
692
  cls_str = stringify(field_cls)
fh_matui/datatable.py CHANGED
@@ -23,8 +23,9 @@ from .components import *
23
23
  #| code-fold: true
24
24
  from math import ceil
25
25
  from urllib.parse import urlencode
26
- from typing import Callable, Optional, Any
26
+ from typing import Callable, Optional, Any, Union
27
27
  from dataclasses import asdict, is_dataclass
28
+ from collections import OrderedDict
28
29
 
29
30
  # Default page size options
30
31
  PAGE_SIZES = [5, 10, 20, 50]
@@ -81,6 +82,25 @@ def _safe_int(value, default):
81
82
  except (TypeError, ValueError):
82
83
  return default
83
84
 
85
+
86
+ def _group_data(data: list[dict], group_key: str) -> list[tuple[Any, list[dict]]]:
87
+ """
88
+ Group data by a column value, preserving input order.
89
+
90
+ Args:
91
+ data: List of row dicts
92
+ group_key: Column key to group by
93
+
94
+ Returns:
95
+ List of (group_value, [rows...]) tuples in order of first appearance
96
+ """
97
+ groups = OrderedDict()
98
+ for row in data:
99
+ val = row.get(group_key)
100
+ groups.setdefault(val, []).append(row)
101
+ return list(groups.items())
102
+
103
+
84
104
  def table_state_from_request(req, page_sizes=None):
85
105
  """
86
106
  Extract pagination state from request query params.
@@ -217,7 +237,12 @@ def DataTable(
217
237
  # BeerCSS table styling options
218
238
  table_style: str = 'border', # border, stripes, min, fixed (can combine: 'border stripes')
219
239
  space: str = 'small-space', # no-space, small-space, medium-space, large-space
220
- align: str = None # left-align, right-align, center-align (None = default left)
240
+ align: str = None, # left-align, right-align, center-align (None = default left)
241
+ # Grouping options
242
+ group_by: str = None, # Column key to group rows by
243
+ group_header_format: Callable[[Any], str] = None, # Format group header text
244
+ group_header_cls: str = 'surface-container', # CSS classes for group header row
245
+ group_header_icon: Union[str, Callable[[Any], str]] = None # Icon name or callable
221
246
  ):
222
247
  """
223
248
  Generic data table with server-side pagination, search, and row actions.
@@ -226,6 +251,12 @@ def DataTable(
226
251
  table_style: Table visual style - 'border', 'stripes', 'min', 'fixed' or combine like 'border stripes'
227
252
  space: Row spacing - 'no-space', 'small-space', 'medium-space', 'large-space'
228
253
  align: Text alignment - 'left-align', 'right-align', 'center-align' (None for default)
254
+
255
+ Grouping:
256
+ group_by: Column key to group rows by (e.g., 'transaction_date')
257
+ group_header_format: Callable to format group value for display (e.g., lambda d: d.strftime('%B %d, %Y'))
258
+ group_header_cls: CSS classes for group header rows (default: 'surface-container')
259
+ group_header_icon: Icon name (str) or callable returning icon name per group value
229
260
  """
230
261
  # Defaults
231
262
  crud_ops = crud_ops or {"create": False, "update": False, "delete": False}
@@ -254,9 +285,11 @@ def DataTable(
254
285
  header_keys = [col["key"] for col in columns] + ["actions"]
255
286
  header_labels = [col.get("label", col["key"]) for col in columns] + [""]
256
287
 
257
- # Build table rows
258
- table_rows = []
259
- for row in data:
288
+ # Calculate colspan for group headers (all visible columns)
289
+ group_colspan = len(header_keys)
290
+
291
+ # Helper to build a single data row
292
+ def build_data_row(row: dict) -> Tr:
260
293
  row_id = row.get(row_id_field)
261
294
  row_dict = {}
262
295
 
@@ -283,7 +316,49 @@ def DataTable(
283
316
  container_id=container_id,
284
317
  feedback_id=feedback_id
285
318
  )
286
- table_rows.append(row_dict)
319
+ return Tr(*[Td(row_dict.get(key, '')) for key in header_keys])
320
+
321
+ # Helper to build a group header row
322
+ def build_group_header(group_value: Any) -> Tr:
323
+ # Format the group label
324
+ if group_header_format and callable(group_header_format):
325
+ label = group_header_format(group_value)
326
+ else:
327
+ label = str(group_value) if group_value is not None else "Ungrouped"
328
+
329
+ # Get icon if specified
330
+ icon_content = None
331
+ if group_header_icon:
332
+ if callable(group_header_icon):
333
+ icon_name = group_header_icon(group_value)
334
+ else:
335
+ icon_name = group_header_icon
336
+ if icon_name:
337
+ icon_content = Icon(icon_name, cls="small")
338
+
339
+ # Build header cell content
340
+ cell_content = []
341
+ if icon_content:
342
+ cell_content.append(icon_content)
343
+ cell_content.append(Span(label, cls="bold"))
344
+
345
+ return Tr(
346
+ Td(*cell_content, colspan=group_colspan, cls="padding"),
347
+ cls=f"group-header {group_header_cls}"
348
+ )
349
+
350
+ # Build table rows - grouped or flat
351
+ if group_by and data:
352
+ # Grouped rendering
353
+ grouped = _group_data(data, group_by)
354
+ tbody_rows = []
355
+ for group_value, group_rows in grouped:
356
+ tbody_rows.append(build_group_header(group_value))
357
+ for row in group_rows:
358
+ tbody_rows.append(build_data_row(row))
359
+ else:
360
+ # Flat rendering (original behavior)
361
+ tbody_rows = [build_data_row(row) for row in data]
287
362
 
288
363
  # Create button - use crud_enabled for callable support
289
364
  create_button = None
@@ -343,10 +418,7 @@ def DataTable(
343
418
  # Build table with proper Thead/Tbody structure
344
419
  data_table = Table(
345
420
  Thead(Tr(*[Th(label) for label in header_labels])),
346
- Tbody(*[
347
- Tr(*[Td(row_dict.get(key, '')) for key in header_keys])
348
- for row_dict in table_rows
349
- ]),
421
+ Tbody(*tbody_rows),
350
422
  cls=table_cls
351
423
  ) if data else Div(empty_message, cls="center-align padding")
352
424
 
@@ -517,12 +589,26 @@ class DataTableResource:
517
589
  - Auto-refresh table via HX-Trigger after mutations
518
590
  - Layout wrapper for full-page (non-HTMX) responses
519
591
  - Optional `get_count` for efficient DB-level pagination
592
+ - **Row grouping** with `group_by` for visual organization
520
593
 
521
594
  **Auto-registers 3 routes:**
522
595
  - `GET {base_route}` → DataTable list view
523
596
  - `GET {base_route}/action` → FormModal for create/edit/view/delete
524
597
  - `POST {base_route}/save` → Save handler with hooks
525
598
 
599
+ **Row Grouping:**
600
+
601
+ Group rows by a column value with formatted section headers:
602
+
603
+ ```python
604
+ DataTableResource(
605
+ ...,
606
+ group_by="transaction_date",
607
+ group_header_format=lambda d: d.strftime("%B %d, %Y"),
608
+ group_header_icon="calendar_today"
609
+ )
610
+ ```
611
+
526
612
  **DB-Level Pagination (Recommended for large datasets):**
527
613
 
528
614
  For efficient pagination, provide both `get_all` (returning paginated rows)
@@ -580,7 +666,12 @@ class DataTableResource:
580
666
  layout_wrapper: Callable[[Any, Any], Any] = None, # (content, req) -> wrapped
581
667
  # Custom generators
582
668
  id_generator: Callable[[], Any] = None,
583
- timestamp_fields: dict = None
669
+ timestamp_fields: dict = None,
670
+ # Grouping options
671
+ group_by: str = None, # Column key to group rows by
672
+ group_header_format: Callable[[Any], str] = None, # Format group header text
673
+ group_header_cls: str = "surface-container", # CSS classes for group header row
674
+ group_header_icon: Union[str, Callable[[Any], str]] = None # Icon name or callable
584
675
  ):
585
676
  self.app = app
586
677
  self.base_route = base_route.rstrip("/")
@@ -598,6 +689,12 @@ class DataTableResource:
598
689
  self.create_label = create_label
599
690
  self.empty_message = empty_message
600
691
 
692
+ # Grouping options
693
+ self.group_by = group_by
694
+ self.group_header_format = group_header_format
695
+ self.group_header_cls = group_header_cls
696
+ self.group_header_icon = group_header_icon
697
+
601
698
  # Determine CRUD ops from provided functions
602
699
  if crud_ops is None:
603
700
  self.crud_ops = {
@@ -796,7 +893,12 @@ class DataTableResource:
796
893
  page_sizes=self.page_sizes,
797
894
  search_placeholder=self.search_placeholder,
798
895
  create_label=self.create_label,
799
- empty_message=self.empty_message
896
+ empty_message=self.empty_message,
897
+ # Grouping options
898
+ group_by=self.group_by,
899
+ group_header_format=self.group_header_format,
900
+ group_header_cls=self.group_header_cls,
901
+ group_header_icon=self.group_header_icon
800
902
  )
801
903
 
802
904
  # Wrap table in auto-refresh container
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-matui
3
- Version: 0.9.16
3
+ Version: 0.9.18
4
4
  Summary: material-ui for fasthtml
5
5
  Home-page: https://github.com/abhisheksreesaila/fh-matui
6
6
  Author: abhishek sreesaila
@@ -84,22 +84,24 @@ responsive web interfaces entirely in Python — no JavaScript required.
84
84
 
85
85
  fh-matui includes 15+ pre-configured Material Design 3 color themes:
86
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` | 🔶 |
87
+ | Theme | Preview | Theme | Preview |
88
+ |------------------------|---------|------------------------|---------|
89
+ | `MatTheme.red` | 🔴 | `MatTheme.pink` | 🩷 |
90
+ | `MatTheme.purple` | 🟣 | `MatTheme.deep_purple` | 💜 |
91
+ | `MatTheme.indigo` | 🔵 | `MatTheme.blue` | 💙 |
92
+ | `MatTheme.light_blue` | 🩵 | `MatTheme.cyan` | 🌊 |
93
+ | `MatTheme.teal` | 🩶 | `MatTheme.green` | 💚 |
94
+ | `MatTheme.light_green` | 🍀 | `MatTheme.lime` | 💛 |
95
+ | `MatTheme.yellow` | 🌟 | `MatTheme.amber` | 🧡 |
96
+ | `MatTheme.orange` | 🟠 | `MatTheme.deep_orange` | 🔶 |
97
+ | `MatTheme.brown` | 🟤 | `MatTheme.grey` | ⚪ |
98
+ | `MatTheme.blue_grey` | 🔘 | | |
97
99
 
98
100
  **Usage:**
99
101
 
100
102
  ``` python
101
103
  # Choose your theme
102
- app, rt = fast_app(hdrs=[MatTheme.deepPurple.headers()])
104
+ app, rt = fast_app(hdrs=[MatTheme.deep_purple.headers()])
103
105
  ```
104
106
 
105
107
  ## 🚀 Quick Start
@@ -120,7 +122,6 @@ def home():
120
122
  Card(
121
123
  H3("Welcome to fh-matui!"),
122
124
  P("Build beautiful Material Design apps with Python."),
123
- FormField("email", label="Email", type="email"),
124
125
  Button("Get Started", cls="primary"),
125
126
  ),
126
127
  cls="padding"
@@ -145,8 +146,10 @@ styling
145
146
 
146
147
  1. **`MatTheme.indigo.headers()`** - Loads BeerCSS with the indigo
147
148
  color scheme
148
- 2. **`Card`** - Creates a Material Design card component with elevation
149
- 3. **`FormField`** - Generates a styled input with floating label
149
+ 2. **[`Card`](https://abhisheksreesaila.github.io/fh-matui/components.html#card)** -
150
+ Creates a Material Design card component with elevation
151
+ 3. **[`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield)** -
152
+ Generates a styled input with floating label
150
153
  4. **`Button`** - Renders a Material Design button with ripple effects
151
154
 
152
155
  ## 📚 Module Reference
@@ -155,9 +158,9 @@ styling
155
158
  |----|----|----|
156
159
  | [Foundations](foundations.html) | Base utilities and helper functions | `BeerHeaders`, `display`, styling helpers |
157
160
  | [Core](core.html) | Theme system and styling | `MatTheme`, color presets, theme configuration |
158
- | [Components](components.html) | UI component library | `Button`, `Card`, `FormField`, `FormModal`, `Grid` |
161
+ | [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) |
159
162
  | [App Pages](app_pages.html) | Application layouts | Navigation, sidebars, full-page layouts |
160
- | [Data Tables](05_table.html) | Data management components | `DataTable`, `DataTableResource`, CRUD operations |
163
+ | [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 |
161
164
  | [Web Pages](web_pages.html) | Marketing/landing pages | Hero sections, feature grids, testimonials |
162
165
 
163
166
  ## 🛠️ Development
@@ -185,8 +188,8 @@ nbdev_prepare
185
188
  | **Component consistency** | Unified API across all components |
186
189
  | **Dark mode support** | Built-in with automatic system preference detection |
187
190
  | **Responsive design** | Mobile-first grid system and responsive utilities |
188
- | **Form handling** | `FormField`, `FormGrid`, `FormModal` for rapid form building |
189
- | **Data management** | `DataTable` and `DataTableResource` for CRUD operations |
191
+ | **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 |
192
+ | **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 |
190
193
 
191
194
  ## 🤖 For LLM Users
192
195
 
@@ -0,0 +1,14 @@
1
+ fh_matui/__init__.py,sha256=LnwynovXVEl6Umz2kNcDZKVZFDV0K6olFWasW49esS4,24
2
+ fh_matui/_modidx.py,sha256=n9f6vJZfytT_mZBD4RpZJD4QngGf7KMeyCA6IZdvqZI,24290
3
+ fh_matui/app_pages.py,sha256=DtaStRuYJ_oiEB2nBzMApCv0NfBkE2wUZMUfKBMhMFs,10378
4
+ fh_matui/components.py,sha256=y00glXXR5dB4zHwCDnqXCeGiYKMETK1WO6l7Ee2Bdls,59103
5
+ fh_matui/core.py,sha256=3j-4ot0MgmyBpsIg7qRh3-KTlRYmgVeetQhQNQPQ19E,11108
6
+ fh_matui/datatable.py,sha256=NwyPcpltqA_wH2qo7n6SqfBLUxzBGMzDiB3he806WkA,46783
7
+ fh_matui/foundations.py,sha256=jSJuELof3B-YeZ4jlsIV-ABpOn9Hv2Qd67o0LuBTqVY,1912
8
+ fh_matui/web_pages.py,sha256=6l0axbrCYmqSmSGrq0qt3nPUNblArCCAy_LlN-iCsNc,35183
9
+ fh_matui-0.9.18.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
+ fh_matui-0.9.18.dist-info/METADATA,sha256=az5hNsGo71wVmtYJeXsRAb2d5uTgd-3EczRasGmsKWY,10601
11
+ fh_matui-0.9.18.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
+ fh_matui-0.9.18.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
+ fh_matui-0.9.18.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
+ fh_matui-0.9.18.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- fh_matui/__init__.py,sha256=1Trt9wiX-dulRPRdt82fqX0JH-xfZMdFAJe6dA4gaQM,24
2
- fh_matui/_modidx.py,sha256=x9AV-LcrfUpON34QN4WnbO-E_GKQ2PhdlY5Zh0NEAT0,24162
3
- fh_matui/app_pages.py,sha256=DtaStRuYJ_oiEB2nBzMApCv0NfBkE2wUZMUfKBMhMFs,10378
4
- fh_matui/components.py,sha256=nbqVBqehyjUYnNlkBwbenKzmlsZ7Ah6i19G4eHoksf0,59053
5
- fh_matui/core.py,sha256=3j-4ot0MgmyBpsIg7qRh3-KTlRYmgVeetQhQNQPQ19E,11108
6
- fh_matui/datatable.py,sha256=DRgA6nmm4h_QCMmd6CJK8QluRJ0S7c8j9H-yHjpUjBo,42590
7
- fh_matui/foundations.py,sha256=jSJuELof3B-YeZ4jlsIV-ABpOn9Hv2Qd67o0LuBTqVY,1912
8
- fh_matui/web_pages.py,sha256=6l0axbrCYmqSmSGrq0qt3nPUNblArCCAy_LlN-iCsNc,35183
9
- fh_matui-0.9.16.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
- fh_matui-0.9.16.dist-info/METADATA,sha256=RR9vOYzH4hsAqBvlzh2s4kTBvvDXQcfMt1W0DIkWT64,9523
11
- fh_matui-0.9.16.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
- fh_matui-0.9.16.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
- fh_matui-0.9.16.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
- fh_matui-0.9.16.dist-info/RECORD,,