fh-pydantic-form 0.2.0__py3-none-any.whl → 0.2.1__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.

Potentially problematic release.


This version of fh-pydantic-form might be problematic. Click here for more details.

@@ -28,9 +28,11 @@ from fh_pydantic_form.form_parser import (
28
28
  _parse_list_fields,
29
29
  _parse_non_list_fields,
30
30
  )
31
+ from fh_pydantic_form.list_path import walk_path
31
32
  from fh_pydantic_form.registry import FieldRendererRegistry
32
33
  from fh_pydantic_form.type_helpers import _UNSET, get_default
33
34
  from fh_pydantic_form.ui_style import (
35
+ COMPACT_EXTRA_CSS,
34
36
  SpacingTheme,
35
37
  SpacingValue,
36
38
  _normalize_spacing,
@@ -207,17 +209,56 @@ class PydanticForm(Generic[ModelType]):
207
209
  - validating request data against the model
208
210
  """
209
211
 
212
+ # --- module-level flag (add near top of file) ---
213
+
210
214
  def _compact_wrapper(self, inner: FT) -> FT:
211
215
  """
212
- Wrap inner markup with a '.compact-form' div and inject one <style>
213
- block when compact theme is used.
216
+ Wrap inner markup in a namespaced div.
217
+ Auto-inject the compact CSS the *first* time any compact form is rendered.
214
218
  """
215
- if self.spacing == SpacingTheme.COMPACT:
216
- from fh_pydantic_form.ui_style import COMPACT_EXTRA_CSS
219
+ wrapper_cls = "fhpf-wrapper w-full flex-1"
217
220
 
218
- return fh.Div(COMPACT_EXTRA_CSS, inner, cls="compact-form")
219
- else:
220
- return inner
221
+ if self.spacing != SpacingTheme.COMPACT:
222
+ return fh.Div(inner, cls=wrapper_cls)
223
+
224
+ return fh.Div(
225
+ COMPACT_EXTRA_CSS,
226
+ fh.Div(inner, cls="fhpf-fields fhpf-compact"),
227
+ cls=wrapper_cls,
228
+ )
229
+
230
+ def _clone_with_values(self, values: Dict[str, Any]) -> "PydanticForm":
231
+ """
232
+ Create a copy of this renderer with the same configuration but different values.
233
+
234
+ This preserves all constructor arguments (label_colors, custom_renderers, etc.)
235
+ to avoid configuration drift during refresh operations.
236
+
237
+ Args:
238
+ values: New values dictionary to use in the cloned renderer
239
+
240
+ Returns:
241
+ A new PydanticForm instance with identical configuration but updated values
242
+ """
243
+ # Get custom renderers if they were registered (not stored directly on instance)
244
+ # We'll rely on global registry state being preserved
245
+
246
+ clone = PydanticForm(
247
+ form_name=self.name,
248
+ model_class=self.model_class,
249
+ initial_values=None, # Will be set via values_dict below
250
+ custom_renderers=None, # Registry is global, no need to re-register
251
+ disabled=self.disabled,
252
+ disabled_fields=self.disabled_fields,
253
+ label_colors=self.label_colors,
254
+ exclude_fields=self.exclude_fields,
255
+ spacing=self.spacing,
256
+ )
257
+
258
+ # Set the values directly
259
+ clone.values_dict = values
260
+
261
+ return clone
221
262
 
222
263
  def __init__(
223
264
  self,
@@ -393,6 +434,7 @@ class PydanticForm(Generic[ModelType]):
393
434
  disabled=is_field_disabled, # Pass the calculated disabled state
394
435
  label_color=label_color, # Pass the label color if specified
395
436
  spacing=self.spacing, # Pass the spacing
437
+ field_path=[field_name], # Set top-level field path
396
438
  )
397
439
 
398
440
  rendered_field = renderer.render()
@@ -452,15 +494,8 @@ class PydanticForm(Generic[ModelType]):
452
494
  cls=mui.AlertT.warning + " mb-4", # Add margin bottom
453
495
  )
454
496
 
455
- # Create Temporary Renderer instance
456
- temp_renderer = PydanticForm(
457
- form_name=self.name,
458
- model_class=self.model_class,
459
- # No initial_data needed here, we set values_dict below
460
- spacing=self.spacing,
461
- )
462
- # Set the values based on the parsed (or fallback) data
463
- temp_renderer.values_dict = parsed_data
497
+ # Create temporary renderer with same configuration but updated values
498
+ temp_renderer = self._clone_with_values(parsed_data)
464
499
 
465
500
  refreshed_inputs_component = temp_renderer.render_inputs()
466
501
 
@@ -565,6 +600,8 @@ class PydanticForm(Generic[ModelType]):
565
600
  Ensures that every field listed in self.exclude_fields is present in data
566
601
  if the model defines a default or default_factory, or if initial_values were provided.
567
602
 
603
+ Also ensures all model fields have appropriate defaults if missing.
604
+
568
605
  Priority order:
569
606
  1. initial_values (if provided during form creation)
570
607
  2. model defaults/default_factory
@@ -577,6 +614,7 @@ class PydanticForm(Generic[ModelType]):
577
614
  Returns:
578
615
  The same dictionary instance for method chaining
579
616
  """
617
+ # Handle excluded fields first
580
618
  for field_name in self.exclude_fields:
581
619
  # Skip if already present (e.g., user provided initial_values)
582
620
  if field_name in data:
@@ -613,6 +651,22 @@ class PydanticForm(Generic[ModelType]):
613
651
  # No default → leave missing; validation will surface error
614
652
  logger.debug(f"No default found for excluded field '{field_name}'")
615
653
 
654
+ # Also handle any other missing fields that should have defaults
655
+ for field_name, field_info in self.model_class.model_fields.items():
656
+ if field_name not in data:
657
+ # Try to inject defaults for missing fields
658
+ if field_name in self.initial_values_dict:
659
+ initial_val = self.initial_values_dict[field_name]
660
+ if hasattr(initial_val, "model_dump"):
661
+ initial_val = initial_val.model_dump()
662
+ data[field_name] = initial_val
663
+ else:
664
+ default_val = get_default(field_info)
665
+ if default_val is not _UNSET:
666
+ if hasattr(default_val, "model_dump"):
667
+ default_val = default_val.model_dump()
668
+ data[field_name] = default_val
669
+
616
670
  return data
617
671
 
618
672
  def register_routes(self, app):
@@ -652,123 +706,76 @@ class PydanticForm(Generic[ModelType]):
652
706
  f"Registered reset route for form '{self.name}' at {reset_route_path}"
653
707
  )
654
708
 
655
- @app.route(f"/form/{self.name}/list/add/{{field_name}}")
656
- async def post_list_add(req, field_name: str):
709
+ # Try the route with a more explicit pattern
710
+ route_pattern = f"/form/{self.name}/list/{{action}}/{{list_path:path}}"
711
+ logger.debug(f"Registering list action route: {route_pattern}")
712
+
713
+ @app.route(route_pattern, methods=["POST", "DELETE"])
714
+ async def list_action(req, action: str, list_path: str):
657
715
  """
658
- Handle adding an item to a list for this specific form
716
+ Handle list actions (add/delete) for nested lists in this specific form
659
717
 
660
718
  Args:
661
719
  req: The request object
662
- field_name: The name of the list field
720
+ action: Either "add" or "delete"
721
+ list_path: Path to the list field (e.g., "tags" or "main_address/tags" or "other_addresses/1/tags")
663
722
 
664
723
  Returns:
665
- A component for the new list item
724
+ A component for the new list item (add) or empty response (delete)
666
725
  """
667
- # Find field info
668
- field_info = None
669
- item_type = None
670
-
671
- if field_name in self.model_class.model_fields:
672
- field_info = self.model_class.model_fields[field_name]
673
- annotation = getattr(field_info, "annotation", None)
674
-
675
- if (
676
- annotation is not None
677
- and hasattr(annotation, "__origin__")
678
- and annotation.__origin__ is list
679
- ):
680
- item_type = annotation.__args__[0]
681
-
682
- if not item_type:
683
- logger.error(
684
- f"Cannot determine item type for list field {field_name}"
685
- )
686
- return mui.Alert(
687
- f"Cannot determine item type for list field {field_name}",
688
- cls=mui.AlertT.error,
689
- )
690
-
691
- # Create a default item using smart defaults
692
- default_item_dict: Dict[str, Any] | str | Any | None = None
693
- try:
694
- if not item_type:
695
- logger.warning(
696
- f"item_type was None when trying to create default for {field_name}"
697
- )
698
- default_item_dict = ""
699
- elif hasattr(item_type, "model_fields"):
700
- # For Pydantic models, use smart default generation
701
- default_item_dict = default_dict_for_model(item_type)
702
- else:
703
- # For simple types, use annotation-based defaults
704
- default_item_dict = default_for_annotation(item_type)
726
+ if action not in {"add", "delete"}:
727
+ return fh.Response(status_code=400, content="Unknown list action")
705
728
 
706
- # Final fallback for exotic cases
707
- if default_item_dict is None:
708
- default_item_dict = ""
709
-
710
- except Exception as e:
711
- logger.error(
712
- f"Error creating default item for {field_name}: {e}", exc_info=True
713
- )
714
- return fh.Li(
715
- mui.Alert(
716
- f"Error creating default item: {str(e)}", cls=mui.AlertT.error
717
- ),
718
- cls="mb-2",
729
+ segments = list_path.split("/")
730
+ try:
731
+ list_field_info, html_parts, item_type = walk_path(
732
+ self.model_class, segments
719
733
  )
734
+ except ValueError as exc:
735
+ logger.warning("Bad list path %s – %s", list_path, exc)
736
+ return mui.Alert(str(exc), cls=mui.AlertT.error)
720
737
 
721
- # Generate a unique placeholder index
722
- placeholder_idx = f"new_{int(pytime.time() * 1000)}"
723
-
724
- # Create a list renderer
725
- if field_info is not None:
726
- list_renderer = ListFieldRenderer(
727
- field_name=field_name,
728
- field_info=field_info,
729
- value=[], # Empty list, we only need to render one item
730
- prefix=self.base_prefix, # Use the form's base prefix
731
- )
732
- else:
733
- logger.error(f"field_info is None for field {field_name}")
734
- return mui.Alert(
735
- f"Field info not found for {field_name}",
736
- cls=mui.AlertT.error,
738
+ if req.method == "DELETE":
739
+ logger.debug(
740
+ f"Received DELETE request for {list_path} for form '{self.name}'"
737
741
  )
742
+ return fh.Response(status_code=200, content="")
738
743
 
739
- # The default_item_dict is already in the correct format (dict for models, primitive for simple types)
740
- item_data_for_renderer = default_item_dict
741
- logger.debug(
742
- f"Add item: Using smart default for renderer: {item_data_for_renderer}"
744
+ # === add (POST) ===
745
+ default_item = (
746
+ default_dict_for_model(item_type)
747
+ if hasattr(item_type, "model_fields")
748
+ else default_for_annotation(item_type)
743
749
  )
744
750
 
745
- # Render the new item card, set is_open=True to make it expanded by default
746
- new_item_card = list_renderer._render_item_card(
747
- item_data_for_renderer, # Pass the dictionary or simple value
748
- placeholder_idx,
749
- item_type,
750
- is_open=True,
751
+ # Build prefix **without** the list field itself to avoid duplication
752
+ parts_before_list = html_parts[:-1] # drop final segment
753
+ if parts_before_list:
754
+ html_prefix = f"{self.base_prefix}{'_'.join(parts_before_list)}_"
755
+ else:
756
+ html_prefix = self.base_prefix
757
+
758
+ # Create renderer for the list field
759
+ renderer = ListFieldRenderer(
760
+ field_name=segments[-1],
761
+ field_info=list_field_info,
762
+ value=[],
763
+ prefix=html_prefix,
764
+ spacing=self.spacing,
765
+ disabled=self.disabled,
766
+ field_path=segments, # Pass the full path segments
767
+ form_name=self.name, # Pass the explicit form name
751
768
  )
752
769
 
753
- return new_item_card
754
-
755
- @app.route(f"/form/{self.name}/list/delete/{{field_name}}", methods=["DELETE"])
756
- async def delete_list_item(req, field_name: str):
757
- """
758
- Handle deleting an item from a list for this specific form
759
-
760
- Args:
761
- req: The request object
762
- field_name: The name of the list field
770
+ # Generate a unique placeholder index
771
+ placeholder_idx = f"new_{int(pytime.time() * 1000)}"
763
772
 
764
- Returns:
765
- Empty string to delete the target element
766
- """
767
- # Return empty string to delete the target element
768
- logger.debug(
769
- f"Received DELETE request for {field_name} for form '{self.name}'"
773
+ # Render the new item card, set is_open=True to make it expanded by default
774
+ new_card = renderer._render_item_card(
775
+ default_item, placeholder_idx, item_type, is_open=True
770
776
  )
771
- return fh.Response(status_code=200, content="")
777
+
778
+ return new_card
772
779
 
773
780
  def refresh_button(self, text: Optional[str] = None, **kwargs) -> FT:
774
781
  """
@@ -787,9 +794,6 @@ class PydanticForm(Generic[ModelType]):
787
794
  # Define the target wrapper ID
788
795
  form_content_wrapper_id = f"{self.name}-inputs-wrapper"
789
796
 
790
- # Define the form ID to include
791
- form_id = f"{self.name}-form"
792
-
793
797
  # Define the target URL
794
798
  refresh_url = f"/form/{self.name}/refresh"
795
799
 
@@ -800,7 +804,7 @@ class PydanticForm(Generic[ModelType]):
800
804
  "hx_target": f"#{form_content_wrapper_id}", # Target the wrapper Div ID
801
805
  "hx_swap": "innerHTML",
802
806
  "hx_trigger": "click", # Explicit trigger on click
803
- "hx_include": f"#{form_id}", # Include all form fields in the request
807
+ "hx_include": "closest form", # Include all form fields from the enclosing form
804
808
  "uk_tooltip": "Update the form display based on current values (e.g., list item titles)",
805
809
  "cls": mui.ButtonT.secondary,
806
810
  }
@@ -883,3 +887,12 @@ class PydanticForm(Generic[ModelType]):
883
887
  logger.info(f"Request validation successful for form '{self.name}'")
884
888
 
885
889
  return validated_model
890
+
891
+ def form_id(self) -> str:
892
+ """
893
+ Get the standard form ID for this renderer.
894
+
895
+ Returns:
896
+ The form ID string that should be used for the HTML form element
897
+ """
898
+ return f"{self.name}-form"
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ from typing import List, Tuple, Type, get_origin, get_args
4
+ from pydantic import BaseModel
5
+ from pydantic.fields import FieldInfo
6
+
7
+ from fh_pydantic_form.type_helpers import _get_underlying_type_if_optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def walk_path(
13
+ model: Type[BaseModel], segments: List[str]
14
+ ) -> Tuple[FieldInfo, List[str], Type]:
15
+ """
16
+ Resolve `segments` against `model`, stopping at the *list* field.
17
+
18
+ Args:
19
+ model: The BaseModel class to traverse
20
+ segments: Path segments like ["main_address", "tags"] or ["other_addresses", "1", "tags"]
21
+ The final segment should always be a list field name.
22
+
23
+ Returns:
24
+ Tuple of:
25
+ - list_field_info: the FieldInfo for the target list field
26
+ - html_prefix_parts: segments used to build element IDs (includes indices)
27
+ - item_type: the concrete python type of items in the list
28
+
29
+ Raises:
30
+ ValueError: if the path is invalid or doesn't lead to a list field
31
+ """
32
+ if not segments:
33
+ raise ValueError("Empty path provided")
34
+
35
+ current_model = model
36
+ html_parts = []
37
+ i = 0
38
+
39
+ # Process all segments except the last one (which should be the list field)
40
+ while i < len(segments) - 1:
41
+ segment = segments[i]
42
+
43
+ # Check if this segment is a field name
44
+ if segment in current_model.model_fields:
45
+ field_info = current_model.model_fields[segment]
46
+ field_type = _get_underlying_type_if_optional(field_info.annotation)
47
+ html_parts.append(segment)
48
+
49
+ # Check if this is a list field (we're traversing into a list element)
50
+ if get_origin(field_type) is list:
51
+ # Next segment should be an index
52
+ if i + 1 >= len(segments) - 1:
53
+ raise ValueError(f"Expected index after list field '{segment}'")
54
+
55
+ next_segment = segments[i + 1]
56
+ if not _is_index_segment(next_segment):
57
+ raise ValueError(
58
+ f"Expected index after list field '{segment}', got '{next_segment}'"
59
+ )
60
+
61
+ # Get the item type of the list
62
+ list_item_type = (
63
+ get_args(field_type)[0] if get_args(field_type) else None
64
+ )
65
+ if not list_item_type or not hasattr(list_item_type, "model_fields"):
66
+ raise ValueError(
67
+ f"List field '{segment}' does not contain BaseModel items"
68
+ )
69
+
70
+ # Add the index to html_parts and update current model
71
+ html_parts.append(next_segment)
72
+ current_model = list_item_type
73
+
74
+ # Skip the next segment (the index) since we processed it
75
+ i += 2
76
+ continue
77
+
78
+ # Check if this is a BaseModel field
79
+ elif hasattr(field_type, "model_fields"):
80
+ current_model = field_type
81
+ i += 1
82
+ else:
83
+ raise ValueError(f"Field '{segment}' is not a BaseModel or list type")
84
+
85
+ elif _is_index_segment(segment):
86
+ # This should only happen if we're processing an index that wasn't handled above
87
+ raise ValueError(
88
+ f"Unexpected index segment '{segment}' without preceding list field"
89
+ )
90
+ else:
91
+ raise ValueError(
92
+ f"Field '{segment}' not found in model {current_model.__name__}"
93
+ )
94
+
95
+ # Process the final segment (should be a list field)
96
+ final_field_name = segments[-1]
97
+ if final_field_name not in current_model.model_fields:
98
+ raise ValueError(
99
+ f"Field '{final_field_name}' not found in model {current_model.__name__}"
100
+ )
101
+
102
+ list_field_info = current_model.model_fields[final_field_name]
103
+ list_field_type = _get_underlying_type_if_optional(list_field_info.annotation)
104
+
105
+ # Verify this is actually a list field
106
+ if get_origin(list_field_type) is not list:
107
+ raise ValueError(f"Final field '{final_field_name}' is not a list type")
108
+
109
+ # Get the item type
110
+ item_type_args = get_args(list_field_type)
111
+ if not item_type_args:
112
+ raise ValueError(
113
+ f"Cannot determine item type for list field '{final_field_name}'"
114
+ )
115
+
116
+ item_type = item_type_args[0]
117
+ html_parts.append(final_field_name)
118
+
119
+ logger.debug(
120
+ f"walk_path resolved: {segments} -> field_info={list_field_info}, html_parts={html_parts}, item_type={item_type}"
121
+ )
122
+
123
+ return list_field_info, html_parts, item_type
124
+
125
+
126
+ def _is_index_segment(segment: str) -> bool:
127
+ """
128
+ Check if a segment represents an index (purely numeric or placeholder like 'new_1234').
129
+
130
+ Args:
131
+ segment: The segment to check
132
+
133
+ Returns:
134
+ True if the segment represents an index
135
+ """
136
+ # Pure numeric (like "0", "1", "2")
137
+ if segment.isdigit():
138
+ return True
139
+
140
+ # Placeholder format (like "new_1234567890")
141
+ if segment.startswith("new_") and len(segment) > 4:
142
+ timestamp_part = segment[4:]
143
+ return timestamp_part.isdigit()
144
+
145
+ return False
@@ -1,5 +1,6 @@
1
1
  from enum import Enum, auto
2
2
  from typing import Dict, Literal, Union
3
+
3
4
  import fasthtml.common as fh
4
5
 
5
6
 
@@ -50,15 +51,17 @@ SPACING_MAP: Dict[SpacingTheme, Dict[str, str]] = {
50
51
  "accordion_content": "",
51
52
  "input_size": "",
52
53
  "input_padding": "",
54
+ "horizontal_gap": "gap-3",
55
+ "label_align": "items-start",
53
56
  },
54
57
  SpacingTheme.COMPACT: {
55
- "outer_margin": "mb-0.5",
56
- "outer_margin_sm": "mb-0.5",
57
- "inner_gap": "",
58
- "inner_gap_small": "",
59
- "stack_gap": "",
60
- "padding": "p-2",
61
- "padding_sm": "p-1",
58
+ "outer_margin": "mb-0",
59
+ "outer_margin_sm": "mb-0",
60
+ "inner_gap": "space-y-1",
61
+ "inner_gap_small": "space-y-0.5",
62
+ "stack_gap": "space-y-1",
63
+ "padding": "p-1",
64
+ "padding_sm": "p-0.5",
62
65
  "padding_card": "px-2 py-1",
63
66
  "card_border": "",
64
67
  "section_divider": "",
@@ -68,6 +71,8 @@ SPACING_MAP: Dict[SpacingTheme, Dict[str, str]] = {
68
71
  "accordion_content": "uk-padding-remove-vertical",
69
72
  "input_size": "uk-form-small",
70
73
  "input_padding": "p-1",
74
+ "horizontal_gap": "gap-2",
75
+ "label_align": "items-center",
71
76
  },
72
77
  }
73
78
 
@@ -78,46 +83,52 @@ def spacing(token: str, spacing: SpacingValue) -> str:
78
83
  return SPACING_MAP[theme][token]
79
84
 
80
85
 
81
- # CSS override to kill any residual borders in compact mode
86
+ # Optional minimal CSS for compact mode - affects only form inputs, not layout
87
+ # Host applications can optionally inject this once at app level if desired
82
88
  COMPACT_EXTRA_CSS = fh.Style("""
83
- /* Aggressive margin reduction for all UIkit margin utilities */
84
- .compact-form .uk-margin-small-bottom,
85
- .compact-form .uk-margin,
86
- .compact-form .uk-margin-bottom {
87
- margin-bottom: 2px !important;
88
- }
89
-
90
- /* Remove borders and shrink accordion chrome */
91
- .compact-form .uk-accordion > li,
92
- .compact-form .uk-accordion .uk-accordion-content {
93
- border: 0 !important;
94
- }
95
-
96
- /* Minimize accordion content padding */
97
- .compact-form .uk-accordion-content {
98
- padding-top: 0.25rem !important;
99
- padding-bottom: 0.25rem !important;
100
- }
101
-
102
- /* Shrink accordion item title padding */
103
- .compact-form li.uk-open > a {
104
- padding-top: 0.25rem;
105
- padding-bottom: 0.25rem;
106
- }
107
-
108
- /* Apply smaller font and reduced padding to all form inputs */
109
- .compact-form input,
110
- .compact-form select,
111
- .compact-form textarea {
112
- line-height: 1.25rem !important; /* ~20px */
113
- font-size: 0.8125rem !important; /* 13px */
114
- }
115
-
116
- /* Legacy overrides for specific UIkit classes */
117
- .compact-form input.uk-form-small,
118
- .compact-form select.uk-form-small,
119
- .compact-form textarea.uk-textarea-small {
120
- padding-top: 2px !important;
121
- padding-bottom: 2px !important;
89
+ /* Compact polish applies ONLY inside .fhpf-compact ------------------- */
90
+ .fhpf-compact {
91
+
92
+ /* Accordion chrome: remove border and default 20 px gap */
93
+ .uk-accordion > li,
94
+ .uk-accordion > li + li { /* second & later items */
95
+ border-top: 0 !important;
96
+ margin-top: 0 !important;
97
+ }
98
+ .uk-accordion-title::after { /* the hair-line we still see */
99
+ border-top: 0 !important;
100
+ }
101
+
102
+ /* Tighter title and content padding */
103
+ li > a.uk-accordion-title,
104
+ .uk-accordion-content {
105
+ padding-top: 0.25rem !important;
106
+ padding-bottom: 0.25rem !important;
107
+ }
108
+
109
+ /* Remove residual card outline */
110
+ .uk-card,
111
+ .uk-card-body { border: 0 !important; }
112
+
113
+ /* Small-size inputs */
114
+ input, select, textarea {
115
+ line-height: 1.25rem !important;
116
+ font-size: 0.8125rem !important;
117
+ padding-top: 0.25rem !important;
118
+ padding-bottom: 0.25rem !important;
119
+ }
120
+
121
+ /* Legacy uk-form-small support */
122
+ input.uk-form-small,
123
+ select.uk-form-small,
124
+ textarea.uk-textarea-small {
125
+ padding-top: 2px !important;
126
+ padding-bottom: 2px !important;
127
+ }
128
+
129
+ /* Kill generic uk-margin utilities inside the form */
130
+ .uk-margin-small-bottom,
131
+ .uk-margin,
132
+ .uk-margin-bottom { margin-bottom: 2px !important; }
122
133
  }
123
134
  """)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-pydantic-form
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: a library to turn any pydantic BaseModel object into a fasthtml/monsterui input form
5
5
  Project-URL: Homepage, https://github.com/Marcura/fh-pydantic-form
6
6
  Project-URL: Repository, https://github.com/Marcura/fh-pydantic-form
@@ -32,7 +32,8 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  `fh-pydantic-form` simplifies creating web forms for [FastHTML](https://github.com/AnswerDotAI/fasthtml) by automatically generating the necessary HTML input elements based on your Pydantic model definitions. It integrates seamlessly with and leverages [MonsterUI](https://github.com/AnswerDotAI/monsterui) components for styling.
34
34
 
35
- <img width="1405" alt="image" src="https://github.com/user-attachments/assets/d65d9d68-1635-4ea4-83f8-70c4b6b79796" />
35
+ <img width="1348" alt="image" src="https://github.com/user-attachments/assets/59cc4f10-6858-41cb-80ed-e735a883cf20" />
36
+
36
37
 
37
38
 
38
39
  <details >
@@ -225,19 +226,8 @@ form_normal = PydanticForm("normal_form", MyModel, spacing="normal")
225
226
  form_compact = PydanticForm("compact_form", MyModel, spacing="compact")
226
227
  ```
227
228
 
228
- **Compact mode** automatically injects additional CSS (`COMPACT_EXTRA_CSS`) to minimize margins, borders, and padding throughout the form. You can also import and use this CSS independently:
229
-
230
- ```python
231
- from fh_pydantic_form import COMPACT_EXTRA_CSS
232
229
 
233
- app, rt = fh.fast_app(
234
- hdrs=[
235
- mui.Theme.blue.headers(),
236
- COMPACT_EXTRA_CSS, # Apply compact styling globally
237
- ],
238
- # ...
239
- )
240
- ```
230
+ **Important:** The compact CSS is now scoped with `.fhpf-compact` classes and only affects form inputs, not layout containers. This prevents conflicts with your application's layout system.
241
231
 
242
232
  ## Working with Lists
243
233