Coverage for fastblocks/adapters/styles/webawesome.py: 0%
135 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""WebAwesome styles adapter for FastBlocks with integrated icon system."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.config import Settings
8from acb.depends import depends
10from ._base import StylesBase
13class WebAwesomeSettings(Settings): # type: ignore[misc]
14 """Settings for WebAwesome styles adapter."""
16 # Required ACB 0.19.0+ metadata
17 MODULE_ID: UUID = UUID("01937d86-7a5c-9f6d-b8e9-d7e8f9a0b1c2") # Static UUID7
18 MODULE_STATUS: str = "stable"
20 # WebAwesome configuration
21 version: str = "latest"
22 cdn_url: str = "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free"
23 include_brands: bool = True
24 include_regular: bool = True
25 include_solid: bool = True
27 # Custom configuration
28 custom_css_url: str | None = None
29 primary_color: str = "#007bff"
30 secondary_color: str = "#6c757d"
31 success_color: str = "#28a745"
32 warning_color: str = "#ffc107"
33 danger_color: str = "#dc3545"
34 info_color: str = "#17a2b8"
36 # Layout settings
37 container_max_width: str = "1200px"
38 grid_columns: int = 12
39 gutter_width: str = "1rem"
41 # Typography
42 font_family: str = (
43 "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
44 )
45 base_font_size: str = "16px"
46 line_height: str = "1.6"
49class WebAwesomeAdapter(StylesBase):
50 """WebAwesome styles adapter with integrated icons and components."""
52 # Required ACB 0.19.0+ metadata
53 MODULE_ID: UUID = UUID("01937d86-7a5c-9f6d-b8e9-d7e8f9a0b1c2") # Static UUID7
54 MODULE_STATUS: str = "stable"
56 def __init__(self) -> None:
57 """Initialize WebAwesome adapter."""
58 super().__init__()
59 self.settings: WebAwesomeSettings | None = None
61 # Register with ACB dependency system
62 with suppress(Exception):
63 depends.set(self)
65 def get_stylesheet_links(self) -> list[str]:
66 """Get WebAwesome stylesheet links."""
67 if not self.settings:
68 self.settings = WebAwesomeSettings()
70 links = []
72 # FontAwesome CSS (for icon integration)
73 if self.settings.include_solid:
74 links.extend(
75 (
76 f'<link rel="stylesheet" href="{self.settings.cdn_url}@{self.settings.version}/css/fontawesome.min.css">',
77 f'<link rel="stylesheet" href="{self.settings.cdn_url}@{self.settings.version}/css/solid.min.css">',
78 )
79 )
81 if self.settings.include_regular:
82 links.append(
83 f'<link rel="stylesheet" href="{self.settings.cdn_url}@{self.settings.version}/css/regular.min.css">'
84 )
86 if self.settings.include_brands:
87 links.append(
88 f'<link rel="stylesheet" href="{self.settings.cdn_url}@{self.settings.version}/css/brands.min.css">'
89 )
91 # Custom WebAwesome CSS
92 if self.settings.custom_css_url:
93 links.append(
94 f'<link rel="stylesheet" href="{self.settings.custom_css_url}">'
95 )
97 # Generate inline CSS for WebAwesome system
98 inline_css = self._generate_webawesome_css()
99 links.append(f"<style>{inline_css}</style>")
101 return links
103 def _generate_webawesome_css(self) -> str:
104 """Generate WebAwesome CSS framework."""
105 if not self.settings:
106 self.settings = WebAwesomeSettings()
108 css = f"""
109/* WebAwesome CSS Framework for FastBlocks */
110:root {{
111 --wa-primary: {self.settings.primary_color};
112 --wa-secondary: {self.settings.secondary_color};
113 --wa-success: {self.settings.success_color};
114 --wa-warning: {self.settings.warning_color};
115 --wa-danger: {self.settings.danger_color};
116 --wa-info: {self.settings.info_color};
117 --wa-font-family: {self.settings.font_family};
118 --wa-font-size: {self.settings.base_font_size};
119 --wa-line-height: {self.settings.line_height};
120 --wa-container-max-width: {self.settings.container_max_width};
121 --wa-gutter: {self.settings.gutter_width};
122}}
124/* Reset and Base */
125*, *::before, *::after {{
126 box-sizing: border-box;
127}}
129body {{
130 font-family: var(--wa-font-family);
131 font-size: var(--wa-font-size);
132 line-height: var(--wa-line-height);
133 margin: 0;
134 padding: 0;
135}}
137/* Container System */
138.wa-container {{
139 max-width: var(--wa-container-max-width);
140 margin: 0 auto;
141 padding: 0 var(--wa-gutter);
142}}
144.wa-container-fluid {{
145 width: 100%;
146 padding: 0 var(--wa-gutter);
147}}
149/* Grid System */
150.wa-row {{
151 display: flex;
152 flex-wrap: wrap;
153 margin: 0 calc(var(--wa-gutter) / -2);
154}}
156.wa-col {{
157 flex: 1;
158 padding: 0 calc(var(--wa-gutter) / 2);
159}}
161/* Responsive columns */
162{self._generate_grid_css()}
164/* Component System */
165.wa-card {{
166 background: white;
167 border: 1px solid #e9ecef;
168 border-radius: 0.5rem;
169 box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
170 overflow: hidden;
171}}
173.wa-card-header {{
174 padding: 1rem;
175 background: #f8f9fa;
176 border-bottom: 1px solid #e9ecef;
177 font-weight: 600;
178}}
180.wa-card-body {{
181 padding: 1rem;
182}}
184.wa-card-footer {{
185 padding: 1rem;
186 background: #f8f9fa;
187 border-top: 1px solid #e9ecef;
188}}
190/* Button System */
191.wa-btn {{
192 display: inline-block;
193 padding: 0.5rem 1rem;
194 border: 1px solid transparent;
195 border-radius: 0.375rem;
196 font-weight: 500;
197 text-align: center;
198 text-decoration: none;
199 cursor: pointer;
200 transition: all 0.2s ease-in-out;
201 font-family: inherit;
202 font-size: 1rem;
203 line-height: 1.5;
204}}
206.wa-btn:hover {{
207 transform: translateY(-1px);
208 box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.15);
209}}
211.wa-btn-primary {{
212 background: var(--wa-primary);
213 border-color: var(--wa-primary);
214 color: white;
215}}
217.wa-btn-secondary {{
218 background: var(--wa-secondary);
219 border-color: var(--wa-secondary);
220 color: white;
221}}
223.wa-btn-success {{
224 background: var(--wa-success);
225 border-color: var(--wa-success);
226 color: white;
227}}
229.wa-btn-warning {{
230 background: var(--wa-warning);
231 border-color: var(--wa-warning);
232 color: #212529;
233}}
235.wa-btn-danger {{
236 background: var(--wa-danger);
237 border-color: var(--wa-danger);
238 color: white;
239}}
241.wa-btn-info {{
242 background: var(--wa-info);
243 border-color: var(--wa-info);
244 color: white;
245}}
247/* Form Controls */
248.wa-form-group {{
249 margin-bottom: 1rem;
250}}
252.wa-form-label {{
253 display: block;
254 margin-bottom: 0.5rem;
255 font-weight: 500;
256 color: #495057;
257}}
259.wa-form-control {{
260 display: block;
261 width: 100%;
262 padding: 0.5rem 0.75rem;
263 font-size: 1rem;
264 line-height: 1.5;
265 color: #495057;
266 background: white;
267 border: 1px solid #ced4da;
268 border-radius: 0.375rem;
269 transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
270}}
272.wa-form-control:focus {{
273 border-color: var(--wa-primary);
274 outline: 0;
275 box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
276}}
278/* Alert System */
279.wa-alert {{
280 padding: 1rem;
281 margin-bottom: 1rem;
282 border: 1px solid transparent;
283 border-radius: 0.375rem;
284}}
286.wa-alert-primary {{
287 color: #084298;
288 background: #cfe2ff;
289 border-color: #b6d4fe;
290}}
292.wa-alert-success {{
293 color: #0f5132;
294 background: #d1e7dd;
295 border-color: #badbcc;
296}}
298.wa-alert-warning {{
299 color: #664d03;
300 background: #fff3cd;
301 border-color: #ffecb5;
302}}
304.wa-alert-danger {{
305 color: #842029;
306 background: #f8d7da;
307 border-color: #f5c2c7;
308}}
310/* Navigation */
311.wa-navbar {{
312 display: flex;
313 align-items: center;
314 justify-content: space-between;
315 padding: 1rem var(--wa-gutter);
316 background: white;
317 border-bottom: 1px solid #e9ecef;
318}}
320.wa-navbar-brand {{
321 font-size: 1.25rem;
322 font-weight: 600;
323 text-decoration: none;
324 color: var(--wa-primary);
325}}
327.wa-navbar-nav {{
328 display: flex;
329 list-style: none;
330 margin: 0;
331 padding: 0;
332 gap: 1rem;
333}}
335.wa-navbar-link {{
336 text-decoration: none;
337 color: #495057;
338 transition: color 0.2s;
339}}
341.wa-navbar-link:hover {{
342 color: var(--wa-primary);
343}}
345/* Icon Integration */
346.wa-icon {{
347 display: inline-block;
348 width: 1em;
349 height: 1em;
350 vertical-align: -0.125em;
351}}
353.wa-icon-sm {{
354 font-size: 0.875rem;
355}}
357.wa-icon-lg {{
358 font-size: 1.125rem;
359}}
361.wa-icon-xl {{
362 font-size: 1.5rem;
363}}
365.wa-icon-2x {{
366 font-size: 2rem;
367}}
369/* Utility Classes */
370.wa-text-center {{ text-align: center; }}
371.wa-text-left {{ text-align: left; }}
372.wa-text-right {{ text-align: right; }}
374.wa-d-block {{ display: block; }}
375.wa-d-inline {{ display: inline; }}
376.wa-d-inline-block {{ display: inline-block; }}
377.wa-d-flex {{ display: flex; }}
378.wa-d-none {{ display: none; }}
380.wa-mt-0 {{ margin-top: 0; }}
381.wa-mt-1 {{ margin-top: 0.25rem; }}
382.wa-mt-2 {{ margin-top: 0.5rem; }}
383.wa-mt-3 {{ margin-top: 1rem; }}
384.wa-mt-4 {{ margin-top: 1.5rem; }}
385.wa-mt-5 {{ margin-top: 3rem; }}
387.wa-mb-0 {{ margin-bottom: 0; }}
388.wa-mb-1 {{ margin-bottom: 0.25rem; }}
389.wa-mb-2 {{ margin-bottom: 0.5rem; }}
390.wa-mb-3 {{ margin-bottom: 1rem; }}
391.wa-mb-4 {{ margin-bottom: 1.5rem; }}
392.wa-mb-5 {{ margin-bottom: 3rem; }}
394.wa-p-0 {{ padding: 0; }}
395.wa-p-1 {{ padding: 0.25rem; }}
396.wa-p-2 {{ padding: 0.5rem; }}
397.wa-p-3 {{ padding: 1rem; }}
398.wa-p-4 {{ padding: 1.5rem; }}
399.wa-p-5 {{ padding: 3rem; }}
401/* Responsive Design */
402@media (max-width: 768px) {{
403 .wa-container {{
404 padding: 0 0.5rem;
405 }}
407 .wa-btn {{
408 width: 100%;
409 margin-bottom: 0.5rem;
410 }}
412 .wa-navbar {{
413 flex-direction: column;
414 gap: 1rem;
415 }}
416}}
417"""
418 return css
420 def _generate_grid_css(self) -> str:
421 """Generate responsive grid CSS."""
422 if not self.settings:
423 self.settings = WebAwesomeSettings()
425 css = ""
426 breakpoints = {
427 "sm": "576px",
428 "md": "768px",
429 "lg": "992px",
430 "xl": "1200px",
431 }
433 for breakpoint, width in breakpoints.items():
434 css += f"\n@media (min-width: {width}) {{\n"
436 for i in range(1, self.settings.grid_columns + 1):
437 percentage = (i / self.settings.grid_columns) * 100
438 css += f" .wa-col-{breakpoint}-{i} {{ flex: 0 0 {percentage:.4f}%; max-width: {percentage:.4f}%; }}\n"
440 css += "}\n"
442 # Default columns
443 for i in range(1, self.settings.grid_columns + 1):
444 percentage = (i / self.settings.grid_columns) * 100
445 css += f".wa-col-{i} {{ flex: 0 0 {percentage:.4f}%; max-width: {percentage:.4f}%; }}\n"
447 return css
449 def get_component_class(self, component: str) -> str:
450 """Get WebAwesome-specific classes."""
451 class_map = {
452 # Layout
453 "container": "wa-container",
454 "container-fluid": "wa-container-fluid",
455 "row": "wa-row",
456 "col": "wa-col",
457 # Components
458 "card": "wa-card",
459 "card-header": "wa-card-header",
460 "card-body": "wa-card-body",
461 "card-footer": "wa-card-footer",
462 # Buttons
463 "button": "wa-btn wa-btn-primary",
464 "btn": "wa-btn",
465 "btn-primary": "wa-btn wa-btn-primary",
466 "btn-secondary": "wa-btn wa-btn-secondary",
467 "btn-success": "wa-btn wa-btn-success",
468 "btn-warning": "wa-btn wa-btn-warning",
469 "btn-danger": "wa-btn wa-btn-danger",
470 "btn-info": "wa-btn wa-btn-info",
471 # Forms
472 "form-group": "wa-form-group",
473 "form-label": "wa-form-label",
474 "form-control": "wa-form-control",
475 "input": "wa-form-control",
476 "textarea": "wa-form-control",
477 "select": "wa-form-control",
478 # Alerts
479 "alert": "wa-alert",
480 "alert-primary": "wa-alert wa-alert-primary",
481 "alert-success": "wa-alert wa-alert-success",
482 "alert-warning": "wa-alert wa-alert-warning",
483 "alert-danger": "wa-alert wa-alert-danger",
484 # Navigation
485 "navbar": "wa-navbar",
486 "navbar-brand": "wa-navbar-brand",
487 "navbar-nav": "wa-navbar-nav",
488 "navbar-link": "wa-navbar-link",
489 # Icons
490 "icon": "wa-icon fas",
491 "icon-sm": "wa-icon wa-icon-sm fas",
492 "icon-lg": "wa-icon wa-icon-lg fas",
493 "icon-xl": "wa-icon wa-icon-xl fas",
494 "icon-2x": "wa-icon wa-icon-2x fas",
495 }
497 return class_map.get(component, f"wa-{component}")
499 def get_icon_class(self, icon_name: str, style: str = "solid") -> str:
500 """Get FontAwesome icon class integrated with WebAwesome."""
501 prefix_map = {
502 "solid": "fas",
503 "regular": "far",
504 "brands": "fab",
505 }
507 prefix = prefix_map.get(style, "fas")
509 # Ensure icon name has fa- prefix
510 if not icon_name.startswith("fa-"):
511 icon_name = f"fa-{icon_name}"
513 return f"wa-icon {prefix} {icon_name}"
516# Template function registration for FastBlocks
517def _register_wa_basic_filters(env: Any) -> None:
518 """Register basic WebAwesome filters."""
520 @env.global_("wa_stylesheet_links") # type: ignore[misc]
521 def wa_stylesheet_links() -> str:
522 """Global function for WebAwesome stylesheet links."""
523 styles = depends.get("styles")
524 if isinstance(styles, WebAwesomeAdapter):
525 return "\n".join(styles.get_stylesheet_links())
526 return ""
528 @env.filter("wa_class") # type: ignore[misc]
529 def wa_class_filter(component: str) -> str:
530 """Filter for getting WebAwesome component classes."""
531 styles = depends.get("styles")
532 if isinstance(styles, WebAwesomeAdapter):
533 return styles.get_component_class(component)
534 return component
536 @env.filter("wa_icon") # type: ignore[misc]
537 def wa_icon_filter(icon_name: str, style: str = "solid") -> str:
538 """Filter for WebAwesome icon classes."""
539 styles = depends.get("styles")
540 if isinstance(styles, WebAwesomeAdapter):
541 return styles.get_icon_class(icon_name, style)
542 return f"fa-{icon_name}"
545def _register_wa_button_functions(env: Any) -> None:
546 """Register WebAwesome button component functions."""
548 @env.global_("wa_button") # type: ignore[misc]
549 def wa_button(
550 text: str, variant: str = "primary", icon: str | None = None, **attributes: Any
551 ) -> str:
552 """Generate WebAwesome button with optional icon."""
553 styles = depends.get("styles")
554 if not isinstance(styles, WebAwesomeAdapter):
555 return f'<button class="btn">{text}</button>'
557 btn_class = styles.get_component_class(f"btn-{variant}")
558 if "class" in attributes:
559 btn_class += f" {attributes.pop('class')}"
561 content = ""
562 if icon:
563 content += f'<i class="{styles.get_icon_class(icon)}"></i> '
564 content += text
566 attr_string = " ".join(f'{k}="{v}"' for k, v in attributes.items())
567 return f'<button class="{btn_class}" {attr_string}>{content}</button>'
570def _register_wa_card_functions(env: Any) -> None:
571 """Register WebAwesome card component functions."""
573 @env.global_("wa_card") # type: ignore[misc]
574 def wa_card(
575 title: str | None = None,
576 content: str = "",
577 footer: str | None = None,
578 **attributes: Any,
579 ) -> str:
580 """Generate WebAwesome card component."""
581 styles = depends.get("styles")
582 if not isinstance(styles, WebAwesomeAdapter):
583 return f'<div class="card">{content}</div>'
585 card_class = styles.get_component_class("card")
586 if "class" in attributes:
587 card_class += f" {attributes.pop('class')}"
589 card_content = ""
590 if title:
591 card_content += f'<div class="{styles.get_component_class("card-header")}">{title}</div>'
592 card_content += (
593 f'<div class="{styles.get_component_class("card-body")}">{content}</div>'
594 )
595 if footer:
596 card_content += f'<div class="{styles.get_component_class("card-footer")}">{footer}</div>'
598 attr_string = " ".join(f'{k}="{v}"' for k, v in attributes.items())
599 return f'<div class="{card_class}" {attr_string}>{card_content}</div>'
602def register_webawesome_functions(env: Any) -> None:
603 """Register WebAwesome functions for Jinja2 templates."""
604 _register_wa_basic_filters(env)
605 _register_wa_button_functions(env)
606 _register_wa_card_functions(env)
609# ACB 0.19.0+ compatibility
610__all__ = ["WebAwesomeAdapter", "WebAwesomeSettings", "register_webawesome_functions"]