fh-matui 0.9.9__py3-none-any.whl → 0.9.11__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.8"
1
+ __version__ = "0.9.10"
fh_matui/components.py CHANGED
@@ -331,9 +331,14 @@ def NavBar(*children, brand=None, sticky=False, cls='', **kwargs):
331
331
  return Nav(*children, cls=f"padding {nav_cls}", **kwargs)
332
332
 
333
333
  # %% ../nbs/02_components.ipynb 35
334
- def Modal(*c, id=None, footer=None, active=False, overlay=True, cls=(), **kwargs):
335
- """BeerCSS modal dialog with optional overlay and footer."""
334
+ def Modal(*c, id=None, footer=None, active=False, overlay='default', position=None, cls=(), **kwargs):
335
+ """BeerCSS modal dialog with position and overlay options."""
336
336
  modal_cls = normalize_tokens(cls)
337
+
338
+ # Add position class if specified
339
+ if position in ['left', 'right', 'top', 'bottom']:
340
+ modal_cls.append(position)
341
+
337
342
  if active:
338
343
  modal_cls.append('active')
339
344
 
@@ -349,9 +354,22 @@ def Modal(*c, id=None, footer=None, active=False, overlay=True, cls=(), **kwargs
349
354
  # Create the dialog
350
355
  dialog = Dialog(*children, id=id, cls=cls_str, **kwargs)
351
356
 
352
- if overlay:
353
- # Return overlay + dialog as separate elements that BeerCSS can manage
354
- overlay_cls = "overlay blur"
357
+ # Handle overlay
358
+ if overlay and overlay not in [False, None]:
359
+ overlay_classes = ['overlay']
360
+
361
+ # Map overlay parameter to BeerCSS classes
362
+ if overlay == 'blur':
363
+ overlay_classes.append('blur')
364
+ elif overlay == 'small-blur':
365
+ overlay_classes.append('small-blur')
366
+ elif overlay == 'medium-blur':
367
+ overlay_classes.append('medium-blur')
368
+ elif overlay == 'large-blur':
369
+ overlay_classes.append('large-blur')
370
+ # 'default' means plain overlay with no blur class
371
+
372
+ overlay_cls = ' '.join(overlay_classes)
355
373
  if active:
356
374
  overlay_cls += " active"
357
375
 
@@ -717,15 +735,14 @@ def Toolbar(*items, cls='', elevate='large', fill=True, **kwargs):
717
735
  if cls: classes.append(cls)
718
736
  return Nav(*items, cls=' '.join(classes), **kwargs)
719
737
 
720
- # %% ../nbs/02_components.ipynb 92
738
+ # %% ../nbs/02_components.ipynb 93
721
739
  #| code-fold: true
722
- def Toast(*c, cls='', position='top', variant='', action=None, dur=5.0, active=False, **kwargs):
723
- """BeerCSS snackbar/toast notification with position and variant options."""
740
+ def Toast(*c, cls='', position='top', variant='', action=None, active=False, dur=None, **kwargs):
741
+ """BeerCSS snackbar/toast notification with position ('top' or 'bottom') and variant options."""
724
742
  classes = ['snackbar']
725
743
  if variant: classes.append(variant)
726
- if position:
727
- position_map = {'top': 'bottom', 'bottom': 'top', 'left': 'right', 'right': 'left'}
728
- classes.append(position_map.get(position, position))
744
+ if position in ['top', 'bottom']:
745
+ classes.append(position)
729
746
  if active: classes.append('active')
730
747
  if cls: classes.append(cls)
731
748
 
@@ -735,13 +752,34 @@ def Toast(*c, cls='', position='top', variant='', action=None, dur=5.0, active=F
735
752
  if isinstance(action, str): content.append(A(action, cls='inverse-link'))
736
753
  else: content.append(action)
737
754
  else: content.extend(c)
738
- return Div(*content, cls=' '.join(classes), **kwargs)
755
+
756
+ snackbar = Div(*content, cls=' '.join(classes), **kwargs)
757
+
758
+ # If duration is specified and there's an id, generate auto-hide script
759
+ if dur and 'id' in kwargs:
760
+ timeout_ms = int(dur * 1000)
761
+ toast_id = kwargs['id']
762
+ # Wait for Beer CSS module to load before calling ui()
763
+ script = Script(f"""
764
+ window.addEventListener('load', function() {{
765
+ function showToast() {{
766
+ if (typeof ui !== 'undefined') {{
767
+ try {{ ui('#{toast_id}', {timeout_ms}); }}
768
+ catch (e) {{ console.error('Toast error:', e.message); }}
769
+ }}
770
+ }}
771
+ setTimeout(showToast, 50);
772
+ }});
773
+ """)
774
+ return (snackbar, script)
775
+
776
+ return snackbar
739
777
 
740
778
  def Snackbar(*c, **kwargs):
741
779
  """Alias for Toast component."""
742
780
  return Toast(*c, **kwargs)
743
781
 
744
- # %% ../nbs/02_components.ipynb 94
782
+ # %% ../nbs/02_components.ipynb 96
745
783
  #| code-fold: true
746
784
  class ContainerT(VEnum):
747
785
  """Container size options (BeerCSS). Most alias to 'responsive'; use 'expand' for full-width."""
@@ -752,7 +790,7 @@ class ContainerT(VEnum):
752
790
  xl = 'responsive'
753
791
  expand = 'responsive max'
754
792
 
755
- # %% ../nbs/02_components.ipynb 96
793
+ # %% ../nbs/02_components.ipynb 98
756
794
  #| code-fold: true
757
795
  def _get_form_config(col: dict) -> dict:
758
796
  """Extract form config from column, with sensible defaults."""
@@ -828,7 +866,7 @@ def FormField(
828
866
  **attrs
829
867
  )
830
868
 
831
- # %% ../nbs/02_components.ipynb 98
869
+ # %% ../nbs/02_components.ipynb 100
832
870
  #| code-fold: true
833
871
  from typing import Callable, Any
834
872
 
@@ -947,7 +985,7 @@ def FormModal(
947
985
  cls="large-width"
948
986
  )
949
987
 
950
- # %% ../nbs/02_components.ipynb 100
988
+ # %% ../nbs/02_components.ipynb 102
951
989
  #| code-fold: true
952
990
  def NavContainer(*li, title=None, brand=None, position='left', close_button=True, cls='active', id=None, **kwargs):
953
991
  """Slide-out navigation drawer with header and close button."""
@@ -1004,7 +1042,7 @@ def BottomNav(*c, cls='bottom', size='s', **kwargs):
1004
1042
  final_cls = f"{cls} {size_cls}".strip()
1005
1043
  return Nav(*c, cls=final_cls, **kwargs)
1006
1044
 
1007
- # %% ../nbs/02_components.ipynb 103
1045
+ # %% ../nbs/02_components.ipynb 105
1008
1046
  #| code-fold: true
1009
1047
  def NavSideBarHeader(*c, cls='', **kwargs):
1010
1048
  """Sidebar header section for menu buttons and branding."""
@@ -1024,7 +1062,7 @@ def NavSideBarContainer(*children, position='left', size='m', cls='', active=Fal
1024
1062
  nav_cls = f"{base_cls} {cls}".strip()
1025
1063
  return Nav(*children, cls=nav_cls, **kwargs)
1026
1064
 
1027
- # %% ../nbs/02_components.ipynb 105
1065
+ # %% ../nbs/02_components.ipynb 107
1028
1066
  #| code-fold: true
1029
1067
  def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
1030
1068
  main_bg='surface', sidebar_id='app-sidebar', cls='', **kwargs):
@@ -1069,7 +1107,7 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
1069
1107
  final_cls = f"surface-container {cls}".strip() if cls else "surface-container"
1070
1108
  return Div(*layout_children, cls=final_cls, **kwargs)
1071
1109
 
1072
- # %% ../nbs/02_components.ipynb 109
1110
+ # %% ../nbs/02_components.ipynb 111
1073
1111
  #| code-fold: true
1074
1112
  class TextT(VEnum):
1075
1113
  """Text styles using BeerCSS typography classes."""
@@ -1101,7 +1139,7 @@ class TextPresets(VEnum):
1101
1139
  primary_link = 'link primary-text'
1102
1140
  muted_link = 'link secondary-text'
1103
1141
 
1104
- # %% ../nbs/02_components.ipynb 110
1142
+ # %% ../nbs/02_components.ipynb 112
1105
1143
  #| code-fold: true
1106
1144
  def CodeSpan(*c, cls=(), **kwargs):
1107
1145
  """Inline code snippet."""
@@ -1157,7 +1195,7 @@ def Sup(*c, cls=(), **kwargs):
1157
1195
  cls_str = stringify(cls) if cls else None
1158
1196
  return fc.Sup(*c, cls=cls_str, **kwargs) if cls_str else fc.Sup(*c, **kwargs)
1159
1197
 
1160
- # %% ../nbs/02_components.ipynb 112
1198
+ # %% ../nbs/02_components.ipynb 114
1161
1199
  #| code-fold: true
1162
1200
  def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str = ''):
1163
1201
  """Collapsible FAQ item using details/summary.
@@ -1175,7 +1213,7 @@ def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str
1175
1213
  Summary(Article(Nav(Div(question, cls=f"max bold {question_cls}".strip()), I("expand_more")), cls="round surface-variant border no-elevate")),
1176
1214
  Article(P(answer, cls=f"secondary-text {answer_cls}".strip()), cls="round border padding"))
1177
1215
 
1178
- # %% ../nbs/02_components.ipynb 116
1216
+ # %% ../nbs/02_components.ipynb 118
1179
1217
  #| code-fold: true
1180
1218
  def CookiesBanner(message='We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.',
1181
1219
  accept_text='Accept', decline_text='Decline', settings_text=None, policy_link='/cookies', policy_text='Learn more',
fh_matui/datatable.py CHANGED
@@ -123,6 +123,7 @@ def _action_menu(
123
123
  row: dict,
124
124
  row_id: Any,
125
125
  crud_ops: dict,
126
+ crud_enabled: dict,
126
127
  base_route: str,
127
128
  search: str,
128
129
  page: int,
@@ -153,14 +154,14 @@ def _action_menu(
153
154
  )
154
155
 
155
156
  items = []
156
- # View is conditional (read operation)
157
- if crud_ops.get("view", True):
157
+ # View is conditional (read operation) - use crud_enabled for callable support
158
+ if crud_enabled.get("view", True):
158
159
  items.append(action_item("View", "visibility", "view"))
159
160
 
160
- if crud_ops.get("update", False):
161
+ if crud_enabled.get("update", False):
161
162
  items.append(action_item("Edit", "edit", "edit"))
162
163
 
163
- if crud_ops.get("delete", False):
164
+ if crud_enabled.get("delete", False):
164
165
  items.append(action_item("Delete", "delete", "delete", confirm="Delete this record?"))
165
166
 
166
167
  # Custom actions
@@ -197,6 +198,7 @@ def DataTable(
197
198
  search: str = '',
198
199
  columns: list[dict] = None,
199
200
  crud_ops: dict = None,
201
+ crud_enabled: dict = None,
200
202
  base_route: str = '',
201
203
  row_id_field: str = 'id',
202
204
  title: str = 'Records',
@@ -209,6 +211,7 @@ def DataTable(
209
211
  "Generic data table with server-side pagination, search, and row actions."
210
212
  # Defaults
211
213
  crud_ops = crud_ops or {"create": False, "update": False, "delete": False}
214
+ crud_enabled = crud_enabled or {k: bool(v) for k, v in crud_ops.items() if k != 'custom_actions'}
212
215
  page_sizes = page_sizes or PAGE_SIZES
213
216
  container_id = container_id or f"crud-table-{base_route.replace('/', '-').strip('-')}"
214
217
  feedback_id = f"{container_id}-feedback"
@@ -227,7 +230,6 @@ def DataTable(
227
230
  start_index = (page - 1) * page_size + 1 if total else 0
228
231
  end_index = min(start_index + page_size - 1, total) if total else 0
229
232
  summary = f"{start_index}-{end_index} of {total} records" if total else "No matching records"
230
-
231
233
  base_query = urlencode({"search": search, "page_size": page_size})
232
234
 
233
235
  # Build table header keys and labels
@@ -255,6 +257,7 @@ def DataTable(
255
257
  row=row,
256
258
  row_id=row_id,
257
259
  crud_ops=crud_ops,
260
+ crud_enabled=crud_enabled,
258
261
  base_route=base_route,
259
262
  search=search,
260
263
  page=page,
@@ -264,9 +267,9 @@ def DataTable(
264
267
  )
265
268
  table_rows.append(row_dict)
266
269
 
267
- # Create button
270
+ # Create button - use crud_enabled for callable support
268
271
  create_button = None
269
- if crud_ops.get("create", False):
272
+ if crud_enabled.get("create", False):
270
273
  query = urlencode({"action": "create", "search": search, "page": page, "page_size": page_size})
271
274
  create_button = Button(
272
275
  Icon("add"),
@@ -277,31 +280,15 @@ def DataTable(
277
280
  cls=ButtonT.primary
278
281
  )
279
282
 
280
- # Pagination controls
283
+ # Pagination controls (using reusable Pagination component)
281
284
  pagination = None
282
285
  if total_pages > 1:
283
- prev_params = urlencode({"search": search, "page": max(1, page - 1), "page_size": page_size})
284
- next_params = urlencode({"search": search, "page": min(total_pages, page + 1), "page_size": page_size})
285
-
286
- pagination = Div(
287
- Button(
288
- Icon("navigate_before"),
289
- hx_get=f"{base_route}?{prev_params}",
290
- hx_target=f"#{container_id}",
291
- hx_push_url="true",
292
- disabled=page == 1,
293
- cls="circle"
294
- ),
295
- Span(f"Page {page} of {total_pages}", cls="middle"),
296
- Button(
297
- Icon("navigate_next"),
298
- hx_get=f"{base_route}?{next_params}",
299
- hx_target=f"#{container_id}",
300
- hx_push_url="true",
301
- disabled=page == total_pages,
302
- cls="circle"
303
- ),
304
- cls="row center-align"
286
+ pagination_params = urlencode({"search": search, "page_size": page_size})
287
+ pagination = Pagination(
288
+ current_page=page,
289
+ total_pages=total_pages,
290
+ hx_get=f"{base_route}?{pagination_params}",
291
+ hx_target=f"#{container_id}"
305
292
  )
306
293
 
307
294
  # Search form
@@ -335,6 +322,24 @@ def DataTable(
335
322
  cls="border"
336
323
  ) if data else Div(empty_message, cls="center-align padding")
337
324
 
325
+ # Footer section with page size selector and pagination
326
+ footer = None
327
+ if total > 0:
328
+ page_info_row = Div(
329
+ _page_size_select(page_size, search, base_route, container_id, page_sizes),
330
+ Span(summary, cls="small-text grey-text"),
331
+ cls="row"
332
+ )
333
+ if pagination:
334
+ # Two-row layout: info on top, centered pagination below
335
+ footer = Div(
336
+ page_info_row,
337
+ Div(pagination, cls="row center-align"),
338
+ )
339
+ else:
340
+ # Single row when no pagination needed
341
+ footer = page_info_row
342
+
338
343
  return Article(
339
344
  Div(id=feedback_id), # Feedback container for modals/toasts
340
345
  Div(
@@ -343,15 +348,7 @@ def DataTable(
343
348
  cls="row"
344
349
  ),
345
350
  data_table,
346
- Div(
347
- Div(
348
- _page_size_select(page_size, search, base_route, container_id, page_sizes),
349
- Span(summary, cls="small-text grey-text"),
350
- cls="row"
351
- ),
352
- pagination,
353
- cls="grid"
354
- ) if total > 0 else None,
351
+ footer,
355
352
  id=container_id,
356
353
  hx_trigger=f"{container_id}-refresh from:body",
357
354
  hx_get=f"{base_route}?{base_query}",
@@ -453,6 +450,7 @@ class CrudContext:
453
450
  tbl: Optional[Any] = None # request.state.tables[table_name] (if available)
454
451
  record: dict = None # Form data dict
455
452
  record_id: Optional[Any] = None # ID for update/delete (None for create)
453
+ feedback_id: Optional[str] = None # Target div ID for HTMX swap (for override handlers)
456
454
 
457
455
  # %% ../nbs/05_datatable.ipynb 13
458
456
  from typing import Callable, Optional, Any, Union
@@ -574,6 +572,20 @@ class DataTableResource:
574
572
  if not callable(action["handler"]):
575
573
  raise ValueError(f"Custom action '{action['name']}' handler must be callable")
576
574
 
575
+ # Parse crud_ops for callable overrides vs boolean enable/disable
576
+ # Callable = override handler (action is enabled)
577
+ # True = use default handler (action is enabled)
578
+ # False = action is disabled
579
+ self.crud_overrides = {}
580
+ self.crud_enabled = {}
581
+ for action_name in ['create', 'update', 'delete', 'view']:
582
+ value = self.crud_ops.get(action_name, action_name == 'view') # view defaults to True
583
+ if callable(value):
584
+ self.crud_overrides[action_name] = value
585
+ self.crud_enabled[action_name] = True # Callable means action is enabled
586
+ else:
587
+ self.crud_enabled[action_name] = bool(value)
588
+
577
589
  # CRUD hooks
578
590
  self.on_create_hook = on_create
579
591
  self.on_update_hook = on_update
@@ -603,7 +615,7 @@ class DataTableResource:
603
615
  return await hook(ctx)
604
616
  return hook(ctx)
605
617
 
606
- def _build_context(self, req, record: dict = None, record_id: Any = None) -> CrudContext:
618
+ def _build_context(self, req, record: dict = None, record_id: Any = None, include_feedback_id: bool = False) -> CrudContext:
607
619
  """🏗️ Build CrudContext from request."""
608
620
  user = None
609
621
  db = None
@@ -629,7 +641,8 @@ class DataTableResource:
629
641
  db=db,
630
642
  tbl=tbl,
631
643
  record=record or {},
632
- record_id=record_id
644
+ record_id=record_id,
645
+ feedback_id=self.feedback_id if include_feedback_id else None
633
646
  )
634
647
 
635
648
  def _wrap_response(self, content, req):
@@ -647,8 +660,8 @@ class DataTableResource:
647
660
  return self._handle_table(req)
648
661
 
649
662
  @rt(f"{self.base_route}/action")
650
- def _action_handler(req):
651
- return self._handle_action(req)
663
+ async def _action_handler(req):
664
+ return await self._handle_action(req)
652
665
 
653
666
  @rt(f"{self.base_route}/save")
654
667
  async def _save_handler(req):
@@ -704,6 +717,7 @@ class DataTableResource:
704
717
  search=search,
705
718
  columns=self.columns,
706
719
  crud_ops=self.crud_ops,
720
+ crud_enabled=self.crud_enabled,
707
721
  base_route=self.base_route,
708
722
  row_id_field=self.row_id_field,
709
723
  title=self.title,
@@ -731,8 +745,8 @@ class DataTableResource:
731
745
  # Wrap with layout if full-page request
732
746
  return self._wrap_response(content, req)
733
747
 
734
- def _handle_action(self, req):
735
- """Handle action route (view/edit/create/delete)."""
748
+ async def _handle_action(self, req):
749
+ """Handle action route (view/edit/create/delete) with override support."""
736
750
  params = getattr(req, "query_params", {})
737
751
  getter = params.get if hasattr(params, "get") else (lambda k, d=None: params[k] if k in params else d)
738
752
 
@@ -755,9 +769,47 @@ class DataTableResource:
755
769
  cancel_url = f"{self.base_route}/action?dismiss=1"
756
770
  save_url = f"{self.base_route}/save?{return_params}"
757
771
 
758
- # Handle CREATE
772
+ # Get record for actions that need it (not create)
773
+ record = None
774
+ if record_id:
775
+ raw_record = self.get_by_id(req, record_id)
776
+ record = _to_dict(raw_record) if raw_record else None
777
+
778
+ # Map action names to internal action names for overrides
779
+ action_map = {'edit': 'update'} # 'edit' action uses 'update' override
780
+ override_key = action_map.get(action, action)
781
+
782
+ # Check for CRUD override FIRST (callable in crud_ops)
783
+ if override_key in self.crud_overrides:
784
+ try:
785
+ ctx = self._build_context(req, record=record, record_id=record_id, include_feedback_id=True)
786
+ handler = self.crud_overrides[override_key]
787
+
788
+ # Execute handler (sync or async)
789
+ if asyncio.iscoroutinefunction(handler):
790
+ result = await handler(ctx)
791
+ else:
792
+ result = handler(ctx)
793
+
794
+ # Handle return value:
795
+ # - FT component: Return directly (full control to user)
796
+ # - str: Wrap in success toast
797
+ # - None: Default success message
798
+ if result is None:
799
+ return self._success_toast(f"{action.title()} completed successfully.")
800
+ elif isinstance(result, str):
801
+ return self._success_toast(result)
802
+ else:
803
+ # FT component - wrap in feedback div
804
+ return Div(result, id=self.feedback_id)
805
+
806
+ except Exception as e:
807
+ logger.error(f"CRUD override '{action}' failed: {e}", exc_info=True)
808
+ return self._error_toast(f"{action.title()} failed: {str(e)}")
809
+
810
+ # Handle CREATE (default behavior)
759
811
  if action == "create":
760
- if not self.crud_ops.get("create"):
812
+ if not self.crud_enabled.get("create"):
761
813
  return self._error_toast("Create operation not enabled.")
762
814
 
763
815
  modal = FormModal(
@@ -773,16 +825,11 @@ class DataTableResource:
773
825
  )
774
826
  return self._wrap_modal(modal)
775
827
 
776
- # Get record for view/edit/delete
777
- record = None
778
- if record_id:
779
- raw_record = self.get_by_id(req, record_id)
780
- record = _to_dict(raw_record) if raw_record else None
781
-
828
+ # Record required for remaining actions
782
829
  if not record:
783
830
  return self._error_toast("Record not found.")
784
831
 
785
- # Handle VIEW
832
+ # Handle VIEW (default behavior)
786
833
  if action == "view":
787
834
  modal = FormModal(
788
835
  columns=self.columns,
@@ -795,9 +842,9 @@ class DataTableResource:
795
842
  )
796
843
  return self._wrap_modal(modal)
797
844
 
798
- # Handle EDIT
845
+ # Handle EDIT (default behavior)
799
846
  if action == "edit":
800
- if not self.crud_ops.get("update"):
847
+ if not self.crud_enabled.get("update"):
801
848
  return self._error_toast("Update operation not enabled.")
802
849
 
803
850
  modal = FormModal(
@@ -813,9 +860,9 @@ class DataTableResource:
813
860
  )
814
861
  return self._wrap_modal(modal)
815
862
 
816
- # Handle DELETE
863
+ # Handle DELETE (default behavior)
817
864
  if action == "delete":
818
- if not self.crud_ops.get("delete"):
865
+ if not self.crud_enabled.get("delete"):
819
866
  return self._error_toast("Delete operation not enabled.")
820
867
 
821
868
  try:
@@ -823,11 +870,7 @@ class DataTableResource:
823
870
 
824
871
  # Use on_delete hook if provided
825
872
  if self.on_delete_hook:
826
- if asyncio.iscoroutinefunction(self.on_delete_hook):
827
- loop = asyncio.get_event_loop()
828
- loop.run_until_complete(self.on_delete_hook(ctx))
829
- else:
830
- self.on_delete_hook(ctx)
873
+ await self._call_hook(self.on_delete_hook, ctx)
831
874
  else:
832
875
  # Default: use delete_fn
833
876
  self.delete_fn(req, record_id)
@@ -842,19 +885,22 @@ class DataTableResource:
842
885
  for custom_action in custom_actions:
843
886
  if action == custom_action["name"]:
844
887
  try:
845
- ctx = self._build_context(req, record=record, record_id=record_id)
888
+ ctx = self._build_context(req, record=record, record_id=record_id, include_feedback_id=True)
846
889
  handler = custom_action["handler"]
847
890
 
848
891
  # Execute handler (sync or async)
849
892
  if asyncio.iscoroutinefunction(handler):
850
- loop = asyncio.get_event_loop()
851
- result = loop.run_until_complete(handler(ctx))
893
+ result = await handler(ctx)
852
894
  else:
853
895
  result = handler(ctx)
854
896
 
855
- # Return success toast with refresh trigger
856
- message = result if isinstance(result, str) else f"{custom_action['label']} completed successfully."
857
- return self._success_toast(message)
897
+ # Handle return value same as overrides
898
+ if result is None:
899
+ return self._success_toast(f"{custom_action['label']} completed successfully.")
900
+ elif isinstance(result, str):
901
+ return self._success_toast(result)
902
+ else:
903
+ return Div(result, id=self.feedback_id)
858
904
 
859
905
  except Exception as e:
860
906
  logger.error(f"Custom action '{action}' failed: {e}", exc_info=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-matui
3
- Version: 0.9.9
3
+ Version: 0.9.11
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=taelSBN5SjzTPZ994Gxw8NFdSDITeBao31tWpkapnXU,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=Q-koxO-BCxG9Dse2QUpP5WrGSrbDyzBSLoqIGatUAg8,49503
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.11.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
+ fh_matui-0.9.11.dist-info/METADATA,sha256=NIMhExSVx75fEZgaO1WJiaugYb2bVtHSDaX_SandwLI,10491
11
+ fh_matui-0.9.11.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
12
+ fh_matui-0.9.11.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
+ fh_matui-0.9.11.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
+ fh_matui-0.9.11.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- fh_matui/__init__.py,sha256=4eQCGJV2-GRHX8FlvA_4etK0E4LFj2or592J4aePZoA,23
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=KjdTHzWRXpVWBEIGskW1HfhjPpzRYzi6UA_yRjZyMWM,48254
5
- fh_matui/core.py,sha256=xtVBN8CtC50ZJ4Iu7o-mUhaA87tWdnz8gBfKRk63Zhs,10680
6
- fh_matui/datatable.py,sha256=NNb6WvGZ0QY13md4itwYH4a9yKZrOdwcaDW7PGo-vQM,36558
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.9.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
- fh_matui-0.9.9.dist-info/METADATA,sha256=OGoxCFS3_qRnEJe2EkD-dMDl21lhYt5ecds1B6f5xpU,10490
11
- fh_matui-0.9.9.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
12
- fh_matui-0.9.9.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
- fh_matui-0.9.9.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
- fh_matui-0.9.9.dist-info/RECORD,,