fh-matui 0.9.12__py3-none-any.whl → 0.9.14__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.11"
1
+ __version__ = "0.9.13"
fh_matui/_modidx.py CHANGED
@@ -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'),
fh_matui/components.py CHANGED
@@ -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 *
@@ -29,9 +29,13 @@ from nbdev.showdoc import show_doc
29
29
  # %% ../nbs/02_components.ipynb 6
30
30
  #| code-fold: true
31
31
  def NavToggleButton(target, icon='menu', **kwargs):
32
- """Create a navigation toggle button that toggles the 'max' class on the target element"""
32
+ """Create a navigation toggle button that toggles the 'max' class on the target element.
33
+
34
+ Also toggles icon between 'menu' and 'menu_open' based on nav state.
35
+ """
33
36
  cls = kwargs.get('cls', 'circle transparent')
34
- onclick = f"toggleNav('{target}'); return false;"
37
+ # Inline JS: toggle nav's max class AND change this button's icon
38
+ onclick = f"var nav=document.querySelector('{target}');if(nav){{nav.classList.toggle('max');var i=this.querySelector('i');if(i)i.textContent=nav.classList.contains('max')?'menu_open':'menu'}};return false;"
35
39
  kwargs.update({'onclick': onclick, 'cls': cls})
36
40
  return Button(I(icon), **kwargs)
37
41
 
@@ -323,12 +327,31 @@ def Icon(icon: str, size: str = None, fill: bool = False, cls = (), **kwargs):
323
327
 
324
328
  # %% ../nbs/02_components.ipynb 32
325
329
  #| code-fold: true
326
- def NavBar(*children, brand=None, sticky=False, cls='', **kwargs):
327
- """Horizontal navigation bar with optional brand and sticky positioning"""
328
- nav_cls = f"{'sticky top' if sticky else ''} surface-container {cls}".strip()
330
+ def NavBar(*children, brand=None, sticky=False, cls='', size='small',
331
+ hx_boost=True, hx_target='#main-content', **kwargs):
332
+ """Horizontal navigation bar with HTMX SPA navigation defaults.
333
+
334
+ Args:
335
+ brand: Brand element (logo/title) positioned on the left
336
+ sticky: Whether navbar sticks to top on scroll
337
+ size: Navbar size - 'small' (default), 'medium', 'large', or None
338
+ hx_boost: Auto-enhance all <a> links for HTMX navigation (default True)
339
+ hx_target: Target element for boosted links (default '#main-content')
340
+ """
341
+ size_cls = size if size else ''
342
+ nav_cls = f"{'sticky top' if sticky else ''} surface {size_cls} {cls}".strip()
343
+
344
+ # HTMX SPA optimizations
345
+ if hx_boost: kwargs['hx_boost'] = 'true'
346
+ if hx_target: kwargs['hx_target'] = hx_target
347
+ kwargs.setdefault('hx_push_url', 'true')
348
+
349
+ # Use small-padding for compact navbar
350
+ padding_cls = 'small-padding' if size == 'small' else 'padding'
351
+
329
352
  if brand:
330
- return Nav(brand, Div(cls='max'), *children, cls=f"row middle-align padding {nav_cls}", **kwargs)
331
- return Nav(*children, cls=f"padding {nav_cls}", **kwargs)
353
+ return Nav(brand, Div(cls='max'), *children, cls=f"row middle-align {padding_cls} {nav_cls}", **kwargs)
354
+ return Nav(*children, cls=f"{padding_cls} {nav_cls}", **kwargs)
332
355
 
333
356
  # %% ../nbs/02_components.ipynb 35
334
357
  def Modal(*c, id=None, footer=None, active=False, overlay='default', position=None, cls=(), **kwargs):
@@ -1072,33 +1095,97 @@ def NavSideBarLinks(*children, as_list=False, cls='', **kwargs):
1072
1095
  return Ul(*children, cls=list_cls, **kwargs)
1073
1096
  return Group(*children) if len(children) > 1 else (children[0] if children else Group())
1074
1097
 
1075
- def NavSideBarContainer(*children, position='left', size='m', cls='', active=False, **kwargs):
1076
- """BeerCSS navigation sidebar/drawer component."""
1077
- base_cls = f"{size} {position} surface-container"
1098
+ def NavSideBarContainer(*children, position='left', size='m', cls='', active=False,
1099
+ hx_boost=True, hx_target='#main-content', **kwargs):
1100
+ """BeerCSS navigation sidebar with HTMX SPA navigation defaults.
1101
+
1102
+ Args:
1103
+ position: Sidebar position ('left' or 'right')
1104
+ size: Sidebar size ('s', 'm', 'l')
1105
+ active: Whether sidebar starts visible
1106
+ hx_boost: Auto-enhance all <a> links for HTMX navigation (default True)
1107
+ hx_target: Target element for boosted links (default '#main-content')
1108
+
1109
+ Usage:
1110
+ Routes should check `req.headers` for HX-Request and return content only for HTMX requests:
1111
+
1112
+ @rt("/dashboard")
1113
+ def dashboard(req):
1114
+ content = dashboard_content()
1115
+ if 'HX-Request' in req.headers: return content # HTMX swap
1116
+ return Layout(content) # Full page load
1117
+ """
1118
+ base_cls = f"{size} {position} surface"
1078
1119
  if active: base_cls += " active"
1079
1120
  nav_cls = f"{base_cls} {cls}".strip()
1121
+
1122
+ # HTMX SPA optimizations
1123
+ if hx_boost: kwargs['hx_boost'] = 'true'
1124
+ if hx_target: kwargs['hx_target'] = hx_target
1125
+ kwargs.setdefault('hx_push_url', 'true')
1126
+
1080
1127
  return Nav(*children, cls=nav_cls, **kwargs)
1081
1128
 
1082
1129
  # %% ../nbs/02_components.ipynb 107
1083
1130
  #| code-fold: true
1084
- def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
1085
- main_bg='surface', sidebar_id='app-sidebar', cls='', **kwargs):
1086
- """App layout wrapper with auto-toggle sidebar and sensible defaults."""
1087
- main_content = []
1131
+ def Page(*c, active=True, position=None, cls='', **kwargs):
1132
+ """BeerCSS animated page container.
1088
1133
 
1089
- if nav_bar:
1090
- if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
1091
- if 'sticky' not in nav_bar.attrs['cls']: nav_bar.attrs['cls'] += ' sticky top'
1092
- main_content.append(nav_bar)
1134
+ Pages are containers that can be a main page, multiple pages, or animated elements.
1135
+
1136
+ Args:
1137
+ active: Show page (default True)
1138
+ position: Animation direction - 'left', 'right', 'top', 'bottom' (optional)
1139
+ cls: Additional classes
1093
1140
 
1141
+ Example:
1142
+ Page(H1("Dashboard"), P("Content here")) # Default active page
1143
+ Page(content, position='right') # Slide from right animation
1144
+ """
1145
+ page_cls = ['page']
1146
+ if active: page_cls.append('active')
1147
+ if position: page_cls.append(position)
1148
+ if cls: page_cls.extend(normalize_tokens(cls))
1149
+ return Div(*c, cls=stringify(page_cls), **kwargs)
1150
+
1151
+ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
1152
+ main_bg='surface', sidebar_id='app-sidebar', main_id='main-content', cls='', **kwargs):
1153
+ """App layout with HTMX SPA navigation.
1154
+
1155
+ Args:
1156
+ main_id: ID for main content area (default 'main-content') - use as hx-target
1157
+
1158
+ HTMX SPA features:
1159
+ - hx-boost on sidebar automatically enhances all <a> links
1160
+ - hx-history-elt for back/forward button caching
1161
+ - Routes should check `req.headers` for HX-Request and return content only for HTMX requests
1162
+
1163
+ Usage:
1164
+ @rt("/dashboard")
1165
+ def dashboard(req):
1166
+ content = dashboard_content()
1167
+ if 'HX-Request' in req.headers: return content # HTMX swap
1168
+ return Layout(content) # Full page load
1169
+ """
1170
+ # Build content wrapper with history caching
1171
+ content_wrapper = None
1094
1172
  if content:
1095
- container_cls = stringify((container_size, 'padding', main_bg))
1096
- main_content.append(Main(*content, cls=container_cls))
1173
+ content_wrapper = Div(*content, hx_history_elt='true')
1097
1174
 
1175
+ # No sidebar - simple layout
1098
1176
  if not sidebar and not sidebar_links:
1099
- if main_content: return Div(*main_content, cls=cls, **kwargs)
1100
- else: return Div(cls=cls, **kwargs)
1101
-
1177
+ result = []
1178
+ if nav_bar:
1179
+ if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
1180
+ if 'sticky' not in nav_bar.attrs['cls']: nav_bar.attrs['cls'] += ' sticky top'
1181
+ result.append(nav_bar)
1182
+ if content_wrapper:
1183
+ container_cls = stringify((container_size, 'padding', main_bg))
1184
+ page_content = Page(content_wrapper, id=main_id)
1185
+ result.append(Main(page_content, cls=container_cls))
1186
+ return Div(*result, cls=cls, **kwargs) if result else Div(cls=cls, **kwargs)
1187
+
1188
+ # Sidebar layout with hx-boost
1102
1189
  sidebar_children = [NavSideBarHeader(NavToggleButton(f"#{sidebar_id}"))]
1103
1190
 
1104
1191
  if sidebar_links: sidebar_children.extend(sidebar_links)
@@ -1108,23 +1195,25 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1108
1195
 
1109
1196
  nav_rail = NavSideBarContainer(*sidebar_children, position='left', size='l', id=sidebar_id)
1110
1197
 
1111
- navbar_elem = None
1112
- content_items = []
1113
- for item in main_content:
1114
- if hasattr(item, 'tag') and item.tag == 'nav': navbar_elem = item
1115
- else: content_items.append(item)
1116
-
1117
1198
  layout_children = []
1118
- if navbar_elem: layout_children.append(navbar_elem)
1199
+
1200
+ if nav_bar:
1201
+ if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
1202
+ if 'sticky' not in nav_bar.attrs['cls']: nav_bar.attrs['cls'] += ' sticky top'
1203
+ layout_children.append(nav_bar)
1204
+
1119
1205
  layout_children.append(nav_rail)
1120
- if content_items:
1121
- container_cls = stringify((container_size, 'round', 'elevate', 'margin'))
1122
- layout_children.append(Main(*content_items, cls=container_cls))
1123
1206
 
1124
- final_cls = f"surface-container {cls}".strip() if cls else "surface-container"
1207
+ if content_wrapper:
1208
+ container_cls = stringify((container_size, 'surface-container', 'round', 'padding', 'bottom-margin'))
1209
+ page_content = Page(content_wrapper, id=main_id)
1210
+ layout_children.append(Main(page_content, cls=container_cls))
1211
+
1212
+ final_cls = f"surface {cls}".strip() if cls else "surface"
1125
1213
  return Div(*layout_children, cls=final_cls, **kwargs)
1126
1214
 
1127
- # %% ../nbs/02_components.ipynb 111
1215
+
1216
+ # %% ../nbs/02_components.ipynb 113
1128
1217
  #| code-fold: true
1129
1218
  class TextT(VEnum):
1130
1219
  """Text styles using BeerCSS typography classes."""
@@ -1156,7 +1245,7 @@ class TextPresets(VEnum):
1156
1245
  primary_link = 'link primary-text'
1157
1246
  muted_link = 'link secondary-text'
1158
1247
 
1159
- # %% ../nbs/02_components.ipynb 112
1248
+ # %% ../nbs/02_components.ipynb 114
1160
1249
  #| code-fold: true
1161
1250
  def CodeSpan(*c, cls=(), **kwargs):
1162
1251
  """Inline code snippet."""
@@ -1212,7 +1301,7 @@ def Sup(*c, cls=(), **kwargs):
1212
1301
  cls_str = stringify(cls) if cls else None
1213
1302
  return fc.Sup(*c, cls=cls_str, **kwargs) if cls_str else fc.Sup(*c, **kwargs)
1214
1303
 
1215
- # %% ../nbs/02_components.ipynb 114
1304
+ # %% ../nbs/02_components.ipynb 116
1216
1305
  #| code-fold: true
1217
1306
  def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str = ''):
1218
1307
  """Collapsible FAQ item using details/summary.
@@ -1230,7 +1319,7 @@ def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str
1230
1319
  Summary(Article(Nav(Div(question, cls=f"max bold {question_cls}".strip()), I("expand_more")), cls="round surface-variant border no-elevate")),
1231
1320
  Article(P(answer, cls=f"secondary-text {answer_cls}".strip()), cls="round border padding"))
1232
1321
 
1233
- # %% ../nbs/02_components.ipynb 118
1322
+ # %% ../nbs/02_components.ipynb 120
1234
1323
  #| code-fold: true
1235
1324
  def CookiesBanner(message='We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.',
1236
1325
  accept_text='Accept', decline_text='Decline', settings_text=None, policy_link='/cookies', policy_text='Learn more',
fh_matui/core.py CHANGED
@@ -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",
@@ -31,7 +32,8 @@ beer_hdrs = (
31
32
  Script(src=HEADER_URLS["mdc_js"], type='module'),
32
33
  )
33
34
 
34
- # %% ../nbs/01_core.ipynb 8
35
+ # %% ../nbs/01_core.ipynb 7
36
+ #| code-fold: true
35
37
  #| code-fold: true
36
38
  # All BeerCSS color names
37
39
  COLOR_NAMES = ['amber', 'blue', 'blue_grey', 'brown', 'cyan', 'deep_orange', 'deep_purple',
@@ -88,7 +90,8 @@ ALL_HELPERS = (SIZES + WIDTH_HEIGHT + ELEVATES + DIRECTIONS + FORMS + MARGINS +
88
90
  POSITIONS + RESPONSIVE + ALIGNMENTS + BLURS + OPACITIES + SHADOWS + SPACES +
89
91
  RIPPLES + SCROLLS + WAVES + ZOOMS + THEME_HELPERS + TYPOGRAPHY + TRIGGERS + COLOR_HELPERS)
90
92
 
91
- # %% ../nbs/01_core.ipynb 11
93
+ # %% ../nbs/01_core.ipynb 10
94
+ #| code-fold: true
92
95
  #| code-fold: true
93
96
  class _ThemeChain:
94
97
  """Internal class for building themed headers"""
@@ -142,14 +145,20 @@ class _ThemeChain:
142
145
  }};
143
146
  window.toggleNav = function(selector) {{
144
147
  const nav = document.querySelector(selector);
145
- if (nav) {{ nav.classList.toggle('max'); }}
148
+ if (nav) {{
149
+ nav.classList.toggle('max');
150
+ const isOpen = nav.classList.contains('max');
151
+ const btn = nav.querySelector('button i, button .icon');
152
+ if (btn) btn.textContent = isOpen ? 'menu_open' : 'menu';
153
+ }}
146
154
  }};
147
155
  ''')
148
156
  hdrs.append(theme_script)
149
157
  hdrs.append(Title(title))
150
158
  return tuple(hdrs)
151
159
 
152
- # %% ../nbs/01_core.ipynb 12
160
+ # %% ../nbs/01_core.ipynb 11
161
+ #| code-fold: true
153
162
  #| code-fold: true
154
163
  class _ThemeNamespace:
155
164
  """Namespace providing color properties that return _ThemeChain instances"""
@@ -208,7 +217,8 @@ class _ThemeNamespace:
208
217
 
209
218
  MatTheme = _ThemeNamespace()
210
219
 
211
- # %% ../nbs/01_core.ipynb 16
220
+ # %% ../nbs/01_core.ipynb 15
221
+ #| code-fold: true
212
222
  #| code-fold: true
213
223
  class BeerCssChain:
214
224
  """Base class for chaining Beer CSS helper classes together"""
fh_matui/datatable.py CHANGED
@@ -19,6 +19,7 @@ from .components import *
19
19
 
20
20
  # %% ../nbs/05_datatable.ipynb 7
21
21
  #| code-fold: true
22
+ #| code-fold: true
22
23
  from math import ceil
23
24
  from urllib.parse import urlencode
24
25
  from typing import Callable, Optional, Any
@@ -452,7 +453,7 @@ class CrudContext:
452
453
  record_id: Optional[Any] = None # ID for update/delete (None for create)
453
454
  feedback_id: Optional[str] = None # Target div ID for HTMX swap (for override handlers)
454
455
 
455
- # %% ../nbs/05_datatable.ipynb 13
456
+ # %% ../nbs/05_datatable.ipynb 14
456
457
  from typing import Callable, Optional, Any, Union
457
458
  from dataclasses import asdict, is_dataclass
458
459
  from datetime import datetime
@@ -488,11 +489,40 @@ class DataTableResource:
488
489
  - Async/sync hook support for external API integration
489
490
  - Auto-refresh table via HX-Trigger after mutations
490
491
  - Layout wrapper for full-page (non-HTMX) responses
492
+ - Optional `get_count` for efficient DB-level pagination
491
493
 
492
494
  **Auto-registers 3 routes:**
493
495
  - `GET {base_route}` → DataTable list view
494
496
  - `GET {base_route}/action` → FormModal for create/edit/view/delete
495
497
  - `POST {base_route}/save` → Save handler with hooks
498
+
499
+ **DB-Level Pagination (Recommended for large datasets):**
500
+
501
+ For efficient pagination, provide both `get_all` (returning paginated rows)
502
+ and `get_count` (returning total count):
503
+
504
+ ```python
505
+ def get_products(req):
506
+ page = int(req.query_params.get('page', 1))
507
+ page_size = int(req.query_params.get('page_size', 10))
508
+ offset = (page - 1) * page_size
509
+ search = req.query_params.get('search', '')
510
+ # SQL-level pagination
511
+ return list(tbl(limit=page_size, offset=offset))
512
+
513
+ def get_product_count(req):
514
+ search = req.query_params.get('search', '')
515
+ return db.execute("SELECT COUNT(*) FROM products").scalar()
516
+
517
+ DataTableResource(
518
+ get_all=get_products, # Returns page_size rows
519
+ get_count=get_product_count, # Returns total count
520
+ ...
521
+ )
522
+ ```
523
+
524
+ If `get_count` is not provided, the library filters and paginates in Python
525
+ (requires `get_all` to return ALL rows - inefficient for large datasets).
496
526
  """
497
527
 
498
528
  def __init__(
@@ -501,8 +531,9 @@ class DataTableResource:
501
531
  base_route: str,
502
532
  columns: list[dict],
503
533
  # Data callbacks - ALL receive request as first param
504
- get_all: Callable[[Any], list], # (req) -> list
534
+ get_all: Callable[[Any], list], # (req) -> list (can be paginated)
505
535
  get_by_id: Callable[[Any, Any], Any], # (req, id) -> record
536
+ get_count: Callable[[Any], int] = None, # (req) -> total count for pagination
506
537
  create: Callable[[Any, dict], Any] = None, # (req, data) -> record
507
538
  update: Callable[[Any, Any, dict], Any] = None, # (req, id, data) -> record
508
539
  delete: Callable[[Any, Any], bool] = None, # (req, id) -> bool
@@ -529,6 +560,7 @@ class DataTableResource:
529
560
  self.columns = columns
530
561
  self.get_all = get_all
531
562
  self.get_by_id = get_by_id
563
+ self.get_count = get_count
532
564
  self.create_fn = create
533
565
  self.update_fn = update
534
566
  self.delete_fn = delete
@@ -704,10 +736,22 @@ class DataTableResource:
704
736
  """Handle main table route."""
705
737
  state = table_state_from_request(req, page_sizes=self.page_sizes)
706
738
  search, page, page_size = state["search"], state["page"], state["page_size"]
707
-
739
+ # Get data from user callback
708
740
  data = self._get_filtered_data(req)
709
- filtered = self._filter_by_search(data, search)
710
- page_data, total, page = self._paginate(filtered, page, page_size)
741
+
742
+ # Determine total count:
743
+ # - If get_count provided: use it (efficient DB-level count)
744
+ # - Otherwise: filter and count in Python (assumes get_all returns all rows)
745
+ if self.get_count:
746
+ # User handles pagination in get_all, we just get count separately
747
+ total = self.get_count(req)
748
+ page_data = data # Already paginated by user
749
+ total_pages = max(1, ceil(total / page_size)) if total > 0 else 1
750
+ page = min(max(1, page), total_pages)
751
+ else:
752
+ # Legacy behavior: filter and paginate in Python
753
+ filtered = self._filter_by_search(data, search)
754
+ page_data, total, page = self._paginate(filtered, page, page_size)
711
755
 
712
756
  table = DataTable(
713
757
  data=page_data,
@@ -828,7 +872,7 @@ class DataTableResource:
828
872
  # Record required for remaining actions
829
873
  if not record:
830
874
  return self._error_toast("Record not found.")
831
-
875
+ return self._wrap_modal(modal)
832
876
  # Handle VIEW (default behavior)
833
877
  if action == "view":
834
878
  modal = FormModal(
fh_matui/foundations.py CHANGED
@@ -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:
fh_matui/web_pages.py CHANGED
@@ -809,20 +809,20 @@ def LandingPageSimple(
809
809
  # %% ../nbs/04_web_pages.ipynb 30
810
810
  def MarkdownSection(
811
811
  content: str, # Markdown text to render
812
- title: str = None, # Optional section title (rendered as H5)
812
+ title: str = None, # Optional section title (rendered as H3 for appropriate size)
813
813
  cls: str = "", # Additional classes
814
814
  ):
815
815
  """Renders markdown content server-side for SEO compatibility.
816
816
 
817
817
  Uses python-markdown to convert markdown to HTML on the server.
818
818
  Search engines see fully rendered HTML (no JavaScript required).
819
- Parent ContentPage uses 'min' class to center the page layout.
819
+ Content is centered using BeerCSS large-width + row center-align pattern.
820
820
 
821
821
  Great for text-heavy pages like Privacy Policy, Terms, About, Blog posts, etc.
822
822
 
823
823
  Args:
824
824
  content: Markdown text string (can include headers, lists, links, code blocks, tables)
825
- title: Optional page title (rendered before markdown content)
825
+ title: Optional page title (rendered as H3 for blog-appropriate sizing)
826
826
  cls: Additional CSS classes
827
827
 
828
828
  Example:
@@ -848,12 +848,15 @@ We value your privacy...
848
848
 
849
849
  elements = []
850
850
  if title:
851
- elements.append(H5(title, cls="center-align"))
851
+ # Use H3 for page title - appropriately sized for content pages
852
+ elements.append(H3(title, cls="bold"))
852
853
 
853
854
  # NotStr tells FastHTML to render raw HTML without escaping
854
855
  elements.append(NotStr(html_content))
855
856
 
856
- return Article(*elements, cls=f"large-padding {cls}".strip())
857
+ # large-width constrains content, wrapped in row center-align for centering
858
+ article = Article(*elements, cls=f"large-width large-padding {cls}".strip())
859
+ return Div(article, cls="row center-align")
857
860
 
858
861
  # %% ../nbs/04_web_pages.ipynb 31
859
862
  def ContentPage(
@@ -871,7 +874,7 @@ def ContentPage(
871
874
  A shell template for text-heavy pages like Privacy Policy, Terms of Service,
872
875
  Security, About, Blog posts, etc. Developer passes any number of content sections.
873
876
 
874
- Layout: Navbar (sticky) -> Content sections (centered with 'min') -> Footer
877
+ Layout: Navbar (sticky) -> Content sections (centered with 'responsive') -> Footer
875
878
 
876
879
  Uses STANDARD_FOOTER_COLUMNS by default for consistent footer across all pages.
877
880
 
@@ -913,7 +916,7 @@ def ContentPage(
913
916
  cls="large-padding",
914
917
  )
915
918
 
916
- # Main content area - use "min" to center content (unlike landing page which uses "max")
917
- main_content = Main(*sections, cls="min large-padding")
919
+ # Main content area - sections handle their own centering
920
+ main_content = Main(*sections, cls="large-padding")
918
921
 
919
922
  return Div(navbar, main_content, footer_el, cls=f"column {cls}".strip())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-matui
3
- Version: 0.9.12
3
+ Version: 0.9.14
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=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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,14 +0,0 @@
1
- fh_matui/__init__.py,sha256=4-YPRncYVIx_LEaOTQQk2IcNM-ASQoM7kDmtXA8kaRQ,24
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=5CBhmcStL88vyrbkThDBTRnj5rwCfvtvgZ0vbnljvVE,50418
5
- fh_matui/core.py,sha256=xtVBN8CtC50ZJ4Iu7o-mUhaA87tWdnz8gBfKRk63Zhs,10680
6
- fh_matui/datatable.py,sha256=LsVwuTuoFOh3rIvETXefsqZOqxos4xi9ct2F7YPIIv8,39266
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.12.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
- fh_matui-0.9.12.dist-info/METADATA,sha256=KbTK3cooiJMUGCYaVaFg1JbgEB7UvKHhpKvlo0AxE-s,10491
11
- fh_matui-0.9.12.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
12
- fh_matui-0.9.12.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
- fh_matui-0.9.12.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
- fh_matui-0.9.12.dist-info/RECORD,,