Coverage for fastblocks / adapters / style / webawesome.py: 35%
138 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:30 -0800
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:30 -0800
1"""WebAwesome styles adapter for FastBlocks with integrated icon system."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.depends import depends
9from ._base import StyleBase, StyleBaseSettings
12class WebAwesomeStyleSettings(StyleBaseSettings):
13 """Settings for WebAwesome styles adapter."""
15 # Required ACB 0.19.0+ metadata
16 MODULE_ID: UUID = UUID("01937d86-7a5c-9f6d-b8e9-d7e8f9a0b1c2") # Static UUID7
17 MODULE_STATUS: str = "stable"
19 # WebAwesome configuration
20 version: str = "latest"
21 cdn_url: str = "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free"
22 include_brands: bool = True
23 include_regular: bool = True
24 include_solid: bool = True
26 # Custom configuration
27 custom_css_url: str | None = None
28 primary_color: str = "#007bff"
29 secondary_color: str = "#6c757d"
30 success_color: str = "#28a745"
31 warning_color: str = "#ffc107"
32 danger_color: str = "#dc3545"
33 info_color: str = "#17a2b8"
35 # Layout settings
36 container_max_width: str = "1200px"
37 grid_columns: int = 12
38 gutter_width: str = "1rem"
40 # Typography
41 font_family: str = (
42 "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
43 )
44 base_font_size: str = "16px"
45 line_height: str = "1.6"
48class WebAwesomeStyle(StyleBase):
49 """WebAwesome styles adapter with integrated icons and components."""
51 # Required ACB 0.19.0+ metadata
52 MODULE_ID: UUID = UUID("01937d86-7a5c-9f6d-b8e9-d7e8f9a0b1c2") # Static UUID7
53 MODULE_STATUS: str = "stable"
55 def __init__(self) -> None:
56 """Initialize WebAwesome adapter."""
57 super().__init__()
58 self.settings: WebAwesomeStyleSettings | None = None
60 # Register with ACB dependency system
61 with suppress(Exception):
62 depends.set(self)
64 def get_stylesheet_links(self) -> list[str]:
65 """Get WebAwesome stylesheet links."""
66 if not self.settings:
67 self.settings = WebAwesomeStyleSettings()
69 links = []
71 # FontAwesome CSS (for icon integration)
72 if self.settings.include_solid:
73 links.extend(
74 (
75 f'<link rel="stylesheet" href="{self.settings.cdn_url}@{self.settings.version}/css/fontawesome.min.css">',
76 f'<link rel="stylesheet" href="{self.settings.cdn_url}@{self.settings.version}/css/solid.min.css">',
77 )
78 )
80 if self.settings.include_regular:
81 links.append(
82 f'<link rel="stylesheet" href="{self.settings.cdn_url}@{self.settings.version}/css/regular.min.css">'
83 )
85 if self.settings.include_brands:
86 links.append(
87 f'<link rel="stylesheet" href="{self.settings.cdn_url}@{self.settings.version}/css/brands.min.css">'
88 )
90 # Custom WebAwesome CSS
91 if self.settings.custom_css_url:
92 links.append(
93 f'<link rel="stylesheet" href="{self.settings.custom_css_url}">'
94 )
96 # Generate inline CSS for WebAwesome system
97 inline_css = self._generate_webawesome_css()
98 links.append(f"<style>{inline_css}</style>")
100 return links
102 def _generate_webawesome_css(self) -> str:
103 """Generate WebAwesome CSS framework."""
104 if not self.settings:
105 self.settings = WebAwesomeStyleSettings()
107 css = f"""
108/* WebAwesome CSS Framework for FastBlocks */
109:root {{
110 --wa-primary: {self.settings.primary_color};
111 --wa-secondary: {self.settings.secondary_color};
112 --wa-success: {self.settings.success_color};
113 --wa-warning: {self.settings.warning_color};
114 --wa-danger: {self.settings.danger_color};
115 --wa-info: {self.settings.info_color};
116 --wa-font-family: {self.settings.font_family};
117 --wa-font-size: {self.settings.base_font_size};
118 --wa-line-height: {self.settings.line_height};
119 --wa-container-max-width: {self.settings.container_max_width};
120 --wa-gutter: {self.settings.gutter_width};
121}}
123/* Reset and Base */
124*, *::before, *::after {{
125 box-sizing: border-box;
126}}
128body {{
129 font-family: var(--wa-font-family);
130 font-size: var(--wa-font-size);
131 line-height: var(--wa-line-height);
132 margin: 0;
133 padding: 0;
134}}
136/* Container System */
137.wa-container {{
138 max-width: var(--wa-container-max-width);
139 margin: 0 auto;
140 padding: 0 var(--wa-gutter);
141}}
143.wa-container-fluid {{
144 width: 100%;
145 padding: 0 var(--wa-gutter);
146}}
148/* Grid System */
149.wa-row {{
150 display: flex;
151 flex-wrap: wrap;
152 margin: 0 calc(var(--wa-gutter) / -2);
153}}
155.wa-col {{
156 flex: 1;
157 padding: 0 calc(var(--wa-gutter) / 2);
158}}
160/* Responsive columns */
161{self._generate_grid_css()}
163/* Component System */
164.wa-card {{
165 background: white;
166 border: 1px solid #e9ecef;
167 border-radius: 0.5rem;
168 box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
169 overflow: hidden;
170}}
172.wa-card-header {{
173 padding: 1rem;
174 background: #f8f9fa;
175 border-bottom: 1px solid #e9ecef;
176 font-weight: 600;
177}}
179.wa-card-body {{
180 padding: 1rem;
181}}
183.wa-card-footer {{
184 padding: 1rem;
185 background: #f8f9fa;
186 border-top: 1px solid #e9ecef;
187}}
189/* Button System */
190.wa-btn {{
191 display: inline-block;
192 padding: 0.5rem 1rem;
193 border: 1px solid transparent;
194 border-radius: 0.375rem;
195 font-weight: 500;
196 text-align: center;
197 text-decoration: none;
198 cursor: pointer;
199 transition: all 0.2s ease-in-out;
200 font-family: inherit;
201 font-size: 1rem;
202 line-height: 1.5;
203}}
205.wa-btn:hover {{
206 transform: translateY(-1px);
207 box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.15);
208}}
210.wa-btn-primary {{
211 background: var(--wa-primary);
212 border-color: var(--wa-primary);
213 color: white;
214}}
216.wa-btn-secondary {{
217 background: var(--wa-secondary);
218 border-color: var(--wa-secondary);
219 color: white;
220}}
222.wa-btn-success {{
223 background: var(--wa-success);
224 border-color: var(--wa-success);
225 color: white;
226}}
228.wa-btn-warning {{
229 background: var(--wa-warning);
230 border-color: var(--wa-warning);
231 color: #212529;
232}}
234.wa-btn-danger {{
235 background: var(--wa-danger);
236 border-color: var(--wa-danger);
237 color: white;
238}}
240.wa-btn-info {{
241 background: var(--wa-info);
242 border-color: var(--wa-info);
243 color: white;
244}}
246/* Form Controls */
247.wa-form-group {{
248 margin-bottom: 1rem;
249}}
251.wa-form-label {{
252 display: block;
253 margin-bottom: 0.5rem;
254 font-weight: 500;
255 color: #495057;
256}}
258.wa-form-control {{
259 display: block;
260 width: 100%;
261 padding: 0.5rem 0.75rem;
262 font-size: 1rem;
263 line-height: 1.5;
264 color: #495057;
265 background: white;
266 border: 1px solid #ced4da;
267 border-radius: 0.375rem;
268 transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
269}}
271.wa-form-control:focus {{
272 border-color: var(--wa-primary);
273 outline: 0;
274 box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
275}}
277/* Alert System */
278.wa-alert {{
279 padding: 1rem;
280 margin-bottom: 1rem;
281 border: 1px solid transparent;
282 border-radius: 0.375rem;
283}}
285.wa-alert-primary {{
286 color: #084298;
287 background: #cfe2ff;
288 border-color: #b6d4fe;
289}}
291.wa-alert-success {{
292 color: #0f5132;
293 background: #d1e7dd;
294 border-color: #badbcc;
295}}
297.wa-alert-warning {{
298 color: #664d03;
299 background: #fff3cd;
300 border-color: #ffecb5;
301}}
303.wa-alert-danger {{
304 color: #842029;
305 background: #f8d7da;
306 border-color: #f5c2c7;
307}}
309/* Navigation */
310.wa-navbar {{
311 display: flex;
312 align-items: center;
313 justify-content: space-between;
314 padding: 1rem var(--wa-gutter);
315 background: white;
316 border-bottom: 1px solid #e9ecef;
317}}
319.wa-navbar-brand {{
320 font-size: 1.25rem;
321 font-weight: 600;
322 text-decoration: none;
323 color: var(--wa-primary);
324}}
326.wa-navbar-nav {{
327 display: flex;
328 list-style: none;
329 margin: 0;
330 padding: 0;
331 gap: 1rem;
332}}
334.wa-navbar-link {{
335 text-decoration: none;
336 color: #495057;
337 transition: color 0.2s;
338}}
340.wa-navbar-link:hover {{
341 color: var(--wa-primary);
342}}
344/* Icon Integration */
345.wa-icon {{
346 display: inline-block;
347 width: 1em;
348 height: 1em;
349 vertical-align: -0.125em;
350}}
352.wa-icon-sm {{
353 font-size: 0.875rem;
354}}
356.wa-icon-lg {{
357 font-size: 1.125rem;
358}}
360.wa-icon-xl {{
361 font-size: 1.5rem;
362}}
364.wa-icon-2x {{
365 font-size: 2rem;
366}}
368/* Utility Classes */
369.wa-text-center {{ text-align: center; }}
370.wa-text-left {{ text-align: left; }}
371.wa-text-right {{ text-align: right; }}
373.wa-d-block {{ display: block; }}
374.wa-d-inline {{ display: inline; }}
375.wa-d-inline-block {{ display: inline-block; }}
376.wa-d-flex {{ display: flex; }}
377.wa-d-none {{ display: none; }}
379.wa-mt-0 {{ margin-top: 0; }}
380.wa-mt-1 {{ margin-top: 0.25rem; }}
381.wa-mt-2 {{ margin-top: 0.5rem; }}
382.wa-mt-3 {{ margin-top: 1rem; }}
383.wa-mt-4 {{ margin-top: 1.5rem; }}
384.wa-mt-5 {{ margin-top: 3rem; }}
386.wa-mb-0 {{ margin-bottom: 0; }}
387.wa-mb-1 {{ margin-bottom: 0.25rem; }}
388.wa-mb-2 {{ margin-bottom: 0.5rem; }}
389.wa-mb-3 {{ margin-bottom: 1rem; }}
390.wa-mb-4 {{ margin-bottom: 1.5rem; }}
391.wa-mb-5 {{ margin-bottom: 3rem; }}
393.wa-p-0 {{ padding: 0; }}
394.wa-p-1 {{ padding: 0.25rem; }}
395.wa-p-2 {{ padding: 0.5rem; }}
396.wa-p-3 {{ padding: 1rem; }}
397.wa-p-4 {{ padding: 1.5rem; }}
398.wa-p-5 {{ padding: 3rem; }}
400/* Responsive Design */
401@media (max-width: 768px) {{
402 .wa-container {{
403 padding: 0 0.5rem;
404 }}
406 .wa-btn {{
407 width: 100%;
408 margin-bottom: 0.5rem;
409 }}
411 .wa-navbar {{
412 flex-direction: column;
413 gap: 1rem;
414 }}
415}}
416"""
417 return css
419 def _generate_grid_css(self) -> str:
420 """Generate responsive grid CSS."""
421 if not self.settings:
422 self.settings = WebAwesomeStyleSettings()
424 css = ""
425 breakpoints = {
426 "sm": "576px",
427 "md": "768px",
428 "lg": "992px",
429 "xl": "1200px",
430 }
432 for _breakpoint, width in breakpoints.items():
433 css += f"\n@media (min-width: {width}) {{\n"
435 for i in range(1, self.settings.grid_columns + 1):
436 percentage = (i / self.settings.grid_columns) * 100
437 css += (
438 f" .wa-col-{_breakpoint}-{i} {{ flex: 0 0 {percentage:.4f}%; "
439 f"max-width: {percentage:.4f}%; }}\n"
440 )
442 css += "}\n"
444 # Default columns
445 for i in range(1, self.settings.grid_columns + 1):
446 percentage = (i / self.settings.grid_columns) * 100
447 css += f".wa-col-{i} {{ flex: 0 0 {percentage:.4f}%; max-width: {percentage:.4f}%; }}\n"
449 return css
451 def get_component_class(self, component: str) -> str:
452 """Get WebAwesome-specific classes."""
453 class_map = {
454 # Layout
455 "container": "wa-container",
456 "container-fluid": "wa-container-fluid",
457 "row": "wa-row",
458 "col": "wa-col",
459 # Components
460 "card": "wa-card",
461 "card-header": "wa-card-header",
462 "card-body": "wa-card-body",
463 "card-footer": "wa-card-footer",
464 # Buttons
465 "button": "wa-btn wa-btn-primary",
466 "btn": "wa-btn",
467 "btn-primary": "wa-btn wa-btn-primary",
468 "btn-secondary": "wa-btn wa-btn-secondary",
469 "btn-success": "wa-btn wa-btn-success",
470 "btn-warning": "wa-btn wa-btn-warning",
471 "btn-danger": "wa-btn wa-btn-danger",
472 "btn-info": "wa-btn wa-btn-info",
473 # Forms
474 "form-group": "wa-form-group",
475 "form-label": "wa-form-label",
476 "form-control": "wa-form-control",
477 "input": "wa-form-control",
478 "textarea": "wa-form-control",
479 "select": "wa-form-control",
480 # Alerts
481 "alert": "wa-alert",
482 "alert-primary": "wa-alert wa-alert-primary",
483 "alert-success": "wa-alert wa-alert-success",
484 "alert-warning": "wa-alert wa-alert-warning",
485 "alert-danger": "wa-alert wa-alert-danger",
486 # Navigation
487 "navbar": "wa-navbar",
488 "navbar-brand": "wa-navbar-brand",
489 "navbar-nav": "wa-navbar-nav",
490 "navbar-link": "wa-navbar-link",
491 # Icons
492 "icon": "wa-icon fas",
493 "icon-sm": "wa-icon wa-icon-sm fas",
494 "icon-lg": "wa-icon wa-icon-lg fas",
495 "icon-xl": "wa-icon wa-icon-xl fas",
496 "icon-2x": "wa-icon wa-icon-2x fas",
497 }
499 return class_map.get(component, f"wa-{component}")
501 @staticmethod
502 def get_icon_class(icon_name: str, style: str = "solid") -> str:
503 """Get FontAwesome icon class integrated with WebAwesome."""
504 prefix_map = {
505 "solid": "fas",
506 "regular": "far",
507 "brands": "fab",
508 }
510 prefix = prefix_map.get(style, "fas")
512 # Ensure icon name has fa- prefix
513 if not icon_name.startswith("fa-"):
514 icon_name = f"fa-{icon_name}"
516 return f"wa-icon {prefix} {icon_name}"
519# Template function registration for FastBlocks
520def _register_wa_basic_filters(env: Any) -> None:
521 """Register basic WebAwesome filters."""
523 @env.global_("wa_stylesheet_links") # type: ignore[misc]
524 def wa_stylesheet_links() -> str:
525 """Global function for WebAwesome stylesheet links."""
526 styles = depends.get_sync("styles")
527 if isinstance(styles, WebAwesomeStyle):
528 return "\n".join(styles.get_stylesheet_links())
529 return ""
531 @env.filter("wa_class") # type: ignore[misc]
532 def wa_class_filter(component: str) -> str:
533 """Filter for getting WebAwesome component classes."""
534 styles = depends.get_sync("styles")
535 if isinstance(styles, WebAwesomeStyle):
536 return styles.get_component_class(component)
537 return component
539 @env.filter("wa_icon") # type: ignore[misc]
540 def wa_icon_filter(icon_name: str, style: str = "solid") -> str:
541 """Filter for WebAwesome icon classes."""
542 styles = depends.get_sync("styles")
543 if isinstance(styles, WebAwesomeStyle):
544 return styles.get_icon_class(icon_name, style)
545 return f"fa-{icon_name}"
548def _register_wa_button_functions(env: Any) -> None:
549 """Register WebAwesome button component functions."""
551 @env.global_("wa_button") # type: ignore[misc]
552 def wa_button(
553 text: str, variant: str = "primary", icon: str | None = None, **attributes: Any
554 ) -> str:
555 """Generate WebAwesome button with optional icon."""
556 styles = depends.get_sync("styles")
557 if not isinstance(styles, WebAwesomeStyle):
558 return f'<button class="btn">{text}</button>'
560 btn_class = styles.get_component_class(f"btn-{variant}")
561 if "class" in attributes:
562 btn_class += f" {attributes.pop('class')}"
564 content = ""
565 if icon:
566 content += f'<i class="{styles.get_icon_class(icon)}"></i> '
567 content += text
569 attr_string = " ".join(f'{k}="{v}"' for k, v in attributes.items())
570 return f'<button class="{btn_class}" {attr_string}>{content}</button>'
573def _register_wa_card_functions(env: Any) -> None:
574 """Register WebAwesome card component functions."""
576 @env.global_("wa_card") # type: ignore[misc]
577 def wa_card(
578 title: str | None = None,
579 content: str = "",
580 footer: str | None = None,
581 **attributes: Any,
582 ) -> str:
583 """Generate WebAwesome card component."""
584 styles = depends.get_sync("styles")
585 if not isinstance(styles, WebAwesomeStyle):
586 return f'<div class="card">{content}</div>'
588 card_class = styles.get_component_class("card")
589 if "class" in attributes:
590 card_class += f" {attributes.pop('class')}"
592 card_content = ""
593 if title:
594 card_content += f'<div class="{styles.get_component_class("card-header")}">{title}</div>'
595 card_content += (
596 f'<div class="{styles.get_component_class("card-body")}">{content}</div>'
597 )
598 if footer:
599 card_content += f'<div class="{styles.get_component_class("card-footer")}">{footer}</div>'
601 attr_string = " ".join(f'{k}="{v}"' for k, v in attributes.items())
602 return f'<div class="{card_class}" {attr_string}>{card_content}</div>'
605def register_webawesome_functions(env: Any) -> None:
606 """Register WebAwesome functions for Jinja2 templates."""
607 _register_wa_basic_filters(env)
608 _register_wa_button_functions(env)
609 _register_wa_card_functions(env)
612StyleSettings = WebAwesomeStyleSettings
613Style = WebAwesomeStyle
615depends.set(Style, "webawesome")
617# ACB 0.19.0+ compatibility
618__all__ = [
619 "WebAwesomeStyle",
620 "WebAwesomeStyleSettings",
621 "register_webawesome_functions",
622 "Style",
623 "StyleSettings",
624]