fh-matui 0.9.14__py3-none-any.whl → 0.9.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fh_matui/__init__.py +1 -1
- fh_matui/components.py +144 -44
- fh_matui/datatable.py +32 -5
- {fh_matui-0.9.14.dist-info → fh_matui-0.9.15.dist-info}/METADATA +1 -1
- fh_matui-0.9.15.dist-info/RECORD +14 -0
- fh_matui-0.9.14.dist-info/RECORD +0 -14
- {fh_matui-0.9.14.dist-info → fh_matui-0.9.15.dist-info}/WHEEL +0 -0
- {fh_matui-0.9.14.dist-info → fh_matui-0.9.15.dist-info}/entry_points.txt +0 -0
- {fh_matui-0.9.14.dist-info → fh_matui-0.9.15.dist-info}/licenses/LICENSE +0 -0
- {fh_matui-0.9.14.dist-info → fh_matui-0.9.15.dist-info}/top_level.txt +0 -0
fh_matui/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.9.
|
|
1
|
+
__version__ = "0.9.14"
|
fh_matui/components.py
CHANGED
|
@@ -163,28 +163,20 @@ def _snap_to_valid_cols(n: int) -> int:
|
|
|
163
163
|
return 12
|
|
164
164
|
|
|
165
165
|
|
|
166
|
-
def _wrap_grid_children(cells,
|
|
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
|
|
@@ -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."""
|
|
@@ -1126,7 +1205,7 @@ def NavSideBarContainer(*children, position='left', size='m', cls='', active=Fal
|
|
|
1126
1205
|
|
|
1127
1206
|
return Nav(*children, cls=nav_cls, **kwargs)
|
|
1128
1207
|
|
|
1129
|
-
# %% ../nbs/02_components.ipynb
|
|
1208
|
+
# %% ../nbs/02_components.ipynb 109
|
|
1130
1209
|
#| code-fold: true
|
|
1131
1210
|
def Page(*c, active=True, position=None, cls='', **kwargs):
|
|
1132
1211
|
"""BeerCSS animated page container.
|
|
@@ -1150,7 +1229,7 @@ def Page(*c, active=True, position=None, cls='', **kwargs):
|
|
|
1150
1229
|
|
|
1151
1230
|
def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
|
|
1152
1231
|
main_bg='surface', sidebar_id='app-sidebar', main_id='main-content', cls='', **kwargs):
|
|
1153
|
-
"""App layout with HTMX SPA navigation.
|
|
1232
|
+
"""App layout with HTMX SPA navigation and responsive mobile support.
|
|
1154
1233
|
|
|
1155
1234
|
Args:
|
|
1156
1235
|
main_id: ID for main content area (default 'main-content') - use as hx-target
|
|
@@ -1160,6 +1239,11 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
|
|
|
1160
1239
|
- hx-history-elt for back/forward button caching
|
|
1161
1240
|
- Routes should check `req.headers` for HX-Request and return content only for HTMX requests
|
|
1162
1241
|
|
|
1242
|
+
Mobile Responsive:
|
|
1243
|
+
- Desktop (>992px): Left sidebar visible, bottom nav hidden
|
|
1244
|
+
- Mobile (≤992px): Bottom nav visible, sidebar hidden
|
|
1245
|
+
- Both navigations share the same sidebar_links for consistent behavior
|
|
1246
|
+
|
|
1163
1247
|
Usage:
|
|
1164
1248
|
@rt("/dashboard")
|
|
1165
1249
|
def dashboard(req):
|
|
@@ -1193,9 +1277,24 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
|
|
|
1193
1277
|
if is_listy(sidebar): sidebar_children.extend(sidebar)
|
|
1194
1278
|
else: sidebar_children.append(sidebar)
|
|
1195
1279
|
|
|
1196
|
-
nav_rail = NavSideBarContainer(*sidebar_children, position='left', size='l', id=sidebar_id)
|
|
1280
|
+
nav_rail = NavSideBarContainer(*sidebar_children, position='left', size='l', id=sidebar_id, cls='desktop-nav')
|
|
1281
|
+
|
|
1282
|
+
# Mobile bottom navigation with same links
|
|
1283
|
+
bottom_nav_children = sidebar_links if sidebar_links else (list(sidebar) if is_listy(sidebar) else [sidebar] if sidebar else [])
|
|
1284
|
+
nav_bottom = BottomNav(*bottom_nav_children, cls='bottom mobile-nav',
|
|
1285
|
+
hx_boost='true', hx_target=f'#{main_id}', hx_push_url='true')
|
|
1286
|
+
|
|
1287
|
+
# Responsive CSS for desktop/mobile navigation switching
|
|
1288
|
+
# Only hide elements when needed - don't override Beer CSS flex layout on desktop
|
|
1289
|
+
responsive_nav_css = Style("""
|
|
1290
|
+
.mobile-nav { display: none !important; }
|
|
1291
|
+
@media (max-width: 992px) {
|
|
1292
|
+
.mobile-nav { display: flex !important; }
|
|
1293
|
+
.desktop-nav { display: none !important; }
|
|
1294
|
+
}
|
|
1295
|
+
""")
|
|
1197
1296
|
|
|
1198
|
-
layout_children = []
|
|
1297
|
+
layout_children = [responsive_nav_css]
|
|
1199
1298
|
|
|
1200
1299
|
if nav_bar:
|
|
1201
1300
|
if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
|
|
@@ -1203,9 +1302,10 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
|
|
|
1203
1302
|
layout_children.append(nav_bar)
|
|
1204
1303
|
|
|
1205
1304
|
layout_children.append(nav_rail)
|
|
1305
|
+
layout_children.append(nav_bottom)
|
|
1206
1306
|
|
|
1207
1307
|
if content_wrapper:
|
|
1208
|
-
container_cls = stringify((container_size, 'surface-container', 'round', 'padding', 'bottom-margin'))
|
|
1308
|
+
container_cls = stringify((container_size, 'surface-container', 'round', 'padding', 'bottom-margin', 'horizontal-margin'))
|
|
1209
1309
|
page_content = Page(content_wrapper, id=main_id)
|
|
1210
1310
|
layout_children.append(Main(page_content, cls=container_cls))
|
|
1211
1311
|
|
|
@@ -1213,7 +1313,7 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
|
|
|
1213
1313
|
return Div(*layout_children, cls=final_cls, **kwargs)
|
|
1214
1314
|
|
|
1215
1315
|
|
|
1216
|
-
# %% ../nbs/02_components.ipynb
|
|
1316
|
+
# %% ../nbs/02_components.ipynb 115
|
|
1217
1317
|
#| code-fold: true
|
|
1218
1318
|
class TextT(VEnum):
|
|
1219
1319
|
"""Text styles using BeerCSS typography classes."""
|
|
@@ -1245,7 +1345,7 @@ class TextPresets(VEnum):
|
|
|
1245
1345
|
primary_link = 'link primary-text'
|
|
1246
1346
|
muted_link = 'link secondary-text'
|
|
1247
1347
|
|
|
1248
|
-
# %% ../nbs/02_components.ipynb
|
|
1348
|
+
# %% ../nbs/02_components.ipynb 116
|
|
1249
1349
|
#| code-fold: true
|
|
1250
1350
|
def CodeSpan(*c, cls=(), **kwargs):
|
|
1251
1351
|
"""Inline code snippet."""
|
|
@@ -1301,7 +1401,7 @@ def Sup(*c, cls=(), **kwargs):
|
|
|
1301
1401
|
cls_str = stringify(cls) if cls else None
|
|
1302
1402
|
return fc.Sup(*c, cls=cls_str, **kwargs) if cls_str else fc.Sup(*c, **kwargs)
|
|
1303
1403
|
|
|
1304
|
-
# %% ../nbs/02_components.ipynb
|
|
1404
|
+
# %% ../nbs/02_components.ipynb 118
|
|
1305
1405
|
#| code-fold: true
|
|
1306
1406
|
def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str = ''):
|
|
1307
1407
|
"""Collapsible FAQ item using details/summary.
|
|
@@ -1319,7 +1419,7 @@ def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str
|
|
|
1319
1419
|
Summary(Article(Nav(Div(question, cls=f"max bold {question_cls}".strip()), I("expand_more")), cls="round surface-variant border no-elevate")),
|
|
1320
1420
|
Article(P(answer, cls=f"secondary-text {answer_cls}".strip()), cls="round border padding"))
|
|
1321
1421
|
|
|
1322
|
-
# %% ../nbs/02_components.ipynb
|
|
1422
|
+
# %% ../nbs/02_components.ipynb 122
|
|
1323
1423
|
#| code-fold: true
|
|
1324
1424
|
def CookiesBanner(message='We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.',
|
|
1325
1425
|
accept_text='Accept', decline_text='Decline', settings_text=None, policy_link='/cookies', policy_text='Learn more',
|
fh_matui/datatable.py
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_datatable.ipynb.
|
|
4
4
|
|
|
5
5
|
# %% auto 0
|
|
6
|
-
__all__ = ['PAGE_SIZES', '
|
|
6
|
+
__all__ = ['PAGE_SIZES', 'TABLE_STYLES', 'TABLE_SPACES', 'TABLE_ALIGNS', 'logger', 'table_state_from_request', 'DataTable',
|
|
7
|
+
'CrudContext', 'DataTableResource']
|
|
7
8
|
|
|
8
9
|
# %% ../nbs/05_datatable.ipynb 2
|
|
9
10
|
from fastcore.utils import *
|
|
@@ -28,6 +29,11 @@ from dataclasses import asdict, is_dataclass
|
|
|
28
29
|
# Default page size options
|
|
29
30
|
PAGE_SIZES = [5, 10, 20, 50]
|
|
30
31
|
|
|
32
|
+
# BeerCSS table styles
|
|
33
|
+
TABLE_STYLES = ['border', 'stripes', 'min', 'fixed'] # Can combine: 'border stripes'
|
|
34
|
+
TABLE_SPACES = ['no-space', 'small-space', 'medium-space', 'large-space']
|
|
35
|
+
TABLE_ALIGNS = ['left-align', 'right-align', 'center-align']
|
|
36
|
+
|
|
31
37
|
|
|
32
38
|
def _to_dict(obj: Any) -> dict:
|
|
33
39
|
"""
|
|
@@ -207,9 +213,20 @@ def DataTable(
|
|
|
207
213
|
page_sizes: list = None,
|
|
208
214
|
search_placeholder: str = 'Search...',
|
|
209
215
|
create_label: str = 'New Record',
|
|
210
|
-
empty_message: str = 'No records match the current filters.'
|
|
216
|
+
empty_message: str = 'No records match the current filters.',
|
|
217
|
+
# BeerCSS table styling options
|
|
218
|
+
table_style: str = 'border', # border, stripes, min, fixed (can combine: 'border stripes')
|
|
219
|
+
space: str = 'small-space', # no-space, small-space, medium-space, large-space
|
|
220
|
+
align: str = None # left-align, right-align, center-align (None = default left)
|
|
211
221
|
):
|
|
212
|
-
"
|
|
222
|
+
"""
|
|
223
|
+
Generic data table with server-side pagination, search, and row actions.
|
|
224
|
+
|
|
225
|
+
Table Styling (BeerCSS):
|
|
226
|
+
table_style: Table visual style - 'border', 'stripes', 'min', 'fixed' or combine like 'border stripes'
|
|
227
|
+
space: Row spacing - 'no-space', 'small-space', 'medium-space', 'large-space'
|
|
228
|
+
align: Text alignment - 'left-align', 'right-align', 'center-align' (None for default)
|
|
229
|
+
"""
|
|
213
230
|
# Defaults
|
|
214
231
|
crud_ops = crud_ops or {"create": False, "update": False, "delete": False}
|
|
215
232
|
crud_enabled = crud_enabled or {k: bool(v) for k, v in crud_ops.items() if k != 'custom_actions'}
|
|
@@ -313,6 +330,16 @@ def DataTable(
|
|
|
313
330
|
cls="max"
|
|
314
331
|
)
|
|
315
332
|
|
|
333
|
+
# Compose table classes from BeerCSS styling options
|
|
334
|
+
table_classes = []
|
|
335
|
+
if table_style:
|
|
336
|
+
table_classes.append(table_style)
|
|
337
|
+
if space:
|
|
338
|
+
table_classes.append(space)
|
|
339
|
+
if align:
|
|
340
|
+
table_classes.append(align)
|
|
341
|
+
table_cls = ' '.join(table_classes) if table_classes else None
|
|
342
|
+
|
|
316
343
|
# Build table with proper Thead/Tbody structure
|
|
317
344
|
data_table = Table(
|
|
318
345
|
Thead(Tr(*[Th(label) for label in header_labels])),
|
|
@@ -320,7 +347,7 @@ def DataTable(
|
|
|
320
347
|
Tr(*[Td(row_dict.get(key, '')) for key in header_keys])
|
|
321
348
|
for row_dict in table_rows
|
|
322
349
|
]),
|
|
323
|
-
cls=
|
|
350
|
+
cls=table_cls
|
|
324
351
|
) if data else Div(empty_message, cls="center-align padding")
|
|
325
352
|
|
|
326
353
|
# Footer section with page size selector and pagination
|
|
@@ -341,7 +368,7 @@ def DataTable(
|
|
|
341
368
|
# Single row when no pagination needed
|
|
342
369
|
footer = page_info_row
|
|
343
370
|
|
|
344
|
-
return
|
|
371
|
+
return Div(
|
|
345
372
|
Div(id=feedback_id), # Feedback container for modals/toasts
|
|
346
373
|
Div(
|
|
347
374
|
search_form,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
fh_matui/__init__.py,sha256=AXW1LY5wrgbjQVfPOZJRcsfowYQAXor5ryaBdvw-SJo,24
|
|
2
|
+
fh_matui/_modidx.py,sha256=r9X_k4ptjzUPy6u6AS6411Qn0Fq0NuxMyV5PrpMH2oA,24032
|
|
3
|
+
fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
|
|
4
|
+
fh_matui/components.py,sha256=_rndyhubrazX6xTr2EnYGQPkGbvCy6VGUFLUfV_1lKg,58416
|
|
5
|
+
fh_matui/core.py,sha256=fMSw0fHvUTYt1Qd1DzaWDS4xWIDbkyUCcr-LIz2IJ5k,11062
|
|
6
|
+
fh_matui/datatable.py,sha256=9jiVu5YU2tc3-4PKhHxIoVEjwwikVCyJhPGPvWIVgzQ,42558
|
|
7
|
+
fh_matui/foundations.py,sha256=xiQOeyV4FhNzPpw6mbtIrPWMyzlMi3BENo41IlGnAz8,1880
|
|
8
|
+
fh_matui/web_pages.py,sha256=at_M34Vxc1i9O0ukS41PaRJntkXXDzMqyzlcrgESUcw,35103
|
|
9
|
+
fh_matui-0.9.15.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
|
|
10
|
+
fh_matui-0.9.15.dist-info/METADATA,sha256=E71WzNH6S2mhg8LnE4b9tTbrI7D5zsMtF6hTZxljFQM,10491
|
|
11
|
+
fh_matui-0.9.15.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
+
fh_matui-0.9.15.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
|
|
13
|
+
fh_matui-0.9.15.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
|
|
14
|
+
fh_matui-0.9.15.dist-info/RECORD,,
|
fh_matui-0.9.14.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
fh_matui/__init__.py,sha256=s9smkhMiLTJ6_VCSfo7J6aeWBGIlYjSsTDGrVprZjrI,24
|
|
2
|
-
fh_matui/_modidx.py,sha256=r9X_k4ptjzUPy6u6AS6411Qn0Fq0NuxMyV5PrpMH2oA,24032
|
|
3
|
-
fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
|
|
4
|
-
fh_matui/components.py,sha256=Lgk8Qazjx3TrrkEcGxiUbgPE-4676wL2fcs9nmhv1Cg,54179
|
|
5
|
-
fh_matui/core.py,sha256=fMSw0fHvUTYt1Qd1DzaWDS4xWIDbkyUCcr-LIz2IJ5k,11062
|
|
6
|
-
fh_matui/datatable.py,sha256=cDk_R9A1WTWa7fxDBBAZsnFU36ko_ml94HxmmbL_ENc,41248
|
|
7
|
-
fh_matui/foundations.py,sha256=xiQOeyV4FhNzPpw6mbtIrPWMyzlMi3BENo41IlGnAz8,1880
|
|
8
|
-
fh_matui/web_pages.py,sha256=at_M34Vxc1i9O0ukS41PaRJntkXXDzMqyzlcrgESUcw,35103
|
|
9
|
-
fh_matui-0.9.14.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
|
|
10
|
-
fh_matui-0.9.14.dist-info/METADATA,sha256=JGk9Wq2RxIQKXxyatsmyVpGKwbxsmU6n1W-hx3rJrjs,10491
|
|
11
|
-
fh_matui-0.9.14.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
-
fh_matui-0.9.14.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
|
|
13
|
-
fh_matui-0.9.14.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
|
|
14
|
-
fh_matui-0.9.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|