fh-matui 0.9__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 ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.9"
fh_matui/_modidx.py ADDED
@@ -0,0 +1,183 @@
1
+ # Autogenerated by nbdev
2
+
3
+ d = { 'settings': { 'branch': 'master',
4
+ 'doc_baseurl': '/fh-matui',
5
+ 'doc_host': 'https://abhisheksreesaila.github.io',
6
+ 'git_url': 'https://github.com/abhisheksreesaila/fh-matui',
7
+ 'lib_path': 'fh_matui'},
8
+ 'syms': { 'fh_matui.app_pages': {'fh_matui.app_pages.LoginScreen': ('app_pages.html#loginscreen', 'fh_matui/app_pages.py')},
9
+ 'fh_matui.components': { 'fh_matui.components.Abbr': ('components.html#abbr', 'fh_matui/components.py'),
10
+ 'fh_matui.components.Blockquote': ('components.html#blockquote', 'fh_matui/components.py'),
11
+ 'fh_matui.components.BottomNav': ('components.html#bottomnav', 'fh_matui/components.py'),
12
+ 'fh_matui.components.Card': ('components.html#card', 'fh_matui/components.py'),
13
+ 'fh_matui.components.CheckboxX': ('components.html#checkboxx', 'fh_matui/components.py'),
14
+ 'fh_matui.components.CodeBlock': ('components.html#codeblock', 'fh_matui/components.py'),
15
+ 'fh_matui.components.CodeSpan': ('components.html#codespan', 'fh_matui/components.py'),
16
+ 'fh_matui.components.ContainerT': ('components.html#containert', 'fh_matui/components.py'),
17
+ 'fh_matui.components.CookiesBanner': ('components.html#cookiesbanner', 'fh_matui/components.py'),
18
+ 'fh_matui.components.DivCentered': ('components.html#divcentered', 'fh_matui/components.py'),
19
+ 'fh_matui.components.DivFullySpaced': ('components.html#divfullyspaced', 'fh_matui/components.py'),
20
+ 'fh_matui.components.DivHStacked': ('components.html#divhstacked', 'fh_matui/components.py'),
21
+ 'fh_matui.components.DivLAligned': ('components.html#divlaligned', 'fh_matui/components.py'),
22
+ 'fh_matui.components.DivRAligned': ('components.html#divraligned', 'fh_matui/components.py'),
23
+ 'fh_matui.components.DivVStacked': ('components.html#divvstacked', 'fh_matui/components.py'),
24
+ 'fh_matui.components.Em': ('components.html#em', 'fh_matui/components.py'),
25
+ 'fh_matui.components.FAQItem': ('components.html#faqitem', 'fh_matui/components.py'),
26
+ 'fh_matui.components.Field': ('components.html#field', 'fh_matui/components.py'),
27
+ 'fh_matui.components.FormField': ('components.html#formfield', 'fh_matui/components.py'),
28
+ 'fh_matui.components.FormGrid': ('components.html#formgrid', 'fh_matui/components.py'),
29
+ 'fh_matui.components.FormLabel': ('components.html#formlabel', 'fh_matui/components.py'),
30
+ 'fh_matui.components.FormModal': ('components.html#formmodal', 'fh_matui/components.py'),
31
+ 'fh_matui.components.Grid': ('components.html#grid', 'fh_matui/components.py'),
32
+ 'fh_matui.components.GridCell': ('components.html#gridcell', 'fh_matui/components.py'),
33
+ 'fh_matui.components.GridSpanT': ('components.html#gridspant', 'fh_matui/components.py'),
34
+ 'fh_matui.components.Icon': ('components.html#icon', 'fh_matui/components.py'),
35
+ 'fh_matui.components.LabelInput': ('components.html#labelinput', 'fh_matui/components.py'),
36
+ 'fh_matui.components.Layout': ('components.html#layout', 'fh_matui/components.py'),
37
+ 'fh_matui.components.LoadingIndicator': ('components.html#loadingindicator', 'fh_matui/components.py'),
38
+ 'fh_matui.components.Mark': ('components.html#mark', 'fh_matui/components.py'),
39
+ 'fh_matui.components.Modal': ('components.html#modal', 'fh_matui/components.py'),
40
+ 'fh_matui.components.ModalBody': ('components.html#modalbody', 'fh_matui/components.py'),
41
+ 'fh_matui.components.ModalButton': ('components.html#modalbutton', 'fh_matui/components.py'),
42
+ 'fh_matui.components.ModalCancel': ('components.html#modalcancel', 'fh_matui/components.py'),
43
+ 'fh_matui.components.ModalConfirm': ('components.html#modalconfirm', 'fh_matui/components.py'),
44
+ 'fh_matui.components.ModalFooter': ('components.html#modalfooter', 'fh_matui/components.py'),
45
+ 'fh_matui.components.ModalTitle': ('components.html#modaltitle', 'fh_matui/components.py'),
46
+ 'fh_matui.components.NavBar': ('components.html#navbar', 'fh_matui/components.py'),
47
+ 'fh_matui.components.NavCloseLi': ('components.html#navcloseli', 'fh_matui/components.py'),
48
+ 'fh_matui.components.NavContainer': ('components.html#navcontainer', 'fh_matui/components.py'),
49
+ 'fh_matui.components.NavDividerLi': ('components.html#navdividerli', 'fh_matui/components.py'),
50
+ 'fh_matui.components.NavHeaderLi': ('components.html#navheaderli', 'fh_matui/components.py'),
51
+ 'fh_matui.components.NavSideBarContainer': ( 'components.html#navsidebarcontainer',
52
+ 'fh_matui/components.py'),
53
+ 'fh_matui.components.NavSideBarHeader': ('components.html#navsidebarheader', 'fh_matui/components.py'),
54
+ 'fh_matui.components.NavSideBarLinks': ('components.html#navsidebarlinks', 'fh_matui/components.py'),
55
+ 'fh_matui.components.NavSubtitle': ('components.html#navsubtitle', 'fh_matui/components.py'),
56
+ 'fh_matui.components.NavToggleButton': ('components.html#navtogglebutton', 'fh_matui/components.py'),
57
+ 'fh_matui.components.Pagination': ('components.html#pagination', 'fh_matui/components.py'),
58
+ 'fh_matui.components.Progress': ('components.html#progress', 'fh_matui/components.py'),
59
+ 'fh_matui.components.Q': ('components.html#q', 'fh_matui/components.py'),
60
+ 'fh_matui.components.Radio': ('components.html#radio', 'fh_matui/components.py'),
61
+ 'fh_matui.components.Range': ('components.html#range', 'fh_matui/components.py'),
62
+ 'fh_matui.components.Select': ('components.html#select', 'fh_matui/components.py'),
63
+ 'fh_matui.components.Small': ('components.html#small', 'fh_matui/components.py'),
64
+ 'fh_matui.components.Snackbar': ('components.html#snackbar', 'fh_matui/components.py'),
65
+ 'fh_matui.components.SpaceT': ('components.html#spacet', 'fh_matui/components.py'),
66
+ 'fh_matui.components.Strong': ('components.html#strong', 'fh_matui/components.py'),
67
+ 'fh_matui.components.Sub': ('components.html#sub', 'fh_matui/components.py'),
68
+ 'fh_matui.components.Sup': ('components.html#sup', 'fh_matui/components.py'),
69
+ 'fh_matui.components.Switch': ('components.html#switch', 'fh_matui/components.py'),
70
+ 'fh_matui.components.Table': ('components.html#table', 'fh_matui/components.py'),
71
+ 'fh_matui.components.TableControls': ('components.html#tablecontrols', 'fh_matui/components.py'),
72
+ 'fh_matui.components.TableFromDicts': ('components.html#tablefromdicts', 'fh_matui/components.py'),
73
+ 'fh_matui.components.TableFromLists': ('components.html#tablefromlists', 'fh_matui/components.py'),
74
+ 'fh_matui.components.Tbody': ('components.html#tbody', 'fh_matui/components.py'),
75
+ 'fh_matui.components.Td': ('components.html#td', 'fh_matui/components.py'),
76
+ 'fh_matui.components.TextArea': ('components.html#textarea', 'fh_matui/components.py'),
77
+ 'fh_matui.components.TextPresets': ('components.html#textpresets', 'fh_matui/components.py'),
78
+ 'fh_matui.components.TextT': ('components.html#textt', 'fh_matui/components.py'),
79
+ 'fh_matui.components.Tfoot': ('components.html#tfoot', 'fh_matui/components.py'),
80
+ 'fh_matui.components.Th': ('components.html#th', 'fh_matui/components.py'),
81
+ 'fh_matui.components.Thead': ('components.html#thead', 'fh_matui/components.py'),
82
+ 'fh_matui.components.Toast': ('components.html#toast', 'fh_matui/components.py'),
83
+ 'fh_matui.components.Toolbar': ('components.html#toolbar', 'fh_matui/components.py'),
84
+ 'fh_matui.components._AnchorChain': ('components.html#_anchorchain', 'fh_matui/components.py'),
85
+ 'fh_matui.components._ButtonChain': ('components.html#_buttonchain', 'fh_matui/components.py'),
86
+ 'fh_matui.components._get_form_config': ('components.html#_get_form_config', 'fh_matui/components.py'),
87
+ 'fh_matui.components._has_space_token': ('components.html#_has_space_token', 'fh_matui/components.py'),
88
+ 'fh_matui.components._make_anchor_property': ( 'components.html#_make_anchor_property',
89
+ 'fh_matui/components.py'),
90
+ 'fh_matui.components._make_button_property': ( 'components.html#_make_button_property',
91
+ 'fh_matui/components.py'),
92
+ 'fh_matui.components._snap_to_valid_cols': ( 'components.html#_snap_to_valid_cols',
93
+ 'fh_matui/components.py'),
94
+ 'fh_matui.components._wrap_grid_children': ( 'components.html#_wrap_grid_children',
95
+ 'fh_matui/components.py')},
96
+ 'fh_matui.core': { 'fh_matui.core.BeerCssChain': ('core.html#beercsschain', 'fh_matui/core.py'),
97
+ 'fh_matui.core.BeerCssChain.__init__': ('core.html#beercsschain.__init__', 'fh_matui/core.py'),
98
+ 'fh_matui.core.BeerCssChain.__iter__': ('core.html#beercsschain.__iter__', 'fh_matui/core.py'),
99
+ 'fh_matui.core.BeerCssChain.__repr__': ('core.html#beercsschain.__repr__', 'fh_matui/core.py'),
100
+ 'fh_matui.core.BeerCssChain.__str__': ('core.html#beercsschain.__str__', 'fh_matui/core.py'),
101
+ 'fh_matui.core._ThemeChain': ('core.html#_themechain', 'fh_matui/core.py'),
102
+ 'fh_matui.core._ThemeChain.__init__': ('core.html#_themechain.__init__', 'fh_matui/core.py'),
103
+ 'fh_matui.core._ThemeChain.headers': ('core.html#_themechain.headers', 'fh_matui/core.py'),
104
+ 'fh_matui.core._ThemeNamespace': ('core.html#_themenamespace', 'fh_matui/core.py'),
105
+ 'fh_matui.core._ThemeNamespace.amber': ('core.html#_themenamespace.amber', 'fh_matui/core.py'),
106
+ 'fh_matui.core._ThemeNamespace.blue': ('core.html#_themenamespace.blue', 'fh_matui/core.py'),
107
+ 'fh_matui.core._ThemeNamespace.blue_grey': ('core.html#_themenamespace.blue_grey', 'fh_matui/core.py'),
108
+ 'fh_matui.core._ThemeNamespace.brown': ('core.html#_themenamespace.brown', 'fh_matui/core.py'),
109
+ 'fh_matui.core._ThemeNamespace.cyan': ('core.html#_themenamespace.cyan', 'fh_matui/core.py'),
110
+ 'fh_matui.core._ThemeNamespace.deep_orange': ('core.html#_themenamespace.deep_orange', 'fh_matui/core.py'),
111
+ 'fh_matui.core._ThemeNamespace.deep_purple': ('core.html#_themenamespace.deep_purple', 'fh_matui/core.py'),
112
+ 'fh_matui.core._ThemeNamespace.gray': ('core.html#_themenamespace.gray', 'fh_matui/core.py'),
113
+ 'fh_matui.core._ThemeNamespace.green': ('core.html#_themenamespace.green', 'fh_matui/core.py'),
114
+ 'fh_matui.core._ThemeNamespace.grey': ('core.html#_themenamespace.grey', 'fh_matui/core.py'),
115
+ 'fh_matui.core._ThemeNamespace.indigo': ('core.html#_themenamespace.indigo', 'fh_matui/core.py'),
116
+ 'fh_matui.core._ThemeNamespace.light_blue': ('core.html#_themenamespace.light_blue', 'fh_matui/core.py'),
117
+ 'fh_matui.core._ThemeNamespace.light_green': ('core.html#_themenamespace.light_green', 'fh_matui/core.py'),
118
+ 'fh_matui.core._ThemeNamespace.lime': ('core.html#_themenamespace.lime', 'fh_matui/core.py'),
119
+ 'fh_matui.core._ThemeNamespace.neutral': ('core.html#_themenamespace.neutral', 'fh_matui/core.py'),
120
+ 'fh_matui.core._ThemeNamespace.orange': ('core.html#_themenamespace.orange', 'fh_matui/core.py'),
121
+ 'fh_matui.core._ThemeNamespace.pink': ('core.html#_themenamespace.pink', 'fh_matui/core.py'),
122
+ 'fh_matui.core._ThemeNamespace.purple': ('core.html#_themenamespace.purple', 'fh_matui/core.py'),
123
+ 'fh_matui.core._ThemeNamespace.red': ('core.html#_themenamespace.red', 'fh_matui/core.py'),
124
+ 'fh_matui.core._ThemeNamespace.rose': ('core.html#_themenamespace.rose', 'fh_matui/core.py'),
125
+ 'fh_matui.core._ThemeNamespace.slate': ('core.html#_themenamespace.slate', 'fh_matui/core.py'),
126
+ 'fh_matui.core._ThemeNamespace.stone': ('core.html#_themenamespace.stone', 'fh_matui/core.py'),
127
+ 'fh_matui.core._ThemeNamespace.teal': ('core.html#_themenamespace.teal', 'fh_matui/core.py'),
128
+ 'fh_matui.core._ThemeNamespace.violet': ('core.html#_themenamespace.violet', 'fh_matui/core.py'),
129
+ 'fh_matui.core._ThemeNamespace.yellow': ('core.html#_themenamespace.yellow', 'fh_matui/core.py'),
130
+ 'fh_matui.core._ThemeNamespace.zinc': ('core.html#_themenamespace.zinc', 'fh_matui/core.py')},
131
+ 'fh_matui.datatable': { 'fh_matui.datatable.DataTable': ('table.html#datatable', 'fh_matui/datatable.py'),
132
+ 'fh_matui.datatable.DataTableResource': ('table.html#datatableresource', 'fh_matui/datatable.py'),
133
+ 'fh_matui.datatable.DataTableResource.__init__': ( 'table.html#datatableresource.__init__',
134
+ 'fh_matui/datatable.py'),
135
+ 'fh_matui.datatable.DataTableResource._error_toast': ( 'table.html#datatableresource._error_toast',
136
+ 'fh_matui/datatable.py'),
137
+ 'fh_matui.datatable.DataTableResource._filter_by_search': ( 'table.html#datatableresource._filter_by_search',
138
+ 'fh_matui/datatable.py'),
139
+ 'fh_matui.datatable.DataTableResource._get_filtered_data': ( 'table.html#datatableresource._get_filtered_data',
140
+ 'fh_matui/datatable.py'),
141
+ 'fh_matui.datatable.DataTableResource._handle_action': ( 'table.html#datatableresource._handle_action',
142
+ 'fh_matui/datatable.py'),
143
+ 'fh_matui.datatable.DataTableResource._handle_save': ( 'table.html#datatableresource._handle_save',
144
+ 'fh_matui/datatable.py'),
145
+ 'fh_matui.datatable.DataTableResource._handle_table': ( 'table.html#datatableresource._handle_table',
146
+ 'fh_matui/datatable.py'),
147
+ 'fh_matui.datatable.DataTableResource._paginate': ( 'table.html#datatableresource._paginate',
148
+ 'fh_matui/datatable.py'),
149
+ 'fh_matui.datatable.DataTableResource._register_routes': ( 'table.html#datatableresource._register_routes',
150
+ 'fh_matui/datatable.py'),
151
+ 'fh_matui.datatable.DataTableResource._success_toast': ( 'table.html#datatableresource._success_toast',
152
+ 'fh_matui/datatable.py'),
153
+ 'fh_matui.datatable.DataTableResource._wrap_modal': ( 'table.html#datatableresource._wrap_modal',
154
+ 'fh_matui/datatable.py'),
155
+ 'fh_matui.datatable._action_menu': ('table.html#_action_menu', 'fh_matui/datatable.py'),
156
+ 'fh_matui.datatable._page_size_select': ('table.html#_page_size_select', 'fh_matui/datatable.py'),
157
+ 'fh_matui.datatable._safe_int': ('table.html#_safe_int', 'fh_matui/datatable.py'),
158
+ 'fh_matui.datatable._to_dict': ('table.html#_to_dict', 'fh_matui/datatable.py'),
159
+ 'fh_matui.datatable.table_state_from_request': ( 'table.html#table_state_from_request',
160
+ 'fh_matui/datatable.py')},
161
+ 'fh_matui.foundations': { 'fh_matui.foundations.VEnum': ('foundations.html#venum', 'fh_matui/foundations.py'),
162
+ 'fh_matui.foundations.VEnum.__add__': ('foundations.html#venum.__add__', 'fh_matui/foundations.py'),
163
+ 'fh_matui.foundations.VEnum.__radd__': ('foundations.html#venum.__radd__', 'fh_matui/foundations.py'),
164
+ 'fh_matui.foundations.VEnum.__str__': ('foundations.html#venum.__str__', 'fh_matui/foundations.py'),
165
+ 'fh_matui.foundations.dedupe_preserve_order': ( 'foundations.html#dedupe_preserve_order',
166
+ 'fh_matui/foundations.py'),
167
+ 'fh_matui.foundations.normalize_tokens': ( 'foundations.html#normalize_tokens',
168
+ 'fh_matui/foundations.py'),
169
+ 'fh_matui.foundations.stringify': ('foundations.html#stringify', 'fh_matui/foundations.py')},
170
+ 'fh_matui.web_pages': { 'fh_matui.web_pages.ContentPage': ('web_pages.html#contentpage', 'fh_matui/web_pages.py'),
171
+ 'fh_matui.web_pages.FAQSection': ('web_pages.html#faqsection', 'fh_matui/web_pages.py'),
172
+ 'fh_matui.web_pages.FeatureShowcase': ('web_pages.html#featureshowcase', 'fh_matui/web_pages.py'),
173
+ 'fh_matui.web_pages.FeaturesGrid': ('web_pages.html#featuresgrid', 'fh_matui/web_pages.py'),
174
+ 'fh_matui.web_pages.LandingNavBar': ('web_pages.html#landingnavbar', 'fh_matui/web_pages.py'),
175
+ 'fh_matui.web_pages.LandingPageSimple': ('web_pages.html#landingpagesimple', 'fh_matui/web_pages.py'),
176
+ 'fh_matui.web_pages.MarkdownSection': ('web_pages.html#markdownsection', 'fh_matui/web_pages.py'),
177
+ 'fh_matui.web_pages.PageFooter': ('web_pages.html#pagefooter', 'fh_matui/web_pages.py'),
178
+ 'fh_matui.web_pages.PricingSection': ('web_pages.html#pricingsection', 'fh_matui/web_pages.py'),
179
+ 'fh_matui.web_pages._SectionWave': ('web_pages.html#_sectionwave', 'fh_matui/web_pages.py'),
180
+ 'fh_matui.web_pages._calc_savings_pct': ('web_pages.html#_calc_savings_pct', 'fh_matui/web_pages.py'),
181
+ 'fh_matui.web_pages._pricing_card': ('web_pages.html#_pricing_card', 'fh_matui/web_pages.py'),
182
+ 'fh_matui.web_pages._pricing_toggle_js': ( 'web_pages.html#_pricing_toggle_js',
183
+ 'fh_matui/web_pages.py')}}}
fh_matui/app_pages.py ADDED
@@ -0,0 +1,291 @@
1
+ """Pre-built page layouts for authenticated app experiences (login, dashboard, admin)."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_app_pages.ipynb.
4
+
5
+ # %% auto 0
6
+ __all__ = ['LoginScreen']
7
+
8
+ # %% ../nbs/03_app_pages.ipynb 2
9
+ from fastcore.utils import *
10
+ from fasthtml.common import *
11
+ from fasthtml.jupyter import *
12
+ from fastlite import *
13
+ import fasthtml.components as fc
14
+ from fasthtml.common import A, Button as FhButton, I, Span
15
+ from .foundations import normalize_tokens, stringify, VEnum
16
+ from .core import *
17
+ from .components import *
18
+
19
+
20
+
21
+ # %% ../nbs/03_app_pages.ipynb 6
22
+ # Default inspirational quotes for login screen (fully customizable via parameter)
23
+ _DEFAULT_LOGIN_QUOTES = [
24
+ "The only way to do great work is to love what you do. — Steve Jobs",
25
+ "Innovation distinguishes between a leader and a follower. — Steve Jobs",
26
+ "Stay hungry, stay foolish. — Steve Jobs",
27
+ "The best time to plant a tree was 20 years ago. The second best time is now. — Chinese Proverb",
28
+ "Success is not final, failure is not fatal: it is the courage to continue that counts. — Winston Churchill",
29
+ "The future belongs to those who believe in the beauty of their dreams. — Eleanor Roosevelt",
30
+ "It does not matter how slowly you go as long as you do not stop. — Confucius",
31
+ "Everything you've ever wanted is on the other side of fear. — George Addair",
32
+ "The only limit to our realization of tomorrow is our doubts of today. — Franklin D. Roosevelt",
33
+ "Believe you can and you're halfway there. — Theodore Roosevelt",
34
+ ]
35
+
36
+ # CSS for login screen: gradient background and quote rotation
37
+ _LOGIN_SCREEN_CSS = """
38
+ /* Hero gradient background - uses current theme colors */
39
+ .login-brand-panel {
40
+ position: relative;
41
+ background: linear-gradient(135deg,
42
+ var(--primary-container) 0%,
43
+ var(--surface-container) 50%,
44
+ var(--secondary-container) 100%);
45
+ overflow: hidden;
46
+ transition: background 1s ease-in-out;
47
+ }
48
+ .login-brand-panel::before {
49
+ content: '';
50
+ position: absolute;
51
+ inset: 0;
52
+ background: radial-gradient(circle at 20% 80%, var(--primary) 0%, transparent 50%),
53
+ radial-gradient(circle at 80% 20%, var(--secondary) 0%, transparent 50%);
54
+ opacity: 0.15;
55
+ pointer-events: none;
56
+ }
57
+ .login-brand-content {
58
+ position: relative;
59
+ z-index: 1;
60
+ display: flex;
61
+ flex-direction: column;
62
+ height: 100%;
63
+ min-height: 100vh;
64
+ padding: 2rem;
65
+ }
66
+
67
+ /* Branding anchored at top-left */
68
+ .login-branding {
69
+ flex-shrink: 0;
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 0.75rem;
73
+ justify-content: flex-start;
74
+ }
75
+ .login-branding img {
76
+ max-height: 3rem;
77
+ }
78
+
79
+ /* Quote in center - larger and prominent */
80
+ .login-quotes-wrapper {
81
+ flex: 1;
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ }
86
+ .login-quotes {
87
+ position: relative;
88
+ min-height: 6rem;
89
+ max-width: 500px;
90
+ width: 100%;
91
+ }
92
+ .login-quote {
93
+ position: absolute;
94
+ width: 100%;
95
+ opacity: 0;
96
+ animation: quote-fade 100s infinite;
97
+ text-align: center;
98
+ font-style: italic;
99
+ font-size: 1.25rem;
100
+ line-height: 1.6;
101
+ }
102
+ /* Stagger each quote: 10 quotes × 10s each = 100s total cycle */
103
+ .login-quote:nth-child(1) { animation-delay: 0s; }
104
+ .login-quote:nth-child(2) { animation-delay: 10s; }
105
+ .login-quote:nth-child(3) { animation-delay: 20s; }
106
+ .login-quote:nth-child(4) { animation-delay: 30s; }
107
+ .login-quote:nth-child(5) { animation-delay: 40s; }
108
+ .login-quote:nth-child(6) { animation-delay: 50s; }
109
+ .login-quote:nth-child(7) { animation-delay: 60s; }
110
+ .login-quote:nth-child(8) { animation-delay: 70s; }
111
+ .login-quote:nth-child(9) { animation-delay: 80s; }
112
+ .login-quote:nth-child(10) { animation-delay: 90s; }
113
+
114
+ @keyframes quote-fade {
115
+ 0%, 8% { opacity: 0; transform: translateY(10px); }
116
+ 10%, 18% { opacity: 1; transform: translateY(0); }
117
+ 20%, 100% { opacity: 0; transform: translateY(-10px); }
118
+ }
119
+
120
+ /* Mobile: hide quotes, simplify layout */
121
+ @media (max-width: 992px) {
122
+ .login-quotes-wrapper { display: none; }
123
+ .login-brand-content {
124
+ min-height: auto;
125
+ padding: 1.5rem;
126
+ }
127
+ .login-branding {
128
+ justify-content: center;
129
+ }
130
+ }
131
+ """
132
+
133
+ # Minimal JS for cycling BeerCSS theme colors
134
+ _LOGIN_COLOR_CYCLE_JS = """
135
+ (function() {
136
+ const colors = ['primary', 'secondary', 'tertiary'];
137
+ let idx = 0;
138
+ const panel = document.querySelector('.login-brand-panel');
139
+ if (!panel) return;
140
+
141
+ setInterval(function() {
142
+ idx = (idx + 1) % colors.length;
143
+ panel.style.background = 'linear-gradient(135deg, var(--' + colors[idx] + '-container) 0%, var(--surface-container) 50%, var(--' + colors[(idx+1) % colors.length] + '-container) 100%)';
144
+ }, 30000);
145
+ })();
146
+ """
147
+
148
+ def LoginScreen(
149
+ title='Sign In',
150
+ subtitle='Choose your preferred sign-in method',
151
+ providers=None,
152
+ left_slot=None,
153
+ logo_src=None,
154
+ brand_name=None, # Brand name displayed at top-left of left panel
155
+ quotes=None, # List of quotes to rotate (uses defaults if None, pass [] to disable)
156
+ color_cycle=True, # Enable color cycling animation
157
+ testimonial_text=None, # Legacy: single testimonial (use quotes instead)
158
+ brand_bg_cls='primary', # Legacy: fallback if gradient fails
159
+ left_cols=9, # Number of columns for left side (out of 12)
160
+ cls='',
161
+ **kwargs
162
+ ):
163
+ """
164
+ A configurable Split Login Screen with dynamic branding.
165
+
166
+ Features:
167
+ - Hero-style gradient background (same as landing page)
168
+ - Brand name + logo at top-left corner
169
+ - Rotating inspirational quotes in center (pure CSS, hidden on mobile)
170
+ - Optional color cycling through BeerCSS theme colors (minimal JS)
171
+
172
+ Args:
173
+ title: Sign-in form title
174
+ subtitle: Sign-in form subtitle
175
+ providers: List of OAuth provider dicts [{label, icon, href, cls}, ...]
176
+ left_slot: Custom content for left panel (overrides default branding)
177
+ logo_src: URL/path to logo image
178
+ brand_name: Brand name displayed at top-left corner
179
+ quotes: List of quote strings to rotate. Defaults to inspirational quotes.
180
+ Pass empty list [] to disable quotes entirely.
181
+ color_cycle: Enable gradient color cycling (default True)
182
+ left_cols: Grid columns for left panel (out of 12). Default 9 = 75%
183
+
184
+ Example:
185
+ LoginScreen(
186
+ brand_name="MyApp",
187
+ logo_src="/static/logo.svg",
188
+ quotes=[
189
+ "Your custom quote here — Author",
190
+ "Another inspiring message — Source",
191
+ ],
192
+ )
193
+ """
194
+
195
+ # 1. Defaults
196
+ if providers is None:
197
+ providers = [
198
+ {'label': 'Continue with Google', 'icon': 'https://authjs.dev/img/providers/google.svg', 'href': '/auth/google', 'cls': 'border responsive surface'},
199
+ {'label': 'Continue with GitHub', 'icon': 'https://authjs.dev/img/providers/github.svg', 'icon_cls': 'invert', 'href': '/auth/github', 'cls': 'fill responsive inverse-surface'}
200
+ ]
201
+
202
+ # Use default quotes if None, allow empty list to disable
203
+ if quotes is None:
204
+ quotes = _DEFAULT_LOGIN_QUOTES
205
+
206
+ # 2. Build Left Column - Branded layout
207
+ if left_slot:
208
+ left_content = left_slot
209
+ else:
210
+ # Top-left: Logo + Brand name (anchored at top-left via CSS)
211
+ top_section = []
212
+ if logo_src:
213
+ top_section.append(Img(src=logo_src, cls="responsive"))
214
+ if brand_name:
215
+ top_section.append(H3(brand_name, cls="bold no-margin"))
216
+ elif not logo_src:
217
+ top_section.append(H3("Welcome", cls="bold no-margin"))
218
+
219
+ top_branding = Div(*top_section, cls="login-branding")
220
+
221
+ # Center section: Rotating quotes (larger, centered)
222
+ center_quotes = Div(cls="login-quotes-wrapper")
223
+ if quotes:
224
+ quote_elements = [
225
+ P(f'"{q}"', cls="login-quote")
226
+ for q in quotes[:10] # Max 10 for CSS animation
227
+ ]
228
+ center_quotes = Div(
229
+ Div(*quote_elements, cls="login-quotes"),
230
+ cls="login-quotes-wrapper"
231
+ )
232
+
233
+ # Legacy support
234
+ if testimonial_text and not quotes:
235
+ center_quotes = Div(
236
+ Blockquote(P(f'"{testimonial_text}"', cls="italic center-align large-text")),
237
+ cls="login-quotes-wrapper"
238
+ )
239
+
240
+ left_content = Div(
241
+ top_branding,
242
+ center_quotes,
243
+ cls="login-brand-content"
244
+ )
245
+
246
+ # 3. Build Right Column Buttons
247
+ button_list = []
248
+ for p in providers:
249
+ icon = Img(src=p['icon'], cls=f"circle tiny spacing-right {p.get('icon_cls', '')}") if p.get('icon') else ""
250
+ button_list.append(
251
+ Div(
252
+ A(
253
+ Button(icon, Span(p['label']), cls=p.get('cls')),
254
+ href=p.get('href', '#')
255
+ ),
256
+ cls="s12"
257
+ )
258
+ )
259
+
260
+ auth_buttons = Div(*button_list, cls="grid small-space")
261
+
262
+ right_content = Div(
263
+ H4(title, cls="center-align bold margin-bottom"),
264
+ P(subtitle, cls="center-align medium-text margin-bottom no-wrap"),
265
+ auth_buttons,
266
+ cls="medium-width"
267
+ )
268
+
269
+ # 4. Build page
270
+ right_cols = 12 - left_cols
271
+ left_panel_cls = f"s12 m12 l{left_cols} login-brand-panel"
272
+
273
+ # Include CSS, and optionally JS for color cycling
274
+ head_elements = [Style(_LOGIN_SCREEN_CSS)]
275
+ if color_cycle:
276
+ head_elements.append(Script(_LOGIN_COLOR_CYCLE_JS))
277
+
278
+ return Div(
279
+ *head_elements,
280
+ # Left (Brand) - gradient background with branding
281
+ Div(left_content, cls=left_panel_cls),
282
+ # Right (Auth) - clean form area
283
+ Div(
284
+ DivCentered(right_content),
285
+ cls=f"s12 m12 l{right_cols} padding middle-align center-align"
286
+ ),
287
+ cls=f"grid no-space {cls}".strip(),
288
+ style="min-height: 100vh;",
289
+ **kwargs
290
+ )
291
+