fh-pydantic-form 0.2.2__tar.gz → 0.2.3__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.

Potentially problematic release.


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

Files changed (19) hide show
  1. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/PKG-INFO +1 -1
  2. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/RELEASE_NOTES.md +5 -0
  3. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/pyproject.toml +1 -1
  4. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/__init__.py +2 -2
  5. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/field_renderers.py +78 -29
  6. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/form_renderer.py +2 -12
  7. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/ui_style.py +29 -70
  8. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/.github/workflows/build.yaml +0 -0
  9. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/.github/workflows/publish.yaml +0 -0
  10. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/.gitignore +0 -0
  11. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/.pre-commit-config.yaml +0 -0
  12. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/LICENSE +0 -0
  13. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/README.md +0 -0
  14. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/defaults.py +0 -0
  15. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/form_parser.py +0 -0
  16. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/list_path.py +0 -0
  17. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/py.typed +0 -0
  18. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/registry.py +0 -0
  19. {fh_pydantic_form-0.2.2 → fh_pydantic_form-0.2.3}/src/fh_pydantic_form/type_helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-pydantic-form
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -1,5 +1,10 @@
1
1
  # Release Notes
2
2
 
3
+ ## Version 0.2.3 (2025-06-16 )
4
+
5
+ - Removed the custom css injection for compact spacing. Instead applying to components directly.
6
+
7
+
3
8
  ## Version 0.2.2 (2025-06-16 )
4
9
 
5
10
  - fix left alignment issue with inputs in the presence of outside css influences
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fh-pydantic-form"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "a library to turn any pydantic BaseModel object into a fasthtml/monsterui input form"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -24,7 +24,7 @@ from fh_pydantic_form.ui_style import (
24
24
  SpacingTheme,
25
25
  SpacingValue,
26
26
  spacing,
27
- COMPACT_EXTRA_CSS,
27
+ spacing_many,
28
28
  )
29
29
  from fh_pydantic_form.defaults import (
30
30
  default_dict_for_model,
@@ -119,7 +119,7 @@ __all__ = [
119
119
  "SpacingTheme",
120
120
  "SpacingValue",
121
121
  "spacing",
122
- "COMPACT_EXTRA_CSS",
122
+ "spacing_many",
123
123
  "default_dict_for_model",
124
124
  "default_for_annotation",
125
125
  ]
@@ -27,6 +27,7 @@ from fh_pydantic_form.ui_style import (
27
27
  SpacingValue,
28
28
  _normalize_spacing,
29
29
  spacing,
30
+ spacing_many,
30
31
  )
31
32
 
32
33
  logger = logging.getLogger(__name__)
@@ -252,6 +253,14 @@ class StringFieldRenderer(BaseFieldRenderer):
252
253
  if self.is_optional:
253
254
  placeholder_text += " (Optional)"
254
255
 
256
+ input_cls_parts = ["w-full"]
257
+ input_spacing_cls = spacing_many(
258
+ ["input_size", "input_padding", "input_line_height", "input_font_size"],
259
+ self.spacing,
260
+ )
261
+ if input_spacing_cls:
262
+ input_cls_parts.append(input_spacing_cls)
263
+
255
264
  input_attrs = {
256
265
  "value": self.value or "",
257
266
  "id": self.field_name,
@@ -259,10 +268,7 @@ class StringFieldRenderer(BaseFieldRenderer):
259
268
  "type": "text",
260
269
  "placeholder": placeholder_text,
261
270
  "required": is_field_required,
262
- "cls": _merge_cls(
263
- "w-full",
264
- f"{spacing('input_size', self.spacing)} {spacing('input_padding', self.spacing)}".strip(),
265
- ),
271
+ "cls": " ".join(input_cls_parts),
266
272
  }
267
273
 
268
274
  # Only add the disabled attribute if the field should actually be disabled
@@ -290,6 +296,14 @@ class NumberFieldRenderer(BaseFieldRenderer):
290
296
  if self.is_optional:
291
297
  placeholder_text += " (Optional)"
292
298
 
299
+ input_cls_parts = ["w-full"]
300
+ input_spacing_cls = spacing_many(
301
+ ["input_size", "input_padding", "input_line_height", "input_font_size"],
302
+ self.spacing,
303
+ )
304
+ if input_spacing_cls:
305
+ input_cls_parts.append(input_spacing_cls)
306
+
293
307
  input_attrs = {
294
308
  "value": str(self.value) if self.value is not None else "",
295
309
  "id": self.field_name,
@@ -297,10 +311,7 @@ class NumberFieldRenderer(BaseFieldRenderer):
297
311
  "type": "number",
298
312
  "placeholder": placeholder_text,
299
313
  "required": is_field_required,
300
- "cls": _merge_cls(
301
- "w-full",
302
- f"{spacing('input_size', self.spacing)} {spacing('input_padding', self.spacing)}".strip(),
303
- ),
314
+ "cls": " ".join(input_cls_parts),
304
315
  "step": "any"
305
316
  if self.field_info.annotation is float
306
317
  or get_origin(self.field_info.annotation) is float
@@ -386,6 +397,14 @@ class DateFieldRenderer(BaseFieldRenderer):
386
397
  if self.is_optional:
387
398
  placeholder_text += " (Optional)"
388
399
 
400
+ input_cls_parts = ["w-full"]
401
+ input_spacing_cls = spacing_many(
402
+ ["input_size", "input_padding", "input_line_height", "input_font_size"],
403
+ self.spacing,
404
+ )
405
+ if input_spacing_cls:
406
+ input_cls_parts.append(input_spacing_cls)
407
+
389
408
  input_attrs = {
390
409
  "value": formatted_value,
391
410
  "id": self.field_name,
@@ -393,10 +412,7 @@ class DateFieldRenderer(BaseFieldRenderer):
393
412
  "type": "date",
394
413
  "placeholder": placeholder_text,
395
414
  "required": is_field_required,
396
- "cls": _merge_cls(
397
- "w-full",
398
- f"{spacing('input_size', self.spacing)} {spacing('input_padding', self.spacing)}".strip(),
399
- ),
415
+ "cls": " ".join(input_cls_parts),
400
416
  }
401
417
 
402
418
  # Only add the disabled attribute if the field should actually be disabled
@@ -433,6 +449,14 @@ class TimeFieldRenderer(BaseFieldRenderer):
433
449
  if self.is_optional:
434
450
  placeholder_text += " (Optional)"
435
451
 
452
+ input_cls_parts = ["w-full"]
453
+ input_spacing_cls = spacing_many(
454
+ ["input_size", "input_padding", "input_line_height", "input_font_size"],
455
+ self.spacing,
456
+ )
457
+ if input_spacing_cls:
458
+ input_cls_parts.append(input_spacing_cls)
459
+
436
460
  input_attrs = {
437
461
  "value": formatted_value,
438
462
  "id": self.field_name,
@@ -440,10 +464,7 @@ class TimeFieldRenderer(BaseFieldRenderer):
440
464
  "type": "time",
441
465
  "placeholder": placeholder_text,
442
466
  "required": is_field_required,
443
- "cls": _merge_cls(
444
- "w-full",
445
- f"{spacing('input_size', self.spacing)} {spacing('input_padding', self.spacing)}".strip(),
446
- ),
467
+ "cls": " ".join(input_cls_parts),
447
468
  }
448
469
 
449
470
  # Only add the disabled attribute if the field should actually be disabled
@@ -503,18 +524,22 @@ class LiteralFieldRenderer(BaseFieldRenderer):
503
524
  placeholder_text += " (Optional)"
504
525
 
505
526
  # Prepare attributes dictionary
527
+ select_cls_parts = ["w-full"]
528
+ select_spacing_cls = spacing_many(
529
+ ["input_size", "input_padding", "input_line_height", "input_font_size"],
530
+ self.spacing,
531
+ )
532
+ if select_spacing_cls:
533
+ select_cls_parts.append(select_spacing_cls)
534
+
506
535
  select_attrs = {
507
536
  "id": self.field_name,
508
537
  "name": self.field_name,
509
538
  "required": is_field_required,
510
539
  "placeholder": placeholder_text,
511
- "cls": _merge_cls(
512
- "w-full",
513
- f"{spacing('input_size', self.spacing)} {spacing('input_padding', self.spacing)}".strip(),
514
- ),
540
+ "cls": " ".join(select_cls_parts),
515
541
  }
516
542
 
517
- # Only add the disabled attribute if the field should actually be disabled
518
543
  if self.disabled:
519
544
  select_attrs["disabled"] = True
520
545
 
@@ -667,12 +692,15 @@ class BaseModelFieldRenderer(BaseFieldRenderer):
667
692
  )
668
693
 
669
694
  # 5. Wrap the single AccordionItem in an Accordion container
695
+ accordion_cls = spacing_many(
696
+ ["accordion_divider", "accordion_content"], self.spacing
697
+ )
670
698
  accordion_container = mui.Accordion(
671
699
  accordion_item, # The single item to include
672
700
  id=accordion_id, # ID for the accordion container (ul)
673
701
  multiple=True, # Allow multiple open (though only one exists)
674
702
  collapsible=True, # Allow toggling
675
- cls=f"{spacing('accordion_divider', self.spacing)} {spacing('accordion_content', self.spacing)} w-full".strip(),
703
+ cls=f"{accordion_cls} w-full".strip(),
676
704
  )
677
705
 
678
706
  return accordion_container
@@ -987,12 +1015,15 @@ class ListFieldRenderer(BaseFieldRenderer):
987
1015
  container_id = self._container_id()
988
1016
 
989
1017
  # Use mui.Accordion component
1018
+ accordion_cls = spacing_many(
1019
+ ["inner_gap_small", "accordion_content", "accordion_divider"], self.spacing
1020
+ )
990
1021
  accordion = mui.Accordion(
991
1022
  *item_elements,
992
1023
  id=container_id,
993
1024
  multiple=True, # Allow multiple items to be open at once
994
1025
  collapsible=True, # Make it collapsible
995
- cls=f"{spacing('inner_gap_small', self.spacing)} {spacing('accordion_content', self.spacing)}".strip(), # Add space between items and accordion content styling
1026
+ cls=accordion_cls.strip(), # Add space between items and accordion content styling
996
1027
  )
997
1028
 
998
1029
  # Empty state message if no items
@@ -1344,10 +1375,19 @@ class ListFieldRenderer(BaseFieldRenderer):
1344
1375
  )
1345
1376
  li_attrs = {"id": full_card_id}
1346
1377
 
1347
- # Build card classes conditionally based on spacing theme
1348
- card_cls = "uk-card uk-margin-small-bottom"
1378
+ # Build card classes using spacing tokens
1379
+ card_cls_parts = ["uk-card"]
1349
1380
  if self.spacing == SpacingTheme.NORMAL:
1350
- card_cls += " uk-card-default"
1381
+ card_cls_parts.append("uk-card-default")
1382
+
1383
+ # Add spacing-based classes
1384
+ card_spacing_cls = spacing_many(
1385
+ ["accordion_item_margin", "card_border_thin"], self.spacing
1386
+ )
1387
+ if card_spacing_cls:
1388
+ card_cls_parts.append(card_spacing_cls)
1389
+
1390
+ card_cls = " ".join(card_cls_parts)
1351
1391
 
1352
1392
  return mui.AccordionItem(
1353
1393
  title_component, # Title as first positional argument
@@ -1370,10 +1410,19 @@ class ListFieldRenderer(BaseFieldRenderer):
1370
1410
  t = self.spacing
1371
1411
  content_wrapper = fh.Div(content_component, cls=spacing("card_body_pad", t))
1372
1412
 
1373
- # Build card classes conditionally based on spacing theme
1374
- card_cls = "uk-card uk-margin-small-bottom"
1413
+ # Build card classes using spacing tokens
1414
+ card_cls_parts = ["uk-card"]
1375
1415
  if self.spacing == SpacingTheme.NORMAL:
1376
- card_cls += " uk-card-default"
1416
+ card_cls_parts.append("uk-card-default")
1417
+
1418
+ # Add spacing-based classes
1419
+ card_spacing_cls = spacing_many(
1420
+ ["accordion_item_margin", "card_border_thin"], self.spacing
1421
+ )
1422
+ if card_spacing_cls:
1423
+ card_cls_parts.append(card_spacing_cls)
1424
+
1425
+ card_cls = " ".join(card_cls_parts)
1377
1426
 
1378
1427
  return mui.AccordionItem(
1379
1428
  title_component, # Title as first positional argument
@@ -32,7 +32,6 @@ from fh_pydantic_form.list_path import walk_path
32
32
  from fh_pydantic_form.registry import FieldRendererRegistry
33
33
  from fh_pydantic_form.type_helpers import _UNSET, get_default
34
34
  from fh_pydantic_form.ui_style import (
35
- COMPACT_EXTRA_CSS,
36
35
  SpacingTheme,
37
36
  SpacingValue,
38
37
  _normalize_spacing,
@@ -213,19 +212,10 @@ class PydanticForm(Generic[ModelType]):
213
212
 
214
213
  def _compact_wrapper(self, inner: FT) -> FT:
215
214
  """
216
- Wrap inner markup in a namespaced div.
217
- Auto-inject the compact CSS the *first* time any compact form is rendered.
215
+ Wrap inner markup in a wrapper div.
218
216
  """
219
217
  wrapper_cls = "fhpf-wrapper w-full flex-1"
220
-
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
- )
218
+ return fh.Div(inner, cls=wrapper_cls)
229
219
 
230
220
  def _clone_with_values(self, values: Dict[str, Any]) -> "PydanticForm":
231
221
  """
@@ -1,8 +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
5
-
6
4
 
7
5
  class SpacingTheme(Enum):
8
6
  NORMAL = auto()
@@ -44,13 +42,19 @@ SPACING_MAP: Dict[SpacingTheme, Dict[str, str]] = {
44
42
  "padding_sm": "p-3",
45
43
  "padding_card": "px-4 py-3",
46
44
  "card_border": "border",
45
+ "card_border_thin": "",
47
46
  "section_divider": "border-t border-gray-200",
48
47
  "accordion_divider": "uk-accordion-divider",
48
+ "accordion_title_pad": "",
49
+ "accordion_content_pad": "",
50
+ "accordion_item_margin": "uk-margin-small-bottom",
49
51
  "label_gap": "mb-1",
50
52
  "card_body_pad": "px-4 py-3",
51
53
  "accordion_content": "",
52
54
  "input_size": "",
53
55
  "input_padding": "",
56
+ "input_line_height": "",
57
+ "input_font_size": "",
54
58
  "horizontal_gap": "gap-3",
55
59
  "label_align": "items-start",
56
60
  },
@@ -64,13 +68,19 @@ SPACING_MAP: Dict[SpacingTheme, Dict[str, str]] = {
64
68
  "padding_sm": "p-0.5",
65
69
  "padding_card": "px-2 py-1",
66
70
  "card_border": "",
71
+ "card_border_thin": "",
67
72
  "section_divider": "",
68
73
  "accordion_divider": "",
74
+ "accordion_title_pad": "py-1",
75
+ "accordion_content_pad": "py-1",
76
+ "accordion_item_margin": "mb-0",
69
77
  "label_gap": "mb-0",
70
78
  "card_body_pad": "px-2 py-0.5",
71
79
  "accordion_content": "uk-padding-remove-vertical",
72
80
  "input_size": "uk-form-small",
73
- "input_padding": "p-1",
81
+ "input_padding": "py-0.5 px-1",
82
+ "input_line_height": "leading-tight",
83
+ "input_font_size": "text-sm",
74
84
  "horizontal_gap": "gap-2",
75
85
  "label_align": "items-start",
76
86
  },
@@ -83,72 +93,21 @@ def spacing(token: str, spacing: SpacingValue) -> str:
83
93
  return SPACING_MAP[theme][token]
84
94
 
85
95
 
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
88
- COMPACT_EXTRA_CSS = fh.Style("""
89
- /* Compact polish – applies ONLY inside .fhpf-compact ------------------- */
90
- .fhpf-compact {
91
- /* Force full width and left alignment */
92
- width: 100% !important;
93
-
94
- /* Ensure all direct children are full width and left aligned */
95
- & > * {
96
- width: 100% !important;
97
- justify-content: flex-start !important;
98
- align-items: flex-start !important;
99
- }
100
-
101
- /* Target the field containers specifically */
102
- & > div > div {
103
- width: 100% !important;
104
- justify-content: flex-start !important;
105
- }
106
-
107
- /* Ensure flex containers don't center */
108
- .flex {
109
- justify-content: flex-start !important;
110
- }
111
-
112
- /* Accordion chrome: remove border and default 20 px gap */
113
- .uk-accordion > li,
114
- .uk-accordion > li + li { /* second & later items */
115
- border-top: 0 !important;
116
- margin-top: 0 !important;
117
- }
118
- .uk-accordion-title::after { /* the hair-line we still see */
119
- border-top: 0 !important;
120
- }
121
-
122
- /* Tighter title and content padding */
123
- li > a.uk-accordion-title,
124
- .uk-accordion-content {
125
- padding-top: 0.25rem !important;
126
- padding-bottom: 0.25rem !important;
127
- }
128
-
129
- /* Remove residual card outline */
130
- .uk-card,
131
- .uk-card-body { border: 0 !important; }
96
+ def spacing_many(tokens: list[str], spacing: SpacingValue) -> str:
97
+ """
98
+ Return combined Tailwind utility classes for multiple semantic tokens.
132
99
 
133
- /* Small-size inputs */
134
- input, select, textarea {
135
- line-height: 1.25rem !important;
136
- font-size: 0.8125rem !important;
137
- padding-top: 0.25rem !important;
138
- padding-bottom: 0.25rem !important;
139
- }
100
+ Args:
101
+ tokens: List of spacing token names
102
+ spacing: Spacing theme to use
140
103
 
141
- /* Legacy uk-form-small support */
142
- input.uk-form-small,
143
- select.uk-form-small,
144
- textarea.uk-textarea-small {
145
- padding-top: 2px !important;
146
- padding-bottom: 2px !important;
147
- }
148
-
149
- /* Kill generic uk-margin utilities inside the form */
150
- .uk-margin-small-bottom,
151
- .uk-margin,
152
- .uk-margin-bottom { margin-bottom: 2px !important; }
153
- }
154
- """)
104
+ Returns:
105
+ String of space-separated CSS classes
106
+ """
107
+ theme = _normalize_spacing(spacing)
108
+ classes = []
109
+ for token in tokens:
110
+ class_value = SPACING_MAP[theme].get(token, "")
111
+ if class_value: # Only add non-empty class values
112
+ classes.append(class_value)
113
+ return " ".join(classes)