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.
- {fh_matui-0.9.13/fh_matui.egg-info → fh_matui-0.9.15}/PKG-INFO +1 -1
- fh_matui-0.9.15/fh_matui/__init__.py +1 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/_modidx.py +1 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/components.py +176 -53
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/core.py +5 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/datatable.py +33 -5
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/foundations.py +3 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15/fh_matui.egg-info}/PKG-INFO +1 -1
- {fh_matui-0.9.13 → fh_matui-0.9.15}/settings.ini +1 -1
- fh_matui-0.9.13/fh_matui/__init__.py +0 -1
- {fh_matui-0.9.13 → fh_matui-0.9.15}/LICENSE +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/MANIFEST.in +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/README.md +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/app_pages.py +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui/web_pages.py +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/SOURCES.txt +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/dependency_links.txt +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/entry_points.txt +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/not-zip-safe +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/requires.txt +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/fh_matui.egg-info/top_level.txt +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/pyproject.toml +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/setup.cfg +0 -0
- {fh_matui-0.9.13 → fh_matui-0.9.15}/setup.py +0 -0
|
@@ -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', '
|
|
14
|
-
'CodeSpan', 'CodeBlock', 'Blockquote', 'Q', 'Em', 'Strong', 'Small', 'Mark', 'Abbr', 'Sub',
|
|
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,
|
|
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([
|
|
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
|
-
#
|
|
203
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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', '
|
|
1188
|
-
|
|
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
|
|
1312
|
+
final_cls = f"surface {cls}".strip() if cls else "surface"
|
|
1191
1313
|
return Div(*layout_children, cls=final_cls, **kwargs)
|
|
1192
1314
|
|
|
1193
|
-
|
|
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
|
|
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
|
|
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
|
|
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', '
|
|
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
|
-
"
|
|
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=
|
|
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
|
|
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 +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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|