fh-matui 0.9.14__py3-none-any.whl → 0.9.15__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.13"
1
+ __version__ = "0.9.14"
fh_matui/components.py CHANGED
@@ -163,28 +163,20 @@ def _snap_to_valid_cols(n: int) -> int:
163
163
  return 12
164
164
 
165
165
 
166
- def _wrap_grid_children(cells, cols=None, cols_sm=None, cols_md=None, cols_lg=None):
166
+ def _wrap_grid_children(cells, cols_sm=None, cols_md=None, cols_lg=None):
167
167
  """Wrap grid children with span classes based on column counts."""
168
168
  # If no column counts specified, return cells as-is
169
- if not any([cols, cols_sm, cols_md, cols_lg]):
169
+ if not any([cols_sm, cols_md, cols_lg]):
170
170
  return cells
171
171
 
172
172
  # Build span string from column counts
173
173
  spans = []
174
174
  if cols_sm:
175
175
  spans.append(f's{12 // cols_sm}')
176
- elif cols:
177
- spans.append(f's{12 // cols}')
178
-
179
176
  if cols_md:
180
177
  spans.append(f'm{12 // cols_md}')
181
- elif cols:
182
- spans.append(f'm{12 // cols}')
183
-
184
178
  if cols_lg:
185
179
  spans.append(f'l{12 // cols_lg}')
186
- elif cols:
187
- spans.append(f'l{12 // cols}')
188
180
 
189
181
  span_str = ' '.join(spans) if spans else 's12'
190
182
 
@@ -196,11 +188,21 @@ def Grid(*cells, space=SpaceT.medium_space,
196
188
  cols_min: int = 1, cols_max: int = 4,
197
189
  cols: int = None, cols_sm: int = None, cols_md: int = None, cols_lg: int = None,
198
190
  responsive: bool = True, padding: bool = True, cls: str = '', **kwargs):
199
- """BeerCSS responsive grid with smart column defaults and mobile-first design."""
191
+ """BeerCSS responsive grid with smart column defaults and mobile-first design.
192
+
193
+ MonsterUI-compatible API: `cols` parameter auto-derives responsive breakpoints:
194
+ cols=4 -> cols_sm=1, cols_md=2, cols_lg=4 (mobile stacks, tablet halves, desktop full)
195
+
196
+ For explicit control, use cols_sm, cols_md, cols_lg directly.
197
+ """
200
198
  # Smart defaults based on content count (MonsterUI pattern)
201
199
  if cols:
202
- # Fixed cols for all breakpoints
203
- cols_sm = cols_md = cols_lg = _snap_to_valid_cols(cols)
200
+ # Auto-derive responsive breakpoints from cols (mobile-friendly by default)
201
+ # cols=4 -> stack on mobile (1), half on tablet (2), full on desktop (4)
202
+ snapped = _snap_to_valid_cols(cols)
203
+ cols_lg = cols_lg or snapped
204
+ cols_md = cols_md or _snap_to_valid_cols(max(1, snapped // 2))
205
+ cols_sm = cols_sm or 1 # Always stack on mobile for readability
204
206
  elif not any([cols_sm, cols_md, cols_lg]):
205
207
  # Auto-calculate responsive columns based on item count
206
208
  n = len(cells)
@@ -214,18 +216,20 @@ def Grid(*cells, space=SpaceT.medium_space,
214
216
  if cols_md: cols_md = _snap_to_valid_cols(cols_md)
215
217
  if cols_lg: cols_lg = _snap_to_valid_cols(cols_lg)
216
218
 
217
- wrapped_cells = _wrap_grid_children(cells, cols, cols_sm, cols_md, cols_lg)
219
+ wrapped_cells = _wrap_grid_children(cells, cols_sm, cols_md, cols_lg)
218
220
 
219
221
  cls_tokens = normalize_tokens(cls)
220
222
  grid_cls = ['grid']
221
- if responsive and 'responsive' not in cls_tokens:
222
- grid_cls.append('responsive')
223
+
223
224
  if padding and 'padding' not in cls_tokens and 'no-padding' not in cls_tokens:
224
225
  grid_cls.append('padding')
226
+
225
227
  if space and not _has_space_token(cls_tokens):
226
228
  grid_cls.extend(normalize_tokens(space))
229
+
227
230
  grid_cls.extend(cls_tokens)
228
231
  grid_cls = [t for t in grid_cls if t]
232
+
229
233
  return Div(*wrapped_cells, cls=stringify(dedupe_preserve_order(grid_cls)), **kwargs)
230
234
 
231
235
  # %% ../nbs/02_components.ipynb 16
@@ -589,30 +593,105 @@ def Range(*c, min=None, max=None, step=None, cls=(), **kwargs):
589
593
 
590
594
  # %% ../nbs/02_components.ipynb 59
591
595
  #| code-fold: true
592
- def Select(*items, value='', placeholder='Select...', prefix_icon=None, name='', cls=(), **kwargs):
593
- """BeerCSS menu-based select dropdown with rich styling."""
596
+ # One-time script for Select component HTMX integration
597
+ # Handles menu item clicks, syncs hidden input, dispatches 'itemselected' event
598
+ # Fixed: Better selector, data-value support, menu closing, dual event dispatch
599
+ _SELECT_SCRIPT = Script("""
600
+ (function() {
601
+ if (window._fhMatuiSelectInit) return;
602
+ window._fhMatuiSelectInit = true;
603
+
604
+ document.addEventListener('click', function(e) {
605
+ // Find clicked li inside a menu that's inside a field wrapper
606
+ const li = e.target.closest('li');
607
+ if (!li) return;
608
+
609
+ // Skip transparent items (headers/labels)
610
+ if (li.classList.contains('transparent')) return;
611
+
612
+ // Find the menu and field wrapper
613
+ const menu = li.closest('menu');
614
+ if (!menu) return;
615
+
616
+ const wrapper = menu.closest('.field');
617
+ if (!wrapper) return;
618
+
619
+ // Get value: prefer data-value attribute, fallback to text content
620
+ const value = li.dataset.value || li.textContent.trim();
621
+ const displayText = li.textContent.trim();
622
+
623
+ // Update display input (readonly visible input)
624
+ const displayInput = wrapper.querySelector('input:not([type="hidden"])');
625
+ if (displayInput) displayInput.value = displayText;
626
+
627
+ // Update hidden input (form submission value)
628
+ const hiddenInput = wrapper.querySelector('input[type="hidden"]');
629
+ if (hiddenInput) hiddenInput.value = value;
630
+
631
+ // Close the menu using BeerCSS ui() function
632
+ if (typeof ui === 'function') {
633
+ ui(menu);
634
+ }
635
+
636
+ // Dispatch custom event for HTMX integration (on wrapper for hx-trigger)
637
+ wrapper.dispatchEvent(new CustomEvent('itemselected', {
638
+ detail: { value: value, displayText: displayText, li: li },
639
+ bubbles: true
640
+ }));
641
+
642
+ // Also dispatch change event for standard form handling
643
+ if (hiddenInput) {
644
+ hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
645
+ }
646
+ });
647
+ })();
648
+ """, id="fh-matui-select-init")
649
+
650
+ def Select(*items, value='', placeholder='Select...', prefix_icon=None, name='', id=None, cls=(), **kwargs):
651
+ """BeerCSS menu-based select dropdown with HTMX integration.
652
+
653
+ Dispatches 'itemselected' custom event when a menu item is clicked,
654
+ enabling HTMX patterns like hx-trigger="itemselected".
655
+ """
594
656
  menu_items = []
595
657
  for item in items:
596
658
  if isinstance(item, str): menu_items.append(Li(item))
597
659
  else: menu_items.append(item)
598
660
 
661
+ # Auto-generate ID from name if not provided
662
+ wrapper_id = id or (f"select-{name}" if name else None)
663
+
599
664
  children = []
600
665
  if prefix_icon: children.append(I(prefix_icon))
666
+
667
+ # Display input (readonly, shows selected value)
601
668
  input_attrs = {'value': value, 'readonly': True, 'placeholder': placeholder if placeholder else ' '}
602
- if name: input_attrs['name'] = name
603
- input_attrs.update(kwargs)
604
669
  children.append(Input(**input_attrs))
670
+
671
+ # Hidden input for form submission (carries the actual value)
672
+ if name:
673
+ children.append(Input(type='hidden', name=name, value=value))
674
+
605
675
  children.append(I('arrow_drop_down'))
606
676
  children.append(Menu(*menu_items))
607
677
 
678
+ # Include the one-time init script
679
+ children.append(_SELECT_SCRIPT)
680
+
608
681
  field_cls = ['field', 'fill', 'round']
609
682
  if prefix_icon: field_cls.append('prefix')
610
683
  field_cls.append('suffix')
611
684
  if cls: field_cls.extend(normalize_tokens(cls))
612
685
  cls_str = stringify(field_cls)
613
- return Div(*children, cls=cls_str)
686
+
687
+ wrapper_attrs = {'cls': cls_str}
688
+ if wrapper_id:
689
+ wrapper_attrs['id'] = wrapper_id
690
+ wrapper_attrs.update(kwargs)
691
+
692
+ return Div(*children, **wrapper_attrs)
614
693
 
615
- # %% ../nbs/02_components.ipynb 68
694
+ # %% ../nbs/02_components.ipynb 70
616
695
  #| code-fold: true
617
696
  def FormGrid(*c, cols: int = 1):
618
697
  """Responsive grid layout for form fields that stacks on mobile."""
@@ -621,7 +700,7 @@ def FormGrid(*c, cols: int = 1):
621
700
  wrapped = [Div(child, cls=col_cls) for child in c]
622
701
  return Div(*wrapped, cls="grid")
623
702
 
624
- # %% ../nbs/02_components.ipynb 71
703
+ # %% ../nbs/02_components.ipynb 73
625
704
  #| code-fold: true
626
705
  def Progress(*c, value='', max='100', cls=(), **kwargs):
627
706
  """Linear progress bar with value/max support."""
@@ -633,7 +712,7 @@ def Progress(*c, value='', max='100', cls=(), **kwargs):
633
712
  if cls_str: return fc.Progress(*c, cls=cls_str, **progress_attrs)
634
713
  return fc.Progress(*c, **progress_attrs)
635
714
 
636
- # %% ../nbs/02_components.ipynb 74
715
+ # %% ../nbs/02_components.ipynb 76
637
716
  #| code-fold: true
638
717
  def LoadingIndicator(size='medium', cls='', **kwargs):
639
718
  """BeerCSS circular spinner for async operations."""
@@ -641,7 +720,7 @@ def LoadingIndicator(size='medium', cls='', **kwargs):
641
720
  progress_cls = f"circle {size_cls} {cls}".strip()
642
721
  return fc.Progress(cls=progress_cls, **kwargs)
643
722
 
644
- # %% ../nbs/02_components.ipynb 77
723
+ # %% ../nbs/02_components.ipynb 79
645
724
  #| code-fold: true
646
725
  def Table(*c, cls = 'border', **kwargs):
647
726
  """BeerCSS table with optional border/stripes classes."""
@@ -709,14 +788,14 @@ def TableFromDicts(header_data, body_data, footer_data = None, header_cell_rende
709
788
  Tfoot(Tr(*[footer_cell_render(k, footer_data.get(k, '')) for k in header_data])) if footer_data else None,
710
789
  cls=cls, **kwargs)
711
790
 
712
- # %% ../nbs/02_components.ipynb 80
791
+ # %% ../nbs/02_components.ipynb 82
713
792
  #| code-fold: true
714
793
  def TableControls(*controls, cls='', **kwargs):
715
794
  """Toolbar container for table filters, search, and actions."""
716
795
  controls_cls = f"padding middle-align space {cls}".strip()
717
796
  return Div(*controls, cls=controls_cls, **kwargs)
718
797
 
719
- # %% ../nbs/02_components.ipynb 83
798
+ # %% ../nbs/02_components.ipynb 85
720
799
  #| code-fold: true
721
800
  def Pagination(current_page: int, total_pages: int, hx_get: str, hx_target: str = '#table-container',
722
801
  show_first_last: bool = True, cls='', **kwargs):
@@ -751,7 +830,7 @@ def Pagination(current_page: int, total_pages: int, hx_get: str, hx_target: str
751
830
  nav_cls = f"center-align middle-align {cls}".strip()
752
831
  return Nav(*buttons, cls=nav_cls, **kwargs)
753
832
 
754
- # %% ../nbs/02_components.ipynb 86
833
+ # %% ../nbs/02_components.ipynb 88
755
834
  #| code-fold: true
756
835
  def Card(*c, header = None, footer = None, body_cls = 'padding', header_cls = (), footer_cls = (), cls = (), **kwargs):
757
836
  """BeerCSS card with optional header/footer sections."""
@@ -765,7 +844,7 @@ def Card(*c, header = None, footer = None, body_cls = 'padding', header_cls = ()
765
844
  if footer is not None: sections.append(Nav(footer, cls=footer_cls) if footer_cls else Nav(footer))
766
845
  return Article(*sections, cls=cls, **kwargs)
767
846
 
768
- # %% ../nbs/02_components.ipynb 89
847
+ # %% ../nbs/02_components.ipynb 91
769
848
  #| code-fold: true
770
849
  def Toolbar(*items, cls='', elevate='large', fill=True, **kwargs):
771
850
  """BeerCSS toolbar for action bars with elevation options."""
@@ -775,7 +854,7 @@ def Toolbar(*items, cls='', elevate='large', fill=True, **kwargs):
775
854
  if cls: classes.append(cls)
776
855
  return Nav(*items, cls=' '.join(classes), **kwargs)
777
856
 
778
- # %% ../nbs/02_components.ipynb 93
857
+ # %% ../nbs/02_components.ipynb 95
779
858
  #| code-fold: true
780
859
  def Toast(*c, cls='', position='top', variant='', action=None, active=False, dur=None, **kwargs):
781
860
  """BeerCSS snackbar/toast notification with position ('top' or 'bottom') and variant options."""
@@ -819,7 +898,7 @@ def Snackbar(*c, **kwargs):
819
898
  """Alias for Toast component."""
820
899
  return Toast(*c, **kwargs)
821
900
 
822
- # %% ../nbs/02_components.ipynb 96
901
+ # %% ../nbs/02_components.ipynb 98
823
902
  #| code-fold: true
824
903
  class ContainerT(VEnum):
825
904
  """Container size options (BeerCSS). Most alias to 'responsive'; use 'expand' for full-width."""
@@ -830,7 +909,7 @@ class ContainerT(VEnum):
830
909
  xl = 'responsive'
831
910
  expand = 'responsive max'
832
911
 
833
- # %% ../nbs/02_components.ipynb 98
912
+ # %% ../nbs/02_components.ipynb 100
834
913
  #| code-fold: true
835
914
  def _get_form_config(col: dict) -> dict:
836
915
  """Extract form config from column, with sensible defaults."""
@@ -906,7 +985,7 @@ def FormField(
906
985
  **attrs
907
986
  )
908
987
 
909
- # %% ../nbs/02_components.ipynb 100
988
+ # %% ../nbs/02_components.ipynb 102
910
989
  #| code-fold: true
911
990
  from typing import Callable, Any
912
991
 
@@ -1025,7 +1104,7 @@ def FormModal(
1025
1104
  cls="large-width"
1026
1105
  )
1027
1106
 
1028
- # %% ../nbs/02_components.ipynb 102
1107
+ # %% ../nbs/02_components.ipynb 104
1029
1108
  #| code-fold: true
1030
1109
  def NavContainer(*li, title=None, brand=None, position='left', close_button=True, cls='active', id=None, **kwargs):
1031
1110
  """Slide-out navigation drawer with header and close button."""
@@ -1082,7 +1161,7 @@ def BottomNav(*c, cls='bottom', size='s', **kwargs):
1082
1161
  final_cls = f"{cls} {size_cls}".strip()
1083
1162
  return Nav(*c, cls=final_cls, **kwargs)
1084
1163
 
1085
- # %% ../nbs/02_components.ipynb 105
1164
+ # %% ../nbs/02_components.ipynb 107
1086
1165
  #| code-fold: true
1087
1166
  def NavSideBarHeader(*c, cls='', **kwargs):
1088
1167
  """Sidebar header section for menu buttons and branding."""
@@ -1126,7 +1205,7 @@ def NavSideBarContainer(*children, position='left', size='m', cls='', active=Fal
1126
1205
 
1127
1206
  return Nav(*children, cls=nav_cls, **kwargs)
1128
1207
 
1129
- # %% ../nbs/02_components.ipynb 107
1208
+ # %% ../nbs/02_components.ipynb 109
1130
1209
  #| code-fold: true
1131
1210
  def Page(*c, active=True, position=None, cls='', **kwargs):
1132
1211
  """BeerCSS animated page container.
@@ -1150,7 +1229,7 @@ def Page(*c, active=True, position=None, cls='', **kwargs):
1150
1229
 
1151
1230
  def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
1152
1231
  main_bg='surface', sidebar_id='app-sidebar', main_id='main-content', cls='', **kwargs):
1153
- """App layout with HTMX SPA navigation.
1232
+ """App layout with HTMX SPA navigation and responsive mobile support.
1154
1233
 
1155
1234
  Args:
1156
1235
  main_id: ID for main content area (default 'main-content') - use as hx-target
@@ -1160,6 +1239,11 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1160
1239
  - hx-history-elt for back/forward button caching
1161
1240
  - Routes should check `req.headers` for HX-Request and return content only for HTMX requests
1162
1241
 
1242
+ Mobile Responsive:
1243
+ - Desktop (>992px): Left sidebar visible, bottom nav hidden
1244
+ - Mobile (≤992px): Bottom nav visible, sidebar hidden
1245
+ - Both navigations share the same sidebar_links for consistent behavior
1246
+
1163
1247
  Usage:
1164
1248
  @rt("/dashboard")
1165
1249
  def dashboard(req):
@@ -1193,9 +1277,24 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1193
1277
  if is_listy(sidebar): sidebar_children.extend(sidebar)
1194
1278
  else: sidebar_children.append(sidebar)
1195
1279
 
1196
- nav_rail = NavSideBarContainer(*sidebar_children, position='left', size='l', id=sidebar_id)
1280
+ nav_rail = NavSideBarContainer(*sidebar_children, position='left', size='l', id=sidebar_id, cls='desktop-nav')
1281
+
1282
+ # Mobile bottom navigation with same links
1283
+ bottom_nav_children = sidebar_links if sidebar_links else (list(sidebar) if is_listy(sidebar) else [sidebar] if sidebar else [])
1284
+ nav_bottom = BottomNav(*bottom_nav_children, cls='bottom mobile-nav',
1285
+ hx_boost='true', hx_target=f'#{main_id}', hx_push_url='true')
1286
+
1287
+ # Responsive CSS for desktop/mobile navigation switching
1288
+ # Only hide elements when needed - don't override Beer CSS flex layout on desktop
1289
+ responsive_nav_css = Style("""
1290
+ .mobile-nav { display: none !important; }
1291
+ @media (max-width: 992px) {
1292
+ .mobile-nav { display: flex !important; }
1293
+ .desktop-nav { display: none !important; }
1294
+ }
1295
+ """)
1197
1296
 
1198
- layout_children = []
1297
+ layout_children = [responsive_nav_css]
1199
1298
 
1200
1299
  if nav_bar:
1201
1300
  if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
@@ -1203,9 +1302,10 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1203
1302
  layout_children.append(nav_bar)
1204
1303
 
1205
1304
  layout_children.append(nav_rail)
1305
+ layout_children.append(nav_bottom)
1206
1306
 
1207
1307
  if content_wrapper:
1208
- container_cls = stringify((container_size, 'surface-container', 'round', 'padding', 'bottom-margin'))
1308
+ container_cls = stringify((container_size, 'surface-container', 'round', 'padding', 'bottom-margin', 'horizontal-margin'))
1209
1309
  page_content = Page(content_wrapper, id=main_id)
1210
1310
  layout_children.append(Main(page_content, cls=container_cls))
1211
1311
 
@@ -1213,7 +1313,7 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1213
1313
  return Div(*layout_children, cls=final_cls, **kwargs)
1214
1314
 
1215
1315
 
1216
- # %% ../nbs/02_components.ipynb 113
1316
+ # %% ../nbs/02_components.ipynb 115
1217
1317
  #| code-fold: true
1218
1318
  class TextT(VEnum):
1219
1319
  """Text styles using BeerCSS typography classes."""
@@ -1245,7 +1345,7 @@ class TextPresets(VEnum):
1245
1345
  primary_link = 'link primary-text'
1246
1346
  muted_link = 'link secondary-text'
1247
1347
 
1248
- # %% ../nbs/02_components.ipynb 114
1348
+ # %% ../nbs/02_components.ipynb 116
1249
1349
  #| code-fold: true
1250
1350
  def CodeSpan(*c, cls=(), **kwargs):
1251
1351
  """Inline code snippet."""
@@ -1301,7 +1401,7 @@ def Sup(*c, cls=(), **kwargs):
1301
1401
  cls_str = stringify(cls) if cls else None
1302
1402
  return fc.Sup(*c, cls=cls_str, **kwargs) if cls_str else fc.Sup(*c, **kwargs)
1303
1403
 
1304
- # %% ../nbs/02_components.ipynb 116
1404
+ # %% ../nbs/02_components.ipynb 118
1305
1405
  #| code-fold: true
1306
1406
  def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str = ''):
1307
1407
  """Collapsible FAQ item using details/summary.
@@ -1319,7 +1419,7 @@ def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str
1319
1419
  Summary(Article(Nav(Div(question, cls=f"max bold {question_cls}".strip()), I("expand_more")), cls="round surface-variant border no-elevate")),
1320
1420
  Article(P(answer, cls=f"secondary-text {answer_cls}".strip()), cls="round border padding"))
1321
1421
 
1322
- # %% ../nbs/02_components.ipynb 120
1422
+ # %% ../nbs/02_components.ipynb 122
1323
1423
  #| code-fold: true
1324
1424
  def CookiesBanner(message='We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.',
1325
1425
  accept_text='Accept', decline_text='Decline', settings_text=None, policy_link='/cookies', policy_text='Learn more',
fh_matui/datatable.py CHANGED
@@ -3,7 +3,8 @@
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_datatable.ipynb.
4
4
 
5
5
  # %% auto 0
6
- __all__ = ['PAGE_SIZES', 'logger', 'table_state_from_request', 'DataTable', 'CrudContext', 'DataTableResource']
6
+ __all__ = ['PAGE_SIZES', 'TABLE_STYLES', 'TABLE_SPACES', 'TABLE_ALIGNS', 'logger', 'table_state_from_request', 'DataTable',
7
+ 'CrudContext', 'DataTableResource']
7
8
 
8
9
  # %% ../nbs/05_datatable.ipynb 2
9
10
  from fastcore.utils import *
@@ -28,6 +29,11 @@ from dataclasses import asdict, is_dataclass
28
29
  # Default page size options
29
30
  PAGE_SIZES = [5, 10, 20, 50]
30
31
 
32
+ # BeerCSS table styles
33
+ TABLE_STYLES = ['border', 'stripes', 'min', 'fixed'] # Can combine: 'border stripes'
34
+ TABLE_SPACES = ['no-space', 'small-space', 'medium-space', 'large-space']
35
+ TABLE_ALIGNS = ['left-align', 'right-align', 'center-align']
36
+
31
37
 
32
38
  def _to_dict(obj: Any) -> dict:
33
39
  """
@@ -207,9 +213,20 @@ def DataTable(
207
213
  page_sizes: list = None,
208
214
  search_placeholder: str = 'Search...',
209
215
  create_label: str = 'New Record',
210
- empty_message: str = 'No records match the current filters.'
216
+ empty_message: str = 'No records match the current filters.',
217
+ # BeerCSS table styling options
218
+ table_style: str = 'border', # border, stripes, min, fixed (can combine: 'border stripes')
219
+ 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)
211
221
  ):
212
- "Generic data table with server-side pagination, search, and row actions."
222
+ """
223
+ Generic data table with server-side pagination, search, and row actions.
224
+
225
+ Table Styling (BeerCSS):
226
+ table_style: Table visual style - 'border', 'stripes', 'min', 'fixed' or combine like 'border stripes'
227
+ space: Row spacing - 'no-space', 'small-space', 'medium-space', 'large-space'
228
+ align: Text alignment - 'left-align', 'right-align', 'center-align' (None for default)
229
+ """
213
230
  # Defaults
214
231
  crud_ops = crud_ops or {"create": False, "update": False, "delete": False}
215
232
  crud_enabled = crud_enabled or {k: bool(v) for k, v in crud_ops.items() if k != 'custom_actions'}
@@ -313,6 +330,16 @@ def DataTable(
313
330
  cls="max"
314
331
  )
315
332
 
333
+ # Compose table classes from BeerCSS styling options
334
+ table_classes = []
335
+ if table_style:
336
+ table_classes.append(table_style)
337
+ if space:
338
+ table_classes.append(space)
339
+ if align:
340
+ table_classes.append(align)
341
+ table_cls = ' '.join(table_classes) if table_classes else None
342
+
316
343
  # Build table with proper Thead/Tbody structure
317
344
  data_table = Table(
318
345
  Thead(Tr(*[Th(label) for label in header_labels])),
@@ -320,7 +347,7 @@ def DataTable(
320
347
  Tr(*[Td(row_dict.get(key, '')) for key in header_keys])
321
348
  for row_dict in table_rows
322
349
  ]),
323
- cls="border"
350
+ cls=table_cls
324
351
  ) if data else Div(empty_message, cls="center-align padding")
325
352
 
326
353
  # Footer section with page size selector and pagination
@@ -341,7 +368,7 @@ def DataTable(
341
368
  # Single row when no pagination needed
342
369
  footer = page_info_row
343
370
 
344
- return Article(
371
+ return Div(
345
372
  Div(id=feedback_id), # Feedback container for modals/toasts
346
373
  Div(
347
374
  search_form,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-matui
3
- Version: 0.9.14
3
+ Version: 0.9.15
4
4
  Summary: material-ui for fasthtml
5
5
  Home-page: https://github.com/abhisheksreesaila/fh-matui
6
6
  Author: abhishek sreesaila
@@ -0,0 +1,14 @@
1
+ fh_matui/__init__.py,sha256=AXW1LY5wrgbjQVfPOZJRcsfowYQAXor5ryaBdvw-SJo,24
2
+ fh_matui/_modidx.py,sha256=r9X_k4ptjzUPy6u6AS6411Qn0Fq0NuxMyV5PrpMH2oA,24032
3
+ fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
4
+ fh_matui/components.py,sha256=_rndyhubrazX6xTr2EnYGQPkGbvCy6VGUFLUfV_1lKg,58416
5
+ fh_matui/core.py,sha256=fMSw0fHvUTYt1Qd1DzaWDS4xWIDbkyUCcr-LIz2IJ5k,11062
6
+ fh_matui/datatable.py,sha256=9jiVu5YU2tc3-4PKhHxIoVEjwwikVCyJhPGPvWIVgzQ,42558
7
+ fh_matui/foundations.py,sha256=xiQOeyV4FhNzPpw6mbtIrPWMyzlMi3BENo41IlGnAz8,1880
8
+ fh_matui/web_pages.py,sha256=at_M34Vxc1i9O0ukS41PaRJntkXXDzMqyzlcrgESUcw,35103
9
+ fh_matui-0.9.15.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
+ fh_matui-0.9.15.dist-info/METADATA,sha256=E71WzNH6S2mhg8LnE4b9tTbrI7D5zsMtF6hTZxljFQM,10491
11
+ fh_matui-0.9.15.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
+ fh_matui-0.9.15.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
+ fh_matui-0.9.15.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
+ fh_matui-0.9.15.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- fh_matui/__init__.py,sha256=s9smkhMiLTJ6_VCSfo7J6aeWBGIlYjSsTDGrVprZjrI,24
2
- fh_matui/_modidx.py,sha256=r9X_k4ptjzUPy6u6AS6411Qn0Fq0NuxMyV5PrpMH2oA,24032
3
- fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
4
- fh_matui/components.py,sha256=Lgk8Qazjx3TrrkEcGxiUbgPE-4676wL2fcs9nmhv1Cg,54179
5
- fh_matui/core.py,sha256=fMSw0fHvUTYt1Qd1DzaWDS4xWIDbkyUCcr-LIz2IJ5k,11062
6
- fh_matui/datatable.py,sha256=cDk_R9A1WTWa7fxDBBAZsnFU36ko_ml94HxmmbL_ENc,41248
7
- fh_matui/foundations.py,sha256=xiQOeyV4FhNzPpw6mbtIrPWMyzlMi3BENo41IlGnAz8,1880
8
- fh_matui/web_pages.py,sha256=at_M34Vxc1i9O0ukS41PaRJntkXXDzMqyzlcrgESUcw,35103
9
- fh_matui-0.9.14.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
- fh_matui-0.9.14.dist-info/METADATA,sha256=JGk9Wq2RxIQKXxyatsmyVpGKwbxsmU6n1W-hx3rJrjs,10491
11
- fh_matui-0.9.14.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
- fh_matui-0.9.14.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
- fh_matui-0.9.14.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
- fh_matui-0.9.14.dist-info/RECORD,,