fh-matui 0.9.5__tar.gz → 0.9.13__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.
- {fh_matui-0.9.5/fh_matui.egg-info → fh_matui-0.9.13}/PKG-INFO +1 -1
- fh_matui-0.9.13/fh_matui/__init__.py +1 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui/_modidx.py +3 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui/components.py +200 -79
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui/core.py +10 -5
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui/datatable.py +350 -152
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui/web_pages.py +11 -8
- {fh_matui-0.9.5 → fh_matui-0.9.13/fh_matui.egg-info}/PKG-INFO +1 -1
- {fh_matui-0.9.5 → fh_matui-0.9.13}/settings.ini +1 -1
- fh_matui-0.9.5/fh_matui/__init__.py +0 -1
- {fh_matui-0.9.5 → fh_matui-0.9.13}/LICENSE +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/MANIFEST.in +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/README.md +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui/app_pages.py +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui/foundations.py +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui.egg-info/SOURCES.txt +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui.egg-info/dependency_links.txt +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui.egg-info/entry_points.txt +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui.egg-info/not-zip-safe +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui.egg-info/requires.txt +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/fh_matui.egg-info/top_level.txt +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/pyproject.toml +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/setup.cfg +0 -0
- {fh_matui-0.9.5 → fh_matui-0.9.13}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.12"
|
|
@@ -157,7 +157,10 @@ d = { 'settings': { 'branch': 'master',
|
|
|
157
157
|
'fh_matui/datatable.py'),
|
|
158
158
|
'fh_matui.datatable.DataTableResource._wrap_modal': ( 'datatable.html#datatableresource._wrap_modal',
|
|
159
159
|
'fh_matui/datatable.py'),
|
|
160
|
+
'fh_matui.datatable.DataTableResource._wrap_response': ( 'datatable.html#datatableresource._wrap_response',
|
|
161
|
+
'fh_matui/datatable.py'),
|
|
160
162
|
'fh_matui.datatable._action_menu': ('datatable.html#_action_menu', 'fh_matui/datatable.py'),
|
|
163
|
+
'fh_matui.datatable._is_htmx_request': ('datatable.html#_is_htmx_request', 'fh_matui/datatable.py'),
|
|
161
164
|
'fh_matui.datatable._page_size_select': ('datatable.html#_page_size_select', 'fh_matui/datatable.py'),
|
|
162
165
|
'fh_matui.datatable._safe_int': ('datatable.html#_safe_int', 'fh_matui/datatable.py'),
|
|
163
166
|
'fh_matui.datatable._to_dict': ('datatable.html#_to_dict', 'fh_matui/datatable.py'),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# %% auto 0
|
|
6
6
|
__all__ = ['BUTTON_SPECIALS', 'ButtonT', 'ANCHOR_SPECIALS', 'AT', 'NavToggleButton', 'SpaceT', 'GridSpanT', 'GridCell', 'Grid',
|
|
7
|
-
'
|
|
7
|
+
'DivHStacked', 'DivLAligned', 'DivVStacked', 'DivRAligned', 'DivCentered', 'DivFullySpaced', 'Icon',
|
|
8
8
|
'NavBar', 'Modal', 'ModalButton', 'ModalCancel', 'ModalConfirm', 'ModalTitle', 'ModalBody', 'ModalFooter',
|
|
9
9
|
'Field', 'LabelInput', 'FormLabel', 'CheckboxX', 'Radio', 'Switch', 'TextArea', 'Range', 'Select',
|
|
10
10
|
'FormGrid', 'Progress', 'LoadingIndicator', 'Table', 'Td', 'Th', 'Thead', 'Tbody', 'Tfoot', 'TableFromLists',
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -225,19 +229,8 @@ def Grid(*cells, space=SpaceT.medium_space,
|
|
|
225
229
|
return Div(*wrapped_cells, cls=stringify(dedupe_preserve_order(grid_cls)), **kwargs)
|
|
226
230
|
|
|
227
231
|
# %% ../nbs/02_components.ipynb 16
|
|
228
|
-
def
|
|
229
|
-
"""
|
|
230
|
-
cls_tokens = normalize_tokens(cls)
|
|
231
|
-
tokens = ['left-align']
|
|
232
|
-
tokens.extend(cls_tokens)
|
|
233
|
-
tokens = [t for t in tokens if t]
|
|
234
|
-
return DivHStacked(*c, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
# %% ../nbs/02_components.ipynb 18
|
|
239
|
-
def DivVStacked(*c, responsive=True, padding=True, cls='', **kwargs):
|
|
240
|
-
"""Responsive vertical stack with padding and mobile compatibility."""
|
|
232
|
+
def DivHStacked(*c, responsive=True, padding=True, cls='', **kwargs):
|
|
233
|
+
"""Responsive horizontal stack with padding and mobile compatibility."""
|
|
241
234
|
cls_tokens = normalize_tokens(cls)
|
|
242
235
|
tokens = []
|
|
243
236
|
if responsive and 'responsive' not in cls_tokens:
|
|
@@ -245,7 +238,7 @@ def DivVStacked(*c, responsive=True, padding=True, cls='', **kwargs):
|
|
|
245
238
|
if padding and 'padding' not in cls_tokens and 'no-padding' not in cls_tokens:
|
|
246
239
|
tokens.append('padding')
|
|
247
240
|
if 'grid' not in cls_tokens:
|
|
248
|
-
tokens.extend(['row', '
|
|
241
|
+
tokens.extend(['row', 'middle-align'])
|
|
249
242
|
if not _has_space_token(cls_tokens):
|
|
250
243
|
tokens.append(SpaceT.medium_space)
|
|
251
244
|
tokens.extend(cls_tokens)
|
|
@@ -253,9 +246,20 @@ def DivVStacked(*c, responsive=True, padding=True, cls='', **kwargs):
|
|
|
253
246
|
return Div(*c, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)
|
|
254
247
|
|
|
255
248
|
|
|
256
|
-
# %% ../nbs/02_components.ipynb
|
|
257
|
-
def
|
|
258
|
-
"""
|
|
249
|
+
# %% ../nbs/02_components.ipynb 17
|
|
250
|
+
def DivLAligned(*c, cls='', **kwargs):
|
|
251
|
+
"""MonsterUI-compatible left-aligned row using BeerCSS tokens."""
|
|
252
|
+
cls_tokens = normalize_tokens(cls)
|
|
253
|
+
tokens = ['left-align']
|
|
254
|
+
tokens.extend(cls_tokens)
|
|
255
|
+
tokens = [t for t in tokens if t]
|
|
256
|
+
return DivHStacked(*c, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# %% ../nbs/02_components.ipynb 19
|
|
261
|
+
def DivVStacked(*c, responsive=True, padding=True, cls='', **kwargs):
|
|
262
|
+
"""Responsive vertical stack with padding and mobile compatibility."""
|
|
259
263
|
cls_tokens = normalize_tokens(cls)
|
|
260
264
|
tokens = []
|
|
261
265
|
if responsive and 'responsive' not in cls_tokens:
|
|
@@ -263,7 +267,7 @@ def DivHStacked(*c, responsive=True, padding=True, cls='', **kwargs):
|
|
|
263
267
|
if padding and 'padding' not in cls_tokens and 'no-padding' not in cls_tokens:
|
|
264
268
|
tokens.append('padding')
|
|
265
269
|
if 'grid' not in cls_tokens:
|
|
266
|
-
tokens.extend(['row', '
|
|
270
|
+
tokens.extend(['row', 'vertical'])
|
|
267
271
|
if not _has_space_token(cls_tokens):
|
|
268
272
|
tokens.append(SpaceT.medium_space)
|
|
269
273
|
tokens.extend(cls_tokens)
|
|
@@ -323,17 +327,58 @@ 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='',
|
|
327
|
-
|
|
328
|
-
|
|
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-container {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
|
|
331
|
-
return Nav(*children, cls=f"
|
|
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
|
-
def Modal(*c, id=None, footer=None, active=False, overlay=
|
|
335
|
-
"""BeerCSS modal dialog with
|
|
357
|
+
def Modal(*c, id=None, footer=None, active=False, overlay='default', position=None, cls=(), **kwargs):
|
|
358
|
+
"""BeerCSS modal dialog with position and overlay options.
|
|
359
|
+
|
|
360
|
+
Always returns a list for consistent unpacking with *Modal(...).
|
|
361
|
+
When overlay is enabled, clicking the overlay closes the modal.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
*c: Modal content (title, body, etc.)
|
|
365
|
+
id: Modal ID for data-ui targeting
|
|
366
|
+
footer: Footer content (auto-wrapped in Nav if not already)
|
|
367
|
+
active: Whether modal starts active/visible
|
|
368
|
+
overlay: Overlay style - 'default' (plain), 'blur', 'small-blur',
|
|
369
|
+
'medium-blur', 'large-blur', or False/None (no overlay)
|
|
370
|
+
position: Position - None (center), 'left', 'right', 'top', 'bottom'
|
|
371
|
+
cls: Additional CSS classes
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
List of elements (overlay + dialog, or just [dialog] if no overlay)
|
|
375
|
+
"""
|
|
336
376
|
modal_cls = normalize_tokens(cls)
|
|
377
|
+
|
|
378
|
+
# Add position class if specified
|
|
379
|
+
if position in ['left', 'right', 'top', 'bottom']:
|
|
380
|
+
modal_cls.append(position)
|
|
381
|
+
|
|
337
382
|
if active:
|
|
338
383
|
modal_cls.append('active')
|
|
339
384
|
|
|
@@ -349,19 +394,32 @@ def Modal(*c, id=None, footer=None, active=False, overlay=True, cls=(), **kwargs
|
|
|
349
394
|
# Create the dialog
|
|
350
395
|
dialog = Dialog(*children, id=id, cls=cls_str, **kwargs)
|
|
351
396
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
397
|
+
# Handle overlay - always return a list for consistent *Modal(...) unpacking
|
|
398
|
+
if overlay and overlay not in [False, None]:
|
|
399
|
+
overlay_classes = ['overlay']
|
|
400
|
+
|
|
401
|
+
# Map overlay parameter to BeerCSS classes
|
|
402
|
+
if overlay == 'blur':
|
|
403
|
+
overlay_classes.append('blur')
|
|
404
|
+
elif overlay == 'small-blur':
|
|
405
|
+
overlay_classes.append('small-blur')
|
|
406
|
+
elif overlay == 'medium-blur':
|
|
407
|
+
overlay_classes.append('medium-blur')
|
|
408
|
+
elif overlay == 'large-blur':
|
|
409
|
+
overlay_classes.append('large-blur')
|
|
410
|
+
# 'default' means plain overlay with no blur class
|
|
411
|
+
|
|
412
|
+
overlay_cls = ' '.join(overlay_classes)
|
|
355
413
|
if active:
|
|
356
414
|
overlay_cls += " active"
|
|
357
|
-
|
|
358
|
-
#
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
]
|
|
415
|
+
|
|
416
|
+
# Overlay with data-ui to close modal on click (click-outside-to-close)
|
|
417
|
+
overlay_el = Div(cls=overlay_cls, data_ui=f"#{id}" if id else None)
|
|
418
|
+
|
|
419
|
+
return [overlay_el, dialog]
|
|
363
420
|
|
|
364
|
-
return
|
|
421
|
+
# No overlay - still return as list for consistent unpacking
|
|
422
|
+
return [dialog]
|
|
365
423
|
|
|
366
424
|
def ModalButton(text: str, id: str, icon: str = None, cls=(), **kwargs):
|
|
367
425
|
"""Button that opens a modal via data-ui attribute."""
|
|
@@ -717,15 +775,14 @@ def Toolbar(*items, cls='', elevate='large', fill=True, **kwargs):
|
|
|
717
775
|
if cls: classes.append(cls)
|
|
718
776
|
return Nav(*items, cls=' '.join(classes), **kwargs)
|
|
719
777
|
|
|
720
|
-
# %% ../nbs/02_components.ipynb
|
|
778
|
+
# %% ../nbs/02_components.ipynb 93
|
|
721
779
|
#| code-fold: true
|
|
722
|
-
def Toast(*c, cls='', position='top', variant='', action=None,
|
|
723
|
-
"""BeerCSS snackbar/toast notification with position and variant options."""
|
|
780
|
+
def Toast(*c, cls='', position='top', variant='', action=None, active=False, dur=None, **kwargs):
|
|
781
|
+
"""BeerCSS snackbar/toast notification with position ('top' or 'bottom') and variant options."""
|
|
724
782
|
classes = ['snackbar']
|
|
725
783
|
if variant: classes.append(variant)
|
|
726
|
-
if position:
|
|
727
|
-
|
|
728
|
-
classes.append(position_map.get(position, position))
|
|
784
|
+
if position in ['top', 'bottom']:
|
|
785
|
+
classes.append(position)
|
|
729
786
|
if active: classes.append('active')
|
|
730
787
|
if cls: classes.append(cls)
|
|
731
788
|
|
|
@@ -735,13 +792,34 @@ def Toast(*c, cls='', position='top', variant='', action=None, dur=5.0, active=F
|
|
|
735
792
|
if isinstance(action, str): content.append(A(action, cls='inverse-link'))
|
|
736
793
|
else: content.append(action)
|
|
737
794
|
else: content.extend(c)
|
|
738
|
-
|
|
795
|
+
|
|
796
|
+
snackbar = Div(*content, cls=' '.join(classes), **kwargs)
|
|
797
|
+
|
|
798
|
+
# If duration is specified and there's an id, generate auto-hide script
|
|
799
|
+
if dur and 'id' in kwargs:
|
|
800
|
+
timeout_ms = int(dur * 1000)
|
|
801
|
+
toast_id = kwargs['id']
|
|
802
|
+
# Wait for Beer CSS module to load before calling ui()
|
|
803
|
+
script = Script(f"""
|
|
804
|
+
window.addEventListener('load', function() {{
|
|
805
|
+
function showToast() {{
|
|
806
|
+
if (typeof ui !== 'undefined') {{
|
|
807
|
+
try {{ ui('#{toast_id}', {timeout_ms}); }}
|
|
808
|
+
catch (e) {{ console.error('Toast error:', e.message); }}
|
|
809
|
+
}}
|
|
810
|
+
}}
|
|
811
|
+
setTimeout(showToast, 50);
|
|
812
|
+
}});
|
|
813
|
+
""")
|
|
814
|
+
return (snackbar, script)
|
|
815
|
+
|
|
816
|
+
return snackbar
|
|
739
817
|
|
|
740
818
|
def Snackbar(*c, **kwargs):
|
|
741
819
|
"""Alias for Toast component."""
|
|
742
820
|
return Toast(*c, **kwargs)
|
|
743
821
|
|
|
744
|
-
# %% ../nbs/02_components.ipynb
|
|
822
|
+
# %% ../nbs/02_components.ipynb 96
|
|
745
823
|
#| code-fold: true
|
|
746
824
|
class ContainerT(VEnum):
|
|
747
825
|
"""Container size options (BeerCSS). Most alias to 'responsive'; use 'expand' for full-width."""
|
|
@@ -752,7 +830,7 @@ class ContainerT(VEnum):
|
|
|
752
830
|
xl = 'responsive'
|
|
753
831
|
expand = 'responsive max'
|
|
754
832
|
|
|
755
|
-
# %% ../nbs/02_components.ipynb
|
|
833
|
+
# %% ../nbs/02_components.ipynb 98
|
|
756
834
|
#| code-fold: true
|
|
757
835
|
def _get_form_config(col: dict) -> dict:
|
|
758
836
|
"""Extract form config from column, with sensible defaults."""
|
|
@@ -828,7 +906,7 @@ def FormField(
|
|
|
828
906
|
**attrs
|
|
829
907
|
)
|
|
830
908
|
|
|
831
|
-
# %% ../nbs/02_components.ipynb
|
|
909
|
+
# %% ../nbs/02_components.ipynb 100
|
|
832
910
|
#| code-fold: true
|
|
833
911
|
from typing import Callable, Any
|
|
834
912
|
|
|
@@ -947,7 +1025,7 @@ def FormModal(
|
|
|
947
1025
|
cls="large-width"
|
|
948
1026
|
)
|
|
949
1027
|
|
|
950
|
-
# %% ../nbs/02_components.ipynb
|
|
1028
|
+
# %% ../nbs/02_components.ipynb 102
|
|
951
1029
|
#| code-fold: true
|
|
952
1030
|
def NavContainer(*li, title=None, brand=None, position='left', close_button=True, cls='active', id=None, **kwargs):
|
|
953
1031
|
"""Slide-out navigation drawer with header and close button."""
|
|
@@ -1004,7 +1082,7 @@ def BottomNav(*c, cls='bottom', size='s', **kwargs):
|
|
|
1004
1082
|
final_cls = f"{cls} {size_cls}".strip()
|
|
1005
1083
|
return Nav(*c, cls=final_cls, **kwargs)
|
|
1006
1084
|
|
|
1007
|
-
# %% ../nbs/02_components.ipynb
|
|
1085
|
+
# %% ../nbs/02_components.ipynb 105
|
|
1008
1086
|
#| code-fold: true
|
|
1009
1087
|
def NavSideBarHeader(*c, cls='', **kwargs):
|
|
1010
1088
|
"""Sidebar header section for menu buttons and branding."""
|
|
@@ -1017,33 +1095,76 @@ def NavSideBarLinks(*children, as_list=False, cls='', **kwargs):
|
|
|
1017
1095
|
return Ul(*children, cls=list_cls, **kwargs)
|
|
1018
1096
|
return Group(*children) if len(children) > 1 else (children[0] if children else Group())
|
|
1019
1097
|
|
|
1020
|
-
def NavSideBarContainer(*children, position='left', size='m', cls='', active=False,
|
|
1021
|
-
|
|
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
|
+
"""
|
|
1022
1118
|
base_cls = f"{size} {position} surface-container"
|
|
1023
1119
|
if active: base_cls += " active"
|
|
1024
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
|
+
|
|
1025
1127
|
return Nav(*children, cls=nav_cls, **kwargs)
|
|
1026
1128
|
|
|
1027
|
-
# %% ../nbs/02_components.ipynb
|
|
1129
|
+
# %% ../nbs/02_components.ipynb 107
|
|
1028
1130
|
#| code-fold: true
|
|
1029
1131
|
def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
|
|
1030
|
-
main_bg='surface', sidebar_id='app-sidebar', cls='', **kwargs):
|
|
1031
|
-
"""App layout
|
|
1032
|
-
main_content = []
|
|
1033
|
-
|
|
1034
|
-
if nav_bar:
|
|
1035
|
-
if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
|
|
1036
|
-
if 'sticky' not in nav_bar.attrs['cls']: nav_bar.attrs['cls'] += ' sticky top'
|
|
1037
|
-
main_content.append(nav_bar)
|
|
1132
|
+
main_bg='surface', sidebar_id='app-sidebar', main_id='main-content', cls='', **kwargs):
|
|
1133
|
+
"""App layout with HTMX SPA navigation.
|
|
1038
1134
|
|
|
1135
|
+
Args:
|
|
1136
|
+
main_id: ID for main content area (default 'main-content') - use as hx-target
|
|
1137
|
+
|
|
1138
|
+
HTMX SPA features:
|
|
1139
|
+
- hx-boost on sidebar automatically enhances all <a> links
|
|
1140
|
+
- hx-history-elt for back/forward button caching
|
|
1141
|
+
- Routes should check `req.headers` for HX-Request and return content only for HTMX requests
|
|
1142
|
+
|
|
1143
|
+
Usage:
|
|
1144
|
+
@rt("/dashboard")
|
|
1145
|
+
def dashboard(req):
|
|
1146
|
+
content = dashboard_content()
|
|
1147
|
+
if 'HX-Request' in req.headers: return content # HTMX swap
|
|
1148
|
+
return Layout(content) # Full page load
|
|
1149
|
+
"""
|
|
1150
|
+
# Build content wrapper with history caching
|
|
1151
|
+
content_wrapper = None
|
|
1039
1152
|
if content:
|
|
1040
|
-
|
|
1041
|
-
main_content.append(Main(*content, cls=container_cls))
|
|
1153
|
+
content_wrapper = Div(*content, id=main_id, hx_history_elt='true')
|
|
1042
1154
|
|
|
1155
|
+
# No sidebar - simple layout
|
|
1043
1156
|
if not sidebar and not sidebar_links:
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1157
|
+
result = []
|
|
1158
|
+
if nav_bar:
|
|
1159
|
+
if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
|
|
1160
|
+
if 'sticky' not in nav_bar.attrs['cls']: nav_bar.attrs['cls'] += ' sticky top'
|
|
1161
|
+
result.append(nav_bar)
|
|
1162
|
+
if content_wrapper:
|
|
1163
|
+
container_cls = stringify((container_size, 'padding', main_bg))
|
|
1164
|
+
result.append(Main(content_wrapper, cls=container_cls))
|
|
1165
|
+
return Div(*result, cls=cls, **kwargs) if result else Div(cls=cls, **kwargs)
|
|
1166
|
+
|
|
1167
|
+
# Sidebar layout with hx-boost
|
|
1047
1168
|
sidebar_children = [NavSideBarHeader(NavToggleButton(f"#{sidebar_id}"))]
|
|
1048
1169
|
|
|
1049
1170
|
if sidebar_links: sidebar_children.extend(sidebar_links)
|
|
@@ -1053,23 +1174,23 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
|
|
|
1053
1174
|
|
|
1054
1175
|
nav_rail = NavSideBarContainer(*sidebar_children, position='left', size='l', id=sidebar_id)
|
|
1055
1176
|
|
|
1056
|
-
navbar_elem = None
|
|
1057
|
-
content_items = []
|
|
1058
|
-
for item in main_content:
|
|
1059
|
-
if hasattr(item, 'tag') and item.tag == 'nav': navbar_elem = item
|
|
1060
|
-
else: content_items.append(item)
|
|
1061
|
-
|
|
1062
1177
|
layout_children = []
|
|
1063
|
-
|
|
1178
|
+
|
|
1179
|
+
if nav_bar:
|
|
1180
|
+
if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
|
|
1181
|
+
if 'sticky' not in nav_bar.attrs['cls']: nav_bar.attrs['cls'] += ' sticky top'
|
|
1182
|
+
layout_children.append(nav_bar)
|
|
1183
|
+
|
|
1064
1184
|
layout_children.append(nav_rail)
|
|
1065
|
-
|
|
1185
|
+
|
|
1186
|
+
if content_wrapper:
|
|
1066
1187
|
container_cls = stringify((container_size, 'round', 'elevate', 'margin'))
|
|
1067
|
-
layout_children.append(Main(
|
|
1188
|
+
layout_children.append(Main(content_wrapper, cls=container_cls))
|
|
1068
1189
|
|
|
1069
1190
|
final_cls = f"surface-container {cls}".strip() if cls else "surface-container"
|
|
1070
1191
|
return Div(*layout_children, cls=final_cls, **kwargs)
|
|
1071
1192
|
|
|
1072
|
-
# %% ../nbs/02_components.ipynb
|
|
1193
|
+
# %% ../nbs/02_components.ipynb 113
|
|
1073
1194
|
#| code-fold: true
|
|
1074
1195
|
class TextT(VEnum):
|
|
1075
1196
|
"""Text styles using BeerCSS typography classes."""
|
|
@@ -1101,7 +1222,7 @@ class TextPresets(VEnum):
|
|
|
1101
1222
|
primary_link = 'link primary-text'
|
|
1102
1223
|
muted_link = 'link secondary-text'
|
|
1103
1224
|
|
|
1104
|
-
# %% ../nbs/02_components.ipynb
|
|
1225
|
+
# %% ../nbs/02_components.ipynb 114
|
|
1105
1226
|
#| code-fold: true
|
|
1106
1227
|
def CodeSpan(*c, cls=(), **kwargs):
|
|
1107
1228
|
"""Inline code snippet."""
|
|
@@ -1157,7 +1278,7 @@ def Sup(*c, cls=(), **kwargs):
|
|
|
1157
1278
|
cls_str = stringify(cls) if cls else None
|
|
1158
1279
|
return fc.Sup(*c, cls=cls_str, **kwargs) if cls_str else fc.Sup(*c, **kwargs)
|
|
1159
1280
|
|
|
1160
|
-
# %% ../nbs/02_components.ipynb
|
|
1281
|
+
# %% ../nbs/02_components.ipynb 116
|
|
1161
1282
|
#| code-fold: true
|
|
1162
1283
|
def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str = ''):
|
|
1163
1284
|
"""Collapsible FAQ item using details/summary.
|
|
@@ -1175,7 +1296,7 @@ def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str
|
|
|
1175
1296
|
Summary(Article(Nav(Div(question, cls=f"max bold {question_cls}".strip()), I("expand_more")), cls="round surface-variant border no-elevate")),
|
|
1176
1297
|
Article(P(answer, cls=f"secondary-text {answer_cls}".strip()), cls="round border padding"))
|
|
1177
1298
|
|
|
1178
|
-
# %% ../nbs/02_components.ipynb
|
|
1299
|
+
# %% ../nbs/02_components.ipynb 120
|
|
1179
1300
|
#| code-fold: true
|
|
1180
1301
|
def CookiesBanner(message='We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.',
|
|
1181
1302
|
accept_text='Accept', decline_text='Decline', settings_text=None, policy_link='/cookies', policy_text='Learn more',
|
|
@@ -31,7 +31,7 @@ beer_hdrs = (
|
|
|
31
31
|
Script(src=HEADER_URLS["mdc_js"], type='module'),
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
# %% ../nbs/01_core.ipynb
|
|
34
|
+
# %% ../nbs/01_core.ipynb 7
|
|
35
35
|
#| code-fold: true
|
|
36
36
|
# All BeerCSS color names
|
|
37
37
|
COLOR_NAMES = ['amber', 'blue', 'blue_grey', 'brown', 'cyan', 'deep_orange', 'deep_purple',
|
|
@@ -88,7 +88,7 @@ ALL_HELPERS = (SIZES + WIDTH_HEIGHT + ELEVATES + DIRECTIONS + FORMS + MARGINS +
|
|
|
88
88
|
POSITIONS + RESPONSIVE + ALIGNMENTS + BLURS + OPACITIES + SHADOWS + SPACES +
|
|
89
89
|
RIPPLES + SCROLLS + WAVES + ZOOMS + THEME_HELPERS + TYPOGRAPHY + TRIGGERS + COLOR_HELPERS)
|
|
90
90
|
|
|
91
|
-
# %% ../nbs/01_core.ipynb
|
|
91
|
+
# %% ../nbs/01_core.ipynb 10
|
|
92
92
|
#| code-fold: true
|
|
93
93
|
class _ThemeChain:
|
|
94
94
|
"""Internal class for building themed headers"""
|
|
@@ -142,14 +142,19 @@ class _ThemeChain:
|
|
|
142
142
|
}};
|
|
143
143
|
window.toggleNav = function(selector) {{
|
|
144
144
|
const nav = document.querySelector(selector);
|
|
145
|
-
if (nav) {{
|
|
145
|
+
if (nav) {{
|
|
146
|
+
nav.classList.toggle('max');
|
|
147
|
+
const isOpen = nav.classList.contains('max');
|
|
148
|
+
const btn = nav.querySelector('button i, button .icon');
|
|
149
|
+
if (btn) btn.textContent = isOpen ? 'menu_open' : 'menu';
|
|
150
|
+
}}
|
|
146
151
|
}};
|
|
147
152
|
''')
|
|
148
153
|
hdrs.append(theme_script)
|
|
149
154
|
hdrs.append(Title(title))
|
|
150
155
|
return tuple(hdrs)
|
|
151
156
|
|
|
152
|
-
# %% ../nbs/01_core.ipynb
|
|
157
|
+
# %% ../nbs/01_core.ipynb 11
|
|
153
158
|
#| code-fold: true
|
|
154
159
|
class _ThemeNamespace:
|
|
155
160
|
"""Namespace providing color properties that return _ThemeChain instances"""
|
|
@@ -208,7 +213,7 @@ class _ThemeNamespace:
|
|
|
208
213
|
|
|
209
214
|
MatTheme = _ThemeNamespace()
|
|
210
215
|
|
|
211
|
-
# %% ../nbs/01_core.ipynb
|
|
216
|
+
# %% ../nbs/01_core.ipynb 15
|
|
212
217
|
#| code-fold: true
|
|
213
218
|
class BeerCssChain:
|
|
214
219
|
"""Base class for chaining Beer CSS helper classes together"""
|