eidosui 0.8.0__tar.gz → 0.9.0__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.
Files changed (26) hide show
  1. {eidosui-0.8.0 → eidosui-0.9.0}/PKG-INFO +1 -1
  2. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/__init__.py +31 -1
  3. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/components/__init__.py +9 -1
  4. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/components/headers.py +4 -4
  5. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/components/table.py +4 -2
  6. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/components/tabs.py +22 -22
  7. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/css/styles.css +318 -0
  8. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/css/themes/dark.css +3 -0
  9. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/css/themes/eidos-variables.css +53 -0
  10. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/plugins/markdown/extensions/alerts.py +8 -6
  11. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/plugins/markdown/renderer.py +15 -3
  12. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/styles.py +33 -1
  13. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/tags.py +148 -0
  14. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/utils.py +1 -1
  15. {eidosui-0.8.0 → eidosui-0.9.0}/pyproject.toml +1 -1
  16. {eidosui-0.8.0 → eidosui-0.9.0}/.gitignore +0 -0
  17. {eidosui-0.8.0 → eidosui-0.9.0}/LICENSE +0 -0
  18. {eidosui-0.8.0 → eidosui-0.9.0}/README.md +0 -0
  19. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/components/navigation.py +0 -0
  20. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/css/themes/light.css +0 -0
  21. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/js/eidos.js +0 -0
  22. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/plugins/__init__.py +0 -0
  23. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/plugins/markdown/__init__.py +0 -0
  24. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/plugins/markdown/components.py +0 -0
  25. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/plugins/markdown/css/markdown.css +0 -0
  26. {eidosui-0.8.0 → eidosui-0.9.0}/eidos/plugins/markdown/extensions/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eidosui
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: A modern, Tailwind CSS-based UI library for air development
5
5
  Project-URL: Homepage, https://github.com/isaac-flath/EidosUI
6
6
  Project-URL: Repository, https://github.com/isaac-flath/EidosUI
@@ -44,12 +44,16 @@ from .tags import (
44
44
  Button,
45
45
  Canvas,
46
46
  Caption,
47
+ # Form elements
48
+ Checkbox,
47
49
  Cite,
48
50
  Code,
49
51
  Col,
50
52
  Colgroup,
53
+ ColorPicker,
51
54
  Data,
52
55
  Datalist,
56
+ DatePicker,
53
57
  Dd,
54
58
  Del,
55
59
  Details,
@@ -59,12 +63,16 @@ from .tags import (
59
63
  Dl,
60
64
  Dt,
61
65
  Em,
66
+ EmailInput,
62
67
  Embed,
63
68
  Fieldset,
64
69
  Figcaption,
65
70
  Figure,
71
+ FileInput,
66
72
  Footer,
67
73
  Form,
74
+ FormError,
75
+ FormHelp,
68
76
  Head,
69
77
  Header,
70
78
  Hgroup,
@@ -88,6 +96,7 @@ from .tags import (
88
96
  Meter,
89
97
  Nav,
90
98
  Noscript,
99
+ NumberInput,
91
100
  Object,
92
101
  Ol,
93
102
  Optgroup,
@@ -95,10 +104,12 @@ from .tags import (
95
104
  Output,
96
105
  P,
97
106
  Param,
107
+ PasswordInput,
98
108
  Picture,
99
109
  Pre,
100
110
  Progress,
101
111
  Q,
112
+ Radio,
102
113
  Rp,
103
114
  Rt,
104
115
  Ruby,
@@ -106,6 +117,7 @@ from .tags import (
106
117
  Samp,
107
118
  Script,
108
119
  Search,
120
+ SearchInput,
109
121
  Section,
110
122
  Select,
111
123
  Small,
@@ -121,24 +133,27 @@ from .tags import (
121
133
  Table,
122
134
  Tbody,
123
135
  Td,
136
+ TelInput,
124
137
  Template,
125
138
  Textarea,
126
139
  Tfoot,
127
140
  Th,
128
141
  Thead,
129
142
  Time,
143
+ TimePicker,
130
144
  Title,
131
145
  Tr,
132
146
  Track,
133
147
  U,
134
148
  Ul,
149
+ UrlInput,
135
150
  Var,
136
151
  Video,
137
152
  Wbr,
138
153
  )
139
154
 
140
155
  # Version info
141
- __version__ = "0.4.0"
156
+ __version__ = "0.9.0"
142
157
 
143
158
  # Define what's available with "from eidos import *"
144
159
  __all__ = [
@@ -267,4 +282,19 @@ __all__ = [
267
282
  "Ul",
268
283
  "Video",
269
284
  "Wbr",
285
+ # Form elements
286
+ "Checkbox",
287
+ "Radio",
288
+ "DatePicker",
289
+ "TimePicker",
290
+ "ColorPicker",
291
+ "NumberInput",
292
+ "EmailInput",
293
+ "PasswordInput",
294
+ "SearchInput",
295
+ "UrlInput",
296
+ "TelInput",
297
+ "FileInput",
298
+ "FormError",
299
+ "FormHelp",
270
300
  ]
@@ -8,4 +8,12 @@ from .navigation import NavBar
8
8
  from .table import DataTable
9
9
  from .tabs import TabContainer, TabList, TabPanel, Tabs
10
10
 
11
- __all__ = ["DataTable", "NavBar", "EidosHeaders", "TabContainer", "TabList", "TabPanel", "Tabs"]
11
+ __all__ = [
12
+ "DataTable",
13
+ "NavBar",
14
+ "EidosHeaders",
15
+ "TabContainer",
16
+ "TabList",
17
+ "TabPanel",
18
+ "Tabs",
19
+ ]
@@ -1,9 +1,9 @@
1
1
  from typing import Literal
2
2
 
3
- from air import Link, Meta, Script
3
+ from air import Link, Meta, Script, Tag
4
4
 
5
5
 
6
- def get_css_urls():
6
+ def get_css_urls() -> list[str]:
7
7
  """Return list of CSS URLs for EidosUI."""
8
8
  return [
9
9
  "/eidos/css/styles.css",
@@ -18,7 +18,7 @@ def EidosHeaders(
18
18
  include_lucide: bool = True,
19
19
  include_eidos_js: bool = True,
20
20
  theme: Literal["light", "dark"] = "light",
21
- ):
21
+ ) -> list[Tag]:
22
22
  """Complete EidosUI headers with EidosUI JavaScript support.
23
23
 
24
24
  Args:
@@ -37,7 +37,7 @@ def EidosHeaders(
37
37
  headers.append(Script(src="https://cdn.tailwindcss.com"))
38
38
 
39
39
  if include_lucide:
40
- headers.append(Script(src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"))
40
+ headers.append(Script(src="https://unpkg.com/lucide@latest"))
41
41
 
42
42
  # EidosUI CSS
43
43
  for css_url in get_css_urls():
@@ -1,5 +1,7 @@
1
1
  from typing import Any
2
2
 
3
+ from air import Tag
4
+
3
5
  from ..tags import Table as BaseTable
4
6
  from ..tags import Tbody, Td, Th, Thead, Tr
5
7
 
@@ -14,7 +16,7 @@ class DataTable:
14
16
  headers: list[str] | None = None,
15
17
  class_: str | list[str] | None = None,
16
18
  **kwargs: Any,
17
- ) -> Any:
19
+ ) -> Tag:
18
20
  """Create table from list of lists.
19
21
 
20
22
  Args:
@@ -50,7 +52,7 @@ class DataTable:
50
52
  headers: list[str] | None = None,
51
53
  class_: str | list[str] | None = None,
52
54
  **kwargs: Any,
53
- ) -> Any:
55
+ ) -> Tag:
54
56
  """Create table from list of dictionaries.
55
57
 
56
58
  Args:
@@ -1,3 +1,5 @@
1
+ from typing import Any, Literal
2
+
1
3
  from air import Button, Div, Tag
2
4
 
3
5
  from .. import styles
@@ -5,22 +7,22 @@ from ..utils import stringify
5
7
 
6
8
 
7
9
  def TabContainer(
8
- *content,
10
+ *content: Tag,
9
11
  initial_tab_url: str,
10
12
  class_: str = "",
11
13
  target_id: str = "tabs",
12
- **kwargs,
14
+ **kwargs: Any,
13
15
  ) -> Tag:
14
16
  """HTMX-based tab container that loads tabs dynamically.
15
-
17
+
16
18
  Args:
17
19
  initial_tab_url: URL to load the initial tab content
18
20
  cls: Additional classes for the container
19
21
  target_id: ID for the tab container (default: "tabs")
20
-
22
+
21
23
  Returns:
22
24
  Tag: The tab container that will be populated via HTMX
23
-
25
+
24
26
  Example:
25
27
  TabContainer("/settings/general")
26
28
  """
@@ -41,21 +43,23 @@ def TabList(
41
43
  selected: int = 0,
42
44
  class_: str = "",
43
45
  hx_target: str = "#tabs",
44
- hx_swap: str = "innerHTML",
45
- **kwargs,
46
+ hx_swap: Literal[
47
+ "innerHTML", "outerHTML", "beforebegin", "afterbegin", "beforeend", "afterend", "delete", "none"
48
+ ] = "innerHTML",
49
+ **kwargs: Any,
46
50
  ) -> Tag:
47
51
  """HTMX-based tab list for server-rendered tabs.
48
-
52
+
49
53
  Args:
50
54
  *tabs: Variable number of (label, url) tuples
51
55
  selected: Index of the selected tab (0-based)
52
56
  tab_cls: Additional classes for tab buttons
53
57
  hx_target: HTMX target for tab content (default: "#tabs")
54
58
  hx_swap: HTMX swap method (default: "innerHTML")
55
-
59
+
56
60
  Returns:
57
61
  Tag: The tab list component
58
-
62
+
59
63
  Example:
60
64
  TabList(
61
65
  ("General", "/settings/general"),
@@ -77,11 +81,7 @@ def TabList(
77
81
  role="tab",
78
82
  aria_selected="true" if is_selected else "false",
79
83
  aria_controls="tab-content",
80
- class_=stringify(
81
- styles.tabs.tab,
82
- styles.tabs.tab_active if is_selected else "",
83
- class_
84
- ),
84
+ class_=stringify(styles.tabs.tab, styles.tabs.tab_active if is_selected else "", class_),
85
85
  )
86
86
  tab_buttons.append(tab_button)
87
87
 
@@ -96,14 +96,14 @@ def TabList(
96
96
  def TabPanel(
97
97
  content: Tag,
98
98
  class_: str = "",
99
- **kwargs,
99
+ **kwargs: Any,
100
100
  ) -> Tag:
101
101
  """Tab panel content wrapper.
102
-
102
+
103
103
  Args:
104
104
  content: The content to display in the tab panel
105
105
  panel_cls: Additional classes for the panel
106
-
106
+
107
107
  Returns:
108
108
  Tag: The tab panel component
109
109
  """
@@ -120,18 +120,18 @@ def Tabs(
120
120
  tab_list: Tag,
121
121
  tab_panel: Tag,
122
122
  cls: str = "",
123
- **kwargs,
123
+ **kwargs: Any,
124
124
  ) -> Tag:
125
125
  """Complete tab component with list and panel.
126
-
126
+
127
127
  Args:
128
128
  tab_list: The TabList component
129
129
  tab_panel: The TabPanel component
130
130
  cls: Additional classes for the container
131
-
131
+
132
132
  Returns:
133
133
  Tag: The complete tabs component
134
-
134
+
135
135
  Example:
136
136
  # In your route handler:
137
137
  tab_list = TabList(
@@ -582,4 +582,322 @@
582
582
  opacity: 1;
583
583
  transform: translateY(0);
584
584
  transition: opacity var(--transition-normal), transform var(--transition-normal);
585
+ }
586
+
587
+ /* ===========================
588
+ Form Styles
589
+ =========================== */
590
+
591
+ /* Fieldset */
592
+ .eidos-fieldset {
593
+ border: var(--form-fieldset-border);
594
+ border-radius: var(--form-fieldset-radius);
595
+ padding: var(--form-fieldset-padding);
596
+ background-color: var(--form-fieldset-bg);
597
+ margin-bottom: var(--space-lg);
598
+ }
599
+
600
+ .eidos-fieldset legend {
601
+ font-size: var(--font-size-lg);
602
+ font-weight: var(--font-weight-semibold);
603
+ color: var(--color-text);
604
+ padding: 0 var(--space-sm);
605
+ }
606
+
607
+ /* Form Group */
608
+ .eidos-form-group {
609
+ margin-bottom: var(--space-lg);
610
+ }
611
+
612
+ /* Labels */
613
+ .eidos-label {
614
+ display: inline-block;
615
+ font-size: var(--form-label-font-size);
616
+ font-weight: var(--form-label-font-weight);
617
+ color: var(--form-label-color);
618
+ margin-bottom: var(--form-label-margin-bottom);
619
+ }
620
+
621
+ .eidos-label-inline {
622
+ display: inline-flex;
623
+ align-items: center;
624
+ gap: var(--space-sm);
625
+ cursor: pointer;
626
+ margin-bottom: 0;
627
+ color: var(--form-label-color);
628
+ font-size: var(--font-size-base);
629
+ }
630
+
631
+ /* Base Input Styles */
632
+ .eidos-input,
633
+ .eidos-textarea,
634
+ .eidos-select {
635
+ display: block;
636
+ width: 100%;
637
+ padding: var(--form-input-padding-y) var(--form-input-padding-x);
638
+ font-size: var(--font-size-base);
639
+ line-height: var(--line-height-normal);
640
+ color: var(--form-input-text);
641
+ background-color: var(--form-input-bg);
642
+ border: var(--form-input-border-width) solid var(--form-input-border-color);
643
+ border-radius: var(--form-input-radius);
644
+ box-shadow: var(--form-input-shadow);
645
+ transition: all var(--transition-fast);
646
+ outline: none;
647
+ }
648
+
649
+ .eidos-input {
650
+ height: var(--form-input-height);
651
+ }
652
+
653
+ .eidos-textarea {
654
+ min-height: calc(var(--form-input-height) * 2);
655
+ resize: vertical;
656
+ }
657
+
658
+ .eidos-select {
659
+ height: var(--form-input-height);
660
+ cursor: pointer;
661
+ padding-right: calc(var(--form-input-padding-x) * 2.5);
662
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
663
+ background-repeat: no-repeat;
664
+ background-position: right var(--form-input-padding-x) center;
665
+ background-size: 16px 12px;
666
+ appearance: none;
667
+ }
668
+
669
+ /* Input States */
670
+ .eidos-input:hover,
671
+ .eidos-textarea:hover,
672
+ .eidos-select:hover {
673
+ border-color: var(--form-input-border-color-hover);
674
+ }
675
+
676
+ .eidos-input:focus,
677
+ .eidos-textarea:focus,
678
+ .eidos-select:focus {
679
+ border-color: var(--form-input-border-color-focus);
680
+ box-shadow: var(--form-input-shadow-focus);
681
+ }
682
+
683
+ .eidos-input:disabled,
684
+ .eidos-textarea:disabled,
685
+ .eidos-select:disabled {
686
+ background-color: var(--form-input-bg-disabled);
687
+ color: var(--form-input-text-disabled);
688
+ cursor: not-allowed;
689
+ opacity: var(--opacity-60);
690
+ }
691
+
692
+ .eidos-input::placeholder,
693
+ .eidos-textarea::placeholder {
694
+ color: var(--form-input-placeholder);
695
+ opacity: 1;
696
+ }
697
+
698
+ /* Checkbox and Radio */
699
+ .eidos-checkbox,
700
+ .eidos-radio {
701
+ width: var(--form-checkbox-size);
702
+ height: var(--form-checkbox-size);
703
+ margin-right: var(--space-sm);
704
+ cursor: pointer;
705
+ flex-shrink: 0;
706
+ accent-color: var(--color-primary);
707
+ background-color: var(--form-checkbox-bg);
708
+ border: var(--form-input-border-width) solid var(--form-checkbox-border);
709
+ }
710
+
711
+ .eidos-checkbox:checked,
712
+ .eidos-radio:checked {
713
+ background-color: var(--form-checkbox-bg-checked);
714
+ border-color: var(--form-checkbox-bg-checked);
715
+ }
716
+
717
+ .eidos-radio {
718
+ border-radius: var(--radius-full);
719
+ }
720
+
721
+ /* Error States */
722
+ .eidos-input[aria-invalid="true"],
723
+ .eidos-textarea[aria-invalid="true"],
724
+ .eidos-select[aria-invalid="true"] {
725
+ border-color: var(--form-error-border-color);
726
+ }
727
+
728
+ .eidos-input[aria-invalid="true"]:focus,
729
+ .eidos-textarea[aria-invalid="true"]:focus,
730
+ .eidos-select[aria-invalid="true"]:focus {
731
+ border-color: var(--form-error-border-color);
732
+ box-shadow: 0 0 0 3px rgba(var(--color-error-rgb), 0.1);
733
+ }
734
+
735
+ /* Error Messages */
736
+ .eidos-error {
737
+ display: block;
738
+ font-size: var(--form-error-font-size);
739
+ color: var(--form-error-color);
740
+ margin-top: var(--form-help-margin-top);
741
+ }
742
+
743
+ /* Help Text */
744
+ .eidos-help {
745
+ display: block;
746
+ font-size: var(--form-help-font-size);
747
+ color: var(--form-help-color);
748
+ margin-top: var(--form-help-margin-top);
749
+ }
750
+
751
+ /* File Input */
752
+ .eidos-file {
753
+ font-size: var(--font-size-sm);
754
+ cursor: pointer;
755
+ }
756
+
757
+ .eidos-file::file-selector-button {
758
+ padding: var(--space-xs) var(--space-sm);
759
+ margin-right: var(--space-sm);
760
+ font-size: var(--font-size-sm);
761
+ font-weight: var(--font-weight-medium);
762
+ color: var(--color-primary);
763
+ background-color: var(--color-primary-light);
764
+ border: var(--border) solid var(--color-primary);
765
+ border-radius: var(--radius-md);
766
+ cursor: pointer;
767
+ transition: all var(--transition-fast);
768
+ }
769
+
770
+ .eidos-file::file-selector-button:hover {
771
+ background-color: var(--color-primary);
772
+ color: var(--color-primary-foreground);
773
+ }
774
+
775
+ /* Form Layout Helpers */
776
+ .eidos-form-row {
777
+ display: flex;
778
+ gap: var(--space-md);
779
+ margin-bottom: var(--space-lg);
780
+ }
781
+
782
+ .eidos-form-col {
783
+ flex: 1;
784
+ }
785
+
786
+ /* Input Groups */
787
+ .eidos-input-group {
788
+ display: flex;
789
+ align-items: stretch;
790
+ }
791
+
792
+ .eidos-input-group .eidos-input {
793
+ border-radius: 0;
794
+ }
795
+
796
+ .eidos-input-group .eidos-input:first-child {
797
+ border-top-left-radius: var(--form-input-radius);
798
+ border-bottom-left-radius: var(--form-input-radius);
799
+ }
800
+
801
+ .eidos-input-group .eidos-input:last-child {
802
+ border-top-right-radius: var(--form-input-radius);
803
+ border-bottom-right-radius: var(--form-input-radius);
804
+ }
805
+
806
+ .eidos-input-addon {
807
+ display: flex;
808
+ align-items: center;
809
+ padding: var(--form-input-padding-y) var(--form-input-padding-x);
810
+ font-size: var(--font-size-base);
811
+ font-weight: var(--font-weight-normal);
812
+ color: var(--color-text-muted);
813
+ background-color: var(--color-surface);
814
+ border: var(--form-input-border-width) solid var(--form-input-border-color);
815
+ }
816
+
817
+ .eidos-input-addon:first-child {
818
+ border-right: 0;
819
+ border-radius: var(--form-input-radius) 0 0 var(--form-input-radius);
820
+ }
821
+
822
+ .eidos-input-addon:last-child {
823
+ border-left: 0;
824
+ border-radius: 0 var(--form-input-radius) var(--form-input-radius) 0;
825
+ }
826
+
827
+
828
+ /* Color Input */
829
+ .eidos-input[type="color"] {
830
+ padding: var(--space-xs);
831
+ cursor: pointer;
832
+ }
833
+
834
+ .eidos-input[type="color"]::-webkit-color-swatch-wrapper {
835
+ padding: 0;
836
+ }
837
+
838
+ .eidos-input[type="color"]::-webkit-color-swatch {
839
+ border: none;
840
+ border-radius: var(--radius-sm);
841
+ }
842
+
843
+ /* Date and Time Input Styling */
844
+ .eidos-input[type="date"],
845
+ .eidos-input[type="time"],
846
+ .eidos-input[type="datetime-local"],
847
+ .eidos-input[type="month"],
848
+ .eidos-input[type="week"] {
849
+ color-scheme: light dark;
850
+ background-color: var(--form-datetime-bg);
851
+ color: var(--form-datetime-text);
852
+ }
853
+
854
+ /* Style the calendar/time picker icon */
855
+ .eidos-input[type="date"]::-webkit-calendar-picker-indicator,
856
+ .eidos-input[type="time"]::-webkit-calendar-picker-indicator,
857
+ .eidos-input[type="datetime-local"]::-webkit-calendar-picker-indicator,
858
+ .eidos-input[type="month"]::-webkit-calendar-picker-indicator,
859
+ .eidos-input[type="week"]::-webkit-calendar-picker-indicator {
860
+ cursor: pointer;
861
+ opacity: 0.8;
862
+ filter: var(--color-scheme-filter, none);
863
+ transition: opacity var(--transition-fast);
864
+ }
865
+
866
+ .eidos-input[type="date"]:hover::-webkit-calendar-picker-indicator,
867
+ .eidos-input[type="time"]:hover::-webkit-calendar-picker-indicator,
868
+ .eidos-input[type="datetime-local"]:hover::-webkit-calendar-picker-indicator,
869
+ .eidos-input[type="month"]:hover::-webkit-calendar-picker-indicator,
870
+ .eidos-input[type="week"]:hover::-webkit-calendar-picker-indicator {
871
+ opacity: 1;
872
+ }
873
+
874
+ /* Dark theme filter */
875
+ [data-theme="dark"] .eidos-input[type="date"]::-webkit-calendar-picker-indicator,
876
+ [data-theme="dark"] .eidos-input[type="time"]::-webkit-calendar-picker-indicator,
877
+ [data-theme="dark"] .eidos-input[type="datetime-local"]::-webkit-calendar-picker-indicator,
878
+ [data-theme="dark"] .eidos-input[type="month"]::-webkit-calendar-picker-indicator,
879
+ [data-theme="dark"] .eidos-input[type="week"]::-webkit-calendar-picker-indicator {
880
+ filter: invert(1);
881
+ }
882
+
883
+ /* Better Select Dropdown Styling */
884
+ .eidos-select option {
885
+ background-color: var(--color-background);
886
+ color: var(--color-text);
887
+ padding: var(--space-sm);
888
+ }
889
+
890
+ .eidos-select option:hover,
891
+ .eidos-select option:focus {
892
+ background-color: var(--color-surface);
893
+ }
894
+
895
+ .eidos-select option:checked {
896
+ background-color: var(--color-primary);
897
+ color: var(--color-primary-foreground);
898
+ }
899
+
900
+ .eidos-select option:disabled {
901
+ color: var(--color-text-muted);
902
+ opacity: 0.6;
585
903
  }
@@ -74,6 +74,9 @@
74
74
  --shadow-xl: 0 0 0 1px rgb(255 255 255 / 0.1), 0 20px 25px -5px rgb(0 0 0 / 0.3);
75
75
  --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.5);
76
76
  --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.3);
77
+
78
+ /* Dark mode specific */
79
+ --invert-icon: 1;
77
80
  }
78
81
 
79
82
  /* Dark theme button overrides for better aesthetics */
@@ -72,6 +72,8 @@
72
72
  --color-accent-rgb: 168, 85, 247;
73
73
  --color-surface-rgb: 248, 250, 252;
74
74
  --color-background-rgb: 255, 255, 255;
75
+ --color-error-rgb: 220, 38, 38;
76
+ --color-success-rgb: 16, 185, 129;
75
77
 
76
78
  /* Spacing Scale */
77
79
  --space-xs: 0.25rem; /* 4px */
@@ -187,4 +189,55 @@
187
189
  --breakpoint-lg: 1024px;
188
190
  --breakpoint-xl: 1280px;
189
191
  --breakpoint-2xl: 1536px;
192
+
193
+ /* Form-specific Variables */
194
+ --form-input-height: 2.5rem; /* 40px */
195
+ --form-input-padding-x: var(--space-md);
196
+ --form-input-padding-y: var(--space-sm);
197
+ --form-input-border-width: var(--border);
198
+ --form-input-border-color: var(--color-border);
199
+ --form-input-border-color-hover: var(--color-border-hover);
200
+ --form-input-border-color-focus: var(--color-primary);
201
+ --form-input-bg: var(--color-input);
202
+ --form-input-bg-disabled: var(--color-surface);
203
+ --form-input-text: var(--color-text);
204
+ --form-input-text-disabled: var(--color-text-muted);
205
+ --form-input-placeholder: var(--color-text-subtle);
206
+ --form-input-radius: var(--radius-md);
207
+ --form-input-shadow: var(--shadow-xs);
208
+ --form-input-shadow-focus: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1);
209
+
210
+ --form-label-font-size: var(--font-size-sm);
211
+ --form-label-font-weight: var(--font-weight-medium);
212
+ --form-label-color: var(--color-text);
213
+ --form-label-margin-bottom: var(--space-xs);
214
+
215
+ --form-help-font-size: var(--font-size-sm);
216
+ --form-help-color: var(--color-text-muted);
217
+ --form-help-margin-top: var(--space-xs);
218
+
219
+ --form-error-color: var(--color-error);
220
+ --form-error-border-color: var(--color-error);
221
+ --form-error-bg: var(--color-error-light);
222
+ --form-error-font-size: var(--font-size-sm);
223
+
224
+ --form-fieldset-border: var(--border) solid var(--color-border);
225
+ --form-fieldset-padding: var(--space-lg);
226
+ --form-fieldset-radius: var(--radius-lg);
227
+ --form-fieldset-bg: var(--color-surface);
228
+
229
+ --form-checkbox-size: 1.25rem; /* 20px */
230
+ --form-radio-size: 1.25rem; /* 20px */
231
+ --form-checkbox-bg: var(--color-background);
232
+ --form-checkbox-bg-checked: var(--color-primary);
233
+ --form-checkbox-border: var(--color-border);
234
+ --form-checkbox-check-color: var(--color-primary-foreground);
235
+
236
+ /* Date/Time Picker Variables */
237
+ --form-datetime-bg: var(--color-background);
238
+ --form-datetime-text: var(--color-text);
239
+ --form-datetime-border: var(--color-border);
240
+ --form-datetime-button-bg: var(--color-surface);
241
+ --form-datetime-button-hover: var(--color-border);
242
+ --form-datetime-icon-color: var(--color-text-muted);
190
243
  }
@@ -2,8 +2,10 @@
2
2
 
3
3
  import re
4
4
  import xml.etree.ElementTree as etree
5
- from xml.etree.ElementTree import SubElement
5
+ from re import Pattern
6
+ from xml.etree.ElementTree import Element, SubElement
6
7
 
8
+ from markdown import Markdown
7
9
  from markdown.blockprocessors import BlockProcessor
8
10
  from markdown.extensions import Extension
9
11
 
@@ -12,10 +14,10 @@ class AlertBlockProcessor(BlockProcessor):
12
14
  """Process GitHub-style alert blocks"""
13
15
 
14
16
  # Pattern to match > [!TYPE] at the start of a blockquote
15
- RE_ALERT = re.compile(r"^> \[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]", re.MULTILINE)
17
+ RE_ALERT: Pattern[str] = re.compile(r"^> \[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]", re.MULTILINE)
16
18
 
17
19
  # Alert type configurations
18
- ALERT_TYPES = {
20
+ ALERT_TYPES: dict[str, dict[str, str]] = {
19
21
  "NOTE": {"class": "eidos-alert eidos-alert-info", "icon": "ℹ️", "title": "Note"},
20
22
  "TIP": {
21
23
  "class": "eidos-alert eidos-alert-success",
@@ -39,11 +41,11 @@ class AlertBlockProcessor(BlockProcessor):
39
41
  },
40
42
  }
41
43
 
42
- def test(self, parent, block):
44
+ def test(self, parent: Element, block: str) -> bool:
43
45
  """Test if the block is a GitHub-style alert"""
44
46
  return bool(self.RE_ALERT.match(block))
45
47
 
46
- def run(self, parent, blocks):
48
+ def run(self, parent: Element, blocks: list[str]) -> bool:
47
49
  """Process the alert block"""
48
50
  block = blocks.pop(0)
49
51
 
@@ -115,7 +117,7 @@ class AlertBlockProcessor(BlockProcessor):
115
117
  class AlertExtension(Extension):
116
118
  """Add GitHub-style alerts to markdown"""
117
119
 
118
- def extendMarkdown(self, md):
120
+ def extendMarkdown(self, md: Markdown) -> None:
119
121
  """Add the alert processor to the markdown instance"""
120
122
  md.parser.blockprocessors.register(
121
123
  AlertBlockProcessor(md.parser),
@@ -6,7 +6,16 @@ from .extensions.alerts import AlertExtension
6
6
 
7
7
 
8
8
  class MarkdownRenderer:
9
- """Core markdown rendering with theme integration"""
9
+ """Core markdown rendering with theme integration.
10
+
11
+ Warning:
12
+ This renderer outputs raw HTML without sanitization to support advanced
13
+ features like forms, embeds, and custom styling. Never use with untrusted
14
+ user content without additional sanitization.
15
+ """
16
+
17
+ extensions: list[str | markdown.Extension]
18
+ md: markdown.Markdown
10
19
 
11
20
  def __init__(self, extensions: list[str | markdown.Extension] | None = None):
12
21
  """Initialize the renderer with optional extensions.
@@ -36,13 +45,16 @@ class MarkdownRenderer:
36
45
  Returns:
37
46
  HTML string wrapped with eidos-md class for styling
38
47
  """
39
- self.md.reset() # TODO: this is a hack to clear the state of the markdown processor
48
+ # Reset markdown processor state to prevent contamination between renders
49
+ # This is required by Python-Markdown when reusing instances, especially
50
+ # with stateful extensions like footnotes or custom parsers
51
+ self.md.reset()
40
52
 
41
53
  html_content = self.md.convert(markdown_text)
42
54
 
43
55
  return f'<div class="eidos-md">{html_content}</div>'
44
56
 
45
- def add_extension(self, extension: str) -> None:
57
+ def add_extension(self, extension: str | markdown.Extension) -> None:
46
58
  """Add a markdown extension.
47
59
 
48
60
  Args:
@@ -39,7 +39,6 @@ class Typography:
39
39
  h5: Final[str] = "eidos-h5"
40
40
  h6: Final[str] = "eidos-h6"
41
41
 
42
-
43
42
  # Text formatting
44
43
  strong: Final[str] = "eidos-strong"
45
44
  i: Final[str] = "eidos-i"
@@ -119,9 +118,42 @@ class Tabs:
119
118
  panel_active: Final[str] = "eidos-tab-panel-active"
120
119
 
121
120
 
121
+ class Forms:
122
+ """Form-related CSS classes from styles.css."""
123
+
124
+ # Container elements
125
+ fieldset: Final[str] = "eidos-fieldset"
126
+ form_group: Final[str] = "eidos-form-group"
127
+
128
+ # Labels
129
+ label: Final[str] = "eidos-label"
130
+ label_inline: Final[str] = "eidos-label-inline"
131
+
132
+ # Input elements
133
+ input: Final[str] = "eidos-input"
134
+ textarea: Final[str] = "eidos-textarea"
135
+ select: Final[str] = "eidos-select"
136
+ checkbox: Final[str] = "eidos-checkbox"
137
+ radio: Final[str] = "eidos-radio"
138
+ file: Final[str] = "eidos-file"
139
+
140
+ # Feedback elements
141
+ error: Final[str] = "eidos-error"
142
+ help: Final[str] = "eidos-help"
143
+
144
+ # Layout helpers
145
+ form_row: Final[str] = "eidos-form-row"
146
+ form_col: Final[str] = "eidos-form-col"
147
+
148
+ # Input groups
149
+ input_group: Final[str] = "eidos-input-group"
150
+ input_addon: Final[str] = "eidos-input-addon"
151
+
152
+
122
153
  # Create singleton instance for easy access
123
154
  buttons = Buttons()
124
155
  typography = Typography()
125
156
  tables = Tables()
126
157
  lists = Lists()
127
158
  tabs = Tabs()
159
+ forms = Forms()
@@ -2,6 +2,7 @@ from typing import Any
2
2
 
3
3
  import air
4
4
  from air.tags import *
5
+
5
6
  from . import styles
6
7
  from .utils import stringify
7
8
 
@@ -247,6 +248,153 @@ def Li(*content: Any, class_: str | list[str] | None = None, **kwargs: Any) -> a
247
248
  return air.Li(*content, class_=stringify(styles.lists.li, class_), **kwargs)
248
249
 
249
250
 
251
+ # Form elements with default styling
252
+ def Fieldset(*content: Any, class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
253
+ """Styled fieldset element."""
254
+ return air.Fieldset(*content, class_=stringify(styles.forms.fieldset, class_), **kwargs)
255
+
256
+
257
+ def Label(*content: Any, class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
258
+ """Styled label element."""
259
+ return air.Label(*content, class_=stringify(styles.forms.label, class_), **kwargs)
260
+
261
+
262
+ def Input(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
263
+ """Styled input element."""
264
+ input_type = kwargs.get("type", "text")
265
+
266
+ # Apply appropriate class based on input type
267
+ if input_type == "checkbox":
268
+ default_class = styles.forms.checkbox
269
+ elif input_type == "radio":
270
+ default_class = styles.forms.radio
271
+ elif input_type == "file":
272
+ default_class = styles.forms.file
273
+ else:
274
+ default_class = styles.forms.input
275
+
276
+ return air.Input(class_=stringify(default_class, class_), **kwargs)
277
+
278
+
279
+ def Textarea(*content: Any, class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
280
+ """Styled textarea element."""
281
+ return air.Textarea(*content, class_=stringify(styles.forms.textarea, class_), **kwargs)
282
+
283
+
284
+ def Select(*content: Any, class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
285
+ """Styled select element."""
286
+ return air.Select(*content, class_=stringify(styles.forms.select, class_), **kwargs)
287
+
288
+
289
+ def Option(*content: Any, **kwargs: Any) -> air.Tag:
290
+ """Option element."""
291
+ return air.Option(*content, **kwargs)
292
+
293
+
294
+ def DatePicker(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
295
+ """Styled date input."""
296
+ return air.Input(type="date", class_=stringify(styles.forms.input, class_), **kwargs)
297
+
298
+
299
+ def TimePicker(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
300
+ """Styled time input."""
301
+ return air.Input(type="time", class_=stringify(styles.forms.input, class_), **kwargs)
302
+
303
+
304
+ def ColorPicker(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
305
+ """Styled color input."""
306
+ return air.Input(type="color", class_=stringify(styles.forms.input, class_), **kwargs)
307
+
308
+
309
+ def NumberInput(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
310
+ """Styled number input."""
311
+ return air.Input(type="number", class_=stringify(styles.forms.input, class_), **kwargs)
312
+
313
+
314
+ def EmailInput(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
315
+ """Styled email input."""
316
+ return air.Input(type="email", class_=stringify(styles.forms.input, class_), **kwargs)
317
+
318
+
319
+ def PasswordInput(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
320
+ """Styled password input."""
321
+ return air.Input(type="password", class_=stringify(styles.forms.input, class_), **kwargs)
322
+
323
+
324
+ def SearchInput(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
325
+ """Styled search input."""
326
+ return air.Input(type="search", class_=stringify(styles.forms.input, class_), **kwargs)
327
+
328
+
329
+ def UrlInput(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
330
+ """Styled URL input."""
331
+ return air.Input(type="url", class_=stringify(styles.forms.input, class_), **kwargs)
332
+
333
+
334
+ def TelInput(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
335
+ """Styled telephone input."""
336
+ return air.Input(type="tel", class_=stringify(styles.forms.input, class_), **kwargs)
337
+
338
+
339
+ def Checkbox(
340
+ name: str | None = None, label: str | None = None, class_: str | list[str] | None = None, **kwargs: Any
341
+ ) -> air.Tag:
342
+ """Styled checkbox input with optional label."""
343
+ # Generate ID if not provided
344
+ input_id = kwargs.get("id", f"{name}-{kwargs.get('value', 'checkbox')}".replace(" ", "-") if name else None)
345
+
346
+ # Create checkbox
347
+ checkbox = air.Input(type="checkbox", name=name, id=input_id, class_=stringify(styles.forms.checkbox), **kwargs)
348
+
349
+ if label:
350
+ # Wrap in label
351
+ return Label(checkbox, label, for_=input_id, class_=stringify(styles.forms.label_inline, class_))
352
+ else:
353
+ # Apply additional classes if provided
354
+ if class_:
355
+ checkbox = air.Input(
356
+ type="checkbox", name=name, id=input_id, class_=stringify(styles.forms.checkbox, class_), **kwargs
357
+ )
358
+ return checkbox
359
+
360
+
361
+ def Radio(
362
+ name: str | None = None, label: str | None = None, class_: str | list[str] | None = None, **kwargs: Any
363
+ ) -> air.Tag:
364
+ """Styled radio input with optional label."""
365
+ # Generate ID if not provided
366
+ input_id = kwargs.get("id", f"{name}-{kwargs.get('value', 'radio')}".replace(" ", "-") if name else None)
367
+
368
+ # Create radio
369
+ radio = air.Input(type="radio", name=name, id=input_id, class_=stringify(styles.forms.radio), **kwargs)
370
+
371
+ if label:
372
+ # Wrap in label
373
+ return Label(radio, label, for_=input_id, class_=stringify(styles.forms.label_inline, class_))
374
+ else:
375
+ # Apply additional classes if provided
376
+ if class_:
377
+ radio = air.Input(
378
+ type="radio", name=name, id=input_id, class_=stringify(styles.forms.radio, class_), **kwargs
379
+ )
380
+ return radio
381
+
382
+
383
+ def FileInput(class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
384
+ """Styled file input."""
385
+ return air.Input(type="file", class_=stringify(styles.forms.file, class_), **kwargs)
386
+
387
+
388
+ # Helper form elements
389
+ def FormError(text: str, class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
390
+ """Styled error message."""
391
+ return Small(text, class_=stringify(styles.forms.error, class_), **kwargs)
392
+
393
+
394
+ def FormHelp(text: str, class_: str | list[str] | None = None, **kwargs: Any) -> air.Tag:
395
+ """Styled help text."""
396
+ return Small(text, class_=stringify(styles.forms.help, class_), **kwargs)
397
+
250
398
 
251
399
  # Pass-through tags from air.tags
252
400
  # Import all standard HTML tags that don't have custom styling
@@ -37,7 +37,7 @@ def stringify(*classes: str | list[str] | None) -> str:
37
37
  return " ".join(result)
38
38
 
39
39
 
40
- def get_eidos_static_files(markdown: bool = False) -> dict:
40
+ def get_eidos_static_files(markdown: bool = False) -> dict[str, str]:
41
41
  """
42
42
  Get a dictionary mapping URL paths to static file directories.
43
43
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "eidosui"
7
- version = "0.8.0"
7
+ version = "0.9.0"
8
8
  description = "A modern, Tailwind CSS-based UI library for air development"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes