fh-matui 0.9.13__tar.gz → 0.9.15__tar.gz

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.
Files changed (24) hide show
  1. {fh_matui-0.9.13/fh_matui.egg-info → fh_matui-0.9.15}/PKG-INFO +1 -1
  2. fh_matui-0.9.15/fh_matui/__init__.py +1 -0
  3. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/_modidx.py +1 -0
  4. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/components.py +176 -53
  5. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/core.py +5 -0
  6. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/datatable.py +33 -5
  7. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/foundations.py +3 -0
  8. {fh_matui-0.9.13 → fh_matui-0.9.15/fh_matui.egg-info}/PKG-INFO +1 -1
  9. {fh_matui-0.9.13 → fh_matui-0.9.15}/settings.ini +1 -1
  10. fh_matui-0.9.13/fh_matui/__init__.py +0 -1
  11. {fh_matui-0.9.13 → fh_matui-0.9.15}/LICENSE +0 -0
  12. {fh_matui-0.9.13 → fh_matui-0.9.15}/MANIFEST.in +0 -0
  13. {fh_matui-0.9.13 → fh_matui-0.9.15}/README.md +0 -0
  14. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/app_pages.py +0 -0
  15. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/web_pages.py +0 -0
  16. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/SOURCES.txt +0 -0
  17. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/dependency_links.txt +0 -0
  18. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/entry_points.txt +0 -0
  19. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/not-zip-safe +0 -0
  20. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/requires.txt +0 -0
  21. {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/top_level.txt +0 -0
  22. {fh_matui-0.9.13 → fh_matui-0.9.15}/pyproject.toml +0 -0
  23. {fh_matui-0.9.13 → fh_matui-0.9.15}/setup.cfg +0 -0
  24. {fh_matui-0.9.13 → fh_matui-0.9.15}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-matui
3
- Version: 0.9.13
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 @@
1
+ __version__ = "0.9.14"
@@ -54,6 +54,7 @@ d = { 'settings': { 'branch': 'master',
54
54
  'fh_matui.components.NavSideBarLinks': ('components.html#navsidebarlinks', 'fh_matui/components.py'),
55
55
  'fh_matui.components.NavSubtitle': ('components.html#navsubtitle', 'fh_matui/components.py'),
56
56
  'fh_matui.components.NavToggleButton': ('components.html#navtogglebutton', 'fh_matui/components.py'),
57
+ 'fh_matui.components.Page': ('components.html#page', 'fh_matui/components.py'),
57
58
  'fh_matui.components.Pagination': ('components.html#pagination', 'fh_matui/components.py'),
58
59
  'fh_matui.components.Progress': ('components.html#progress', 'fh_matui/components.py'),
59
60
  'fh_matui.components.Q': ('components.html#q', 'fh_matui/components.py'),
@@ -10,9 +10,9 @@ __all__ = ['BUTTON_SPECIALS', 'ButtonT', 'ANCHOR_SPECIALS', 'AT', 'NavToggleButt
10
10
  'FormGrid', 'Progress', 'LoadingIndicator', 'Table', 'Td', 'Th', 'Thead', 'Tbody', 'Tfoot', 'TableFromLists',
11
11
  'TableFromDicts', 'TableControls', 'Pagination', 'Card', 'Toolbar', 'Toast', 'Snackbar', 'ContainerT',
12
12
  'FormField', 'FormModal', 'NavContainer', 'NavHeaderLi', 'NavDividerLi', 'NavCloseLi', 'NavSubtitle',
13
- 'BottomNav', 'NavSideBarHeader', 'NavSideBarLinks', 'NavSideBarContainer', 'Layout', 'TextT', 'TextPresets',
14
- 'CodeSpan', 'CodeBlock', 'Blockquote', 'Q', 'Em', 'Strong', 'Small', 'Mark', 'Abbr', 'Sub', 'Sup', 'FAQItem',
15
- 'CookiesBanner']
13
+ 'BottomNav', 'NavSideBarHeader', 'NavSideBarLinks', 'NavSideBarContainer', 'Page', 'Layout', 'TextT',
14
+ 'TextPresets', 'CodeSpan', 'CodeBlock', 'Blockquote', 'Q', 'Em', 'Strong', 'Small', 'Mark', 'Abbr', 'Sub',
15
+ 'Sup', 'FAQItem', 'CookiesBanner']
16
16
 
17
17
  # %% ../nbs/02_components.ipynb 2
18
18
  from fastcore.utils import *
@@ -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
@@ -339,7 +343,7 @@ def NavBar(*children, brand=None, sticky=False, cls='', size='small',
339
343
  hx_target: Target element for boosted links (default '#main-content')
340
344
  """
341
345
  size_cls = size if size else ''
342
- nav_cls = f"{'sticky top' if sticky else ''} surface-container {size_cls} {cls}".strip()
346
+ nav_cls = f"{'sticky top' if sticky else ''} surface {size_cls} {cls}".strip()
343
347
 
344
348
  # HTMX SPA optimizations
345
349
  if hx_boost: kwargs['hx_boost'] = 'true'
@@ -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."""
@@ -1115,7 +1194,7 @@ def NavSideBarContainer(*children, position='left', size='m', cls='', active=Fal
1115
1194
  if 'HX-Request' in req.headers: return content # HTMX swap
1116
1195
  return Layout(content) # Full page load
1117
1196
  """
1118
- base_cls = f"{size} {position} surface-container"
1197
+ base_cls = f"{size} {position} surface"
1119
1198
  if active: base_cls += " active"
1120
1199
  nav_cls = f"{base_cls} {cls}".strip()
1121
1200
 
@@ -1126,11 +1205,31 @@ 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
1210
+ def Page(*c, active=True, position=None, cls='', **kwargs):
1211
+ """BeerCSS animated page container.
1212
+
1213
+ Pages are containers that can be a main page, multiple pages, or animated elements.
1214
+
1215
+ Args:
1216
+ active: Show page (default True)
1217
+ position: Animation direction - 'left', 'right', 'top', 'bottom' (optional)
1218
+ cls: Additional classes
1219
+
1220
+ Example:
1221
+ Page(H1("Dashboard"), P("Content here")) # Default active page
1222
+ Page(content, position='right') # Slide from right animation
1223
+ """
1224
+ page_cls = ['page']
1225
+ if active: page_cls.append('active')
1226
+ if position: page_cls.append(position)
1227
+ if cls: page_cls.extend(normalize_tokens(cls))
1228
+ return Div(*c, cls=stringify(page_cls), **kwargs)
1229
+
1131
1230
  def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
1132
1231
  main_bg='surface', sidebar_id='app-sidebar', main_id='main-content', cls='', **kwargs):
1133
- """App layout with HTMX SPA navigation.
1232
+ """App layout with HTMX SPA navigation and responsive mobile support.
1134
1233
 
1135
1234
  Args:
1136
1235
  main_id: ID for main content area (default 'main-content') - use as hx-target
@@ -1140,6 +1239,11 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1140
1239
  - hx-history-elt for back/forward button caching
1141
1240
  - Routes should check `req.headers` for HX-Request and return content only for HTMX requests
1142
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
+
1143
1247
  Usage:
1144
1248
  @rt("/dashboard")
1145
1249
  def dashboard(req):
@@ -1150,7 +1254,7 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1150
1254
  # Build content wrapper with history caching
1151
1255
  content_wrapper = None
1152
1256
  if content:
1153
- content_wrapper = Div(*content, id=main_id, hx_history_elt='true')
1257
+ content_wrapper = Div(*content, hx_history_elt='true')
1154
1258
 
1155
1259
  # No sidebar - simple layout
1156
1260
  if not sidebar and not sidebar_links:
@@ -1161,7 +1265,8 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1161
1265
  result.append(nav_bar)
1162
1266
  if content_wrapper:
1163
1267
  container_cls = stringify((container_size, 'padding', main_bg))
1164
- result.append(Main(content_wrapper, cls=container_cls))
1268
+ page_content = Page(content_wrapper, id=main_id)
1269
+ result.append(Main(page_content, cls=container_cls))
1165
1270
  return Div(*result, cls=cls, **kwargs) if result else Div(cls=cls, **kwargs)
1166
1271
 
1167
1272
  # Sidebar layout with hx-boost
@@ -1172,9 +1277,24 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1172
1277
  if is_listy(sidebar): sidebar_children.extend(sidebar)
1173
1278
  else: sidebar_children.append(sidebar)
1174
1279
 
1175
- 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
+ """)
1176
1296
 
1177
- layout_children = []
1297
+ layout_children = [responsive_nav_css]
1178
1298
 
1179
1299
  if nav_bar:
1180
1300
  if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
@@ -1182,15 +1302,18 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1182
1302
  layout_children.append(nav_bar)
1183
1303
 
1184
1304
  layout_children.append(nav_rail)
1305
+ layout_children.append(nav_bottom)
1185
1306
 
1186
1307
  if content_wrapper:
1187
- container_cls = stringify((container_size, 'round', 'elevate', 'margin'))
1188
- layout_children.append(Main(content_wrapper, cls=container_cls))
1308
+ container_cls = stringify((container_size, 'surface-container', 'round', 'padding', 'bottom-margin', 'horizontal-margin'))
1309
+ page_content = Page(content_wrapper, id=main_id)
1310
+ layout_children.append(Main(page_content, cls=container_cls))
1189
1311
 
1190
- final_cls = f"surface-container {cls}".strip() if cls else "surface-container"
1312
+ final_cls = f"surface {cls}".strip() if cls else "surface"
1191
1313
  return Div(*layout_children, cls=final_cls, **kwargs)
1192
1314
 
1193
- # %% ../nbs/02_components.ipynb 113
1315
+
1316
+ # %% ../nbs/02_components.ipynb 115
1194
1317
  #| code-fold: true
1195
1318
  class TextT(VEnum):
1196
1319
  """Text styles using BeerCSS typography classes."""
@@ -1222,7 +1345,7 @@ class TextPresets(VEnum):
1222
1345
  primary_link = 'link primary-text'
1223
1346
  muted_link = 'link secondary-text'
1224
1347
 
1225
- # %% ../nbs/02_components.ipynb 114
1348
+ # %% ../nbs/02_components.ipynb 116
1226
1349
  #| code-fold: true
1227
1350
  def CodeSpan(*c, cls=(), **kwargs):
1228
1351
  """Inline code snippet."""
@@ -1278,7 +1401,7 @@ def Sup(*c, cls=(), **kwargs):
1278
1401
  cls_str = stringify(cls) if cls else None
1279
1402
  return fc.Sup(*c, cls=cls_str, **kwargs) if cls_str else fc.Sup(*c, **kwargs)
1280
1403
 
1281
- # %% ../nbs/02_components.ipynb 116
1404
+ # %% ../nbs/02_components.ipynb 118
1282
1405
  #| code-fold: true
1283
1406
  def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str = ''):
1284
1407
  """Collapsible FAQ item using details/summary.
@@ -1296,7 +1419,7 @@ def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str
1296
1419
  Summary(Article(Nav(Div(question, cls=f"max bold {question_cls}".strip()), I("expand_more")), cls="round surface-variant border no-elevate")),
1297
1420
  Article(P(answer, cls=f"secondary-text {answer_cls}".strip()), cls="round border padding"))
1298
1421
 
1299
- # %% ../nbs/02_components.ipynb 120
1422
+ # %% ../nbs/02_components.ipynb 122
1300
1423
  #| code-fold: true
1301
1424
  def CookiesBanner(message='We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.',
1302
1425
  accept_text='Accept', decline_text='Decline', settings_text=None, policy_link='/cookies', policy_text='Learn more',
@@ -19,6 +19,7 @@ from nbdev.showdoc import show_doc
19
19
 
20
20
  # %% ../nbs/01_core.ipynb 5
21
21
  #| code-fold: true
22
+ #| code-fold: true
22
23
  HEADER_URLS = {
23
24
  "beercss_css": "https://cdn.jsdelivr.net/npm/beercss@3.13.1/dist/cdn/beer.min.css",
24
25
  "beercss_js": "https://cdn.jsdelivr.net/npm/beercss@3.13.1/dist/cdn/beer.min.js",
@@ -33,6 +34,7 @@ beer_hdrs = (
33
34
 
34
35
  # %% ../nbs/01_core.ipynb 7
35
36
  #| code-fold: true
37
+ #| code-fold: true
36
38
  # All BeerCSS color names
37
39
  COLOR_NAMES = ['amber', 'blue', 'blue_grey', 'brown', 'cyan', 'deep_orange', 'deep_purple',
38
40
  'green', 'grey', 'indigo', 'light_blue', 'light_green', 'lime', 'orange',
@@ -90,6 +92,7 @@ ALL_HELPERS = (SIZES + WIDTH_HEIGHT + ELEVATES + DIRECTIONS + FORMS + MARGINS +
90
92
 
91
93
  # %% ../nbs/01_core.ipynb 10
92
94
  #| code-fold: true
95
+ #| code-fold: true
93
96
  class _ThemeChain:
94
97
  """Internal class for building themed headers"""
95
98
  def __init__(self, color=None):
@@ -156,6 +159,7 @@ class _ThemeChain:
156
159
 
157
160
  # %% ../nbs/01_core.ipynb 11
158
161
  #| code-fold: true
162
+ #| code-fold: true
159
163
  class _ThemeNamespace:
160
164
  """Namespace providing color properties that return _ThemeChain instances"""
161
165
  @property
@@ -215,6 +219,7 @@ MatTheme = _ThemeNamespace()
215
219
 
216
220
  # %% ../nbs/01_core.ipynb 15
217
221
  #| code-fold: true
222
+ #| code-fold: true
218
223
  class BeerCssChain:
219
224
  """Base class for chaining Beer CSS helper classes together"""
220
225
  def __init__(self, tokens=None):
@@ -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 *
@@ -19,6 +20,7 @@ from .components import *
19
20
 
20
21
  # %% ../nbs/05_datatable.ipynb 7
21
22
  #| code-fold: true
23
+ #| code-fold: true
22
24
  from math import ceil
23
25
  from urllib.parse import urlencode
24
26
  from typing import Callable, Optional, Any
@@ -27,6 +29,11 @@ from dataclasses import asdict, is_dataclass
27
29
  # Default page size options
28
30
  PAGE_SIZES = [5, 10, 20, 50]
29
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
+
30
37
 
31
38
  def _to_dict(obj: Any) -> dict:
32
39
  """
@@ -206,9 +213,20 @@ def DataTable(
206
213
  page_sizes: list = None,
207
214
  search_placeholder: str = 'Search...',
208
215
  create_label: str = 'New Record',
209
- 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)
210
221
  ):
211
- "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
+ """
212
230
  # Defaults
213
231
  crud_ops = crud_ops or {"create": False, "update": False, "delete": False}
214
232
  crud_enabled = crud_enabled or {k: bool(v) for k, v in crud_ops.items() if k != 'custom_actions'}
@@ -312,6 +330,16 @@ def DataTable(
312
330
  cls="max"
313
331
  )
314
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
+
315
343
  # Build table with proper Thead/Tbody structure
316
344
  data_table = Table(
317
345
  Thead(Tr(*[Th(label) for label in header_labels])),
@@ -319,7 +347,7 @@ def DataTable(
319
347
  Tr(*[Td(row_dict.get(key, '')) for key in header_keys])
320
348
  for row_dict in table_rows
321
349
  ]),
322
- cls="border"
350
+ cls=table_cls
323
351
  ) if data else Div(empty_message, cls="center-align padding")
324
352
 
325
353
  # Footer section with page size selector and pagination
@@ -340,7 +368,7 @@ def DataTable(
340
368
  # Single row when no pagination needed
341
369
  footer = page_info_row
342
370
 
343
- return Article(
371
+ return Div(
344
372
  Div(id=feedback_id), # Feedback container for modals/toasts
345
373
  Div(
346
374
  search_form,
@@ -13,6 +13,7 @@ from nbdev.showdoc import show_doc
13
13
 
14
14
  # %% ../nbs/00_foundations.ipynb 4
15
15
  #| code-fold: true
16
+ #| code-fold: true
16
17
  class VEnum(Enum):
17
18
  """Enum with string conversion and concatenation support"""
18
19
  def __str__(self): return self.value
@@ -21,6 +22,7 @@ class VEnum(Enum):
21
22
 
22
23
  # %% ../nbs/00_foundations.ipynb 8
23
24
  #| code-fold: true
25
+ #| code-fold: true
24
26
  def stringify(o):
25
27
  """Converts input types into strings that can be passed to FT components"""
26
28
  if is_listy(o):
@@ -29,6 +31,7 @@ def stringify(o):
29
31
 
30
32
  # %% ../nbs/00_foundations.ipynb 12
31
33
  #| code-fold: true
34
+ #| code-fold: true
32
35
  def normalize_tokens(cls):
33
36
  """Normalize class input to list of string tokens"""
34
37
  if cls is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-matui
3
- Version: 0.9.13
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
@@ -5,7 +5,7 @@
5
5
  ### Python library ###
6
6
  repo = fh-matui
7
7
  lib_name = %(repo)s
8
- version = 0.9.13
8
+ version = 0.9.15
9
9
  min_python = 3.9
10
10
  license = apache2
11
11
  black_formatting = False
@@ -1 +0,0 @@
1
- __version__ = "0.9.12"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes