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

1"""WebAwesome styles adapter for FastBlocks with integrated icon system.""" 

2 

3from contextlib import suppress 

4from typing import Any 

5from uuid import UUID 

6 

7from acb.config import Settings 

8from acb.depends import depends 

9 

10from ._base import StylesBase 

11 

12 

13class WebAwesomeSettings(Settings): # type: ignore[misc] 

14 """Settings for WebAwesome styles adapter.""" 

15 

16 # Required ACB 0.19.0+ metadata 

17 MODULE_ID: UUID = UUID("01937d86-7a5c-9f6d-b8e9-d7e8f9a0b1c2") # Static UUID7 

18 MODULE_STATUS: str = "stable" 

19 

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 

26 

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" 

35 

36 # Layout settings 

37 container_max_width: str = "1200px" 

38 grid_columns: int = 12 

39 gutter_width: str = "1rem" 

40 

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" 

47 

48 

49class WebAwesomeAdapter(StylesBase): 

50 """WebAwesome styles adapter with integrated icons and components.""" 

51 

52 # Required ACB 0.19.0+ metadata 

53 MODULE_ID: UUID = UUID("01937d86-7a5c-9f6d-b8e9-d7e8f9a0b1c2") # Static UUID7 

54 MODULE_STATUS: str = "stable" 

55 

56 def __init__(self) -> None: 

57 """Initialize WebAwesome adapter.""" 

58 super().__init__() 

59 self.settings: WebAwesomeSettings | None = None 

60 

61 # Register with ACB dependency system 

62 with suppress(Exception): 

63 depends.set(self) 

64 

65 def get_stylesheet_links(self) -> list[str]: 

66 """Get WebAwesome stylesheet links.""" 

67 if not self.settings: 

68 self.settings = WebAwesomeSettings() 

69 

70 links = [] 

71 

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 ) 

80 

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 ) 

85 

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 ) 

90 

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 ) 

96 

97 # Generate inline CSS for WebAwesome system 

98 inline_css = self._generate_webawesome_css() 

99 links.append(f"<style>{inline_css}</style>") 

100 

101 return links 

102 

103 def _generate_webawesome_css(self) -> str: 

104 """Generate WebAwesome CSS framework.""" 

105 if not self.settings: 

106 self.settings = WebAwesomeSettings() 

107 

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}} 

123 

124/* Reset and Base */ 

125*, *::before, *::after {{ 

126 box-sizing: border-box; 

127}} 

128 

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}} 

136 

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}} 

143 

144.wa-container-fluid {{ 

145 width: 100%; 

146 padding: 0 var(--wa-gutter); 

147}} 

148 

149/* Grid System */ 

150.wa-row {{ 

151 display: flex; 

152 flex-wrap: wrap; 

153 margin: 0 calc(var(--wa-gutter) / -2); 

154}} 

155 

156.wa-col {{ 

157 flex: 1; 

158 padding: 0 calc(var(--wa-gutter) / 2); 

159}} 

160 

161/* Responsive columns */ 

162{self._generate_grid_css()} 

163 

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}} 

172 

173.wa-card-header {{ 

174 padding: 1rem; 

175 background: #f8f9fa; 

176 border-bottom: 1px solid #e9ecef; 

177 font-weight: 600; 

178}} 

179 

180.wa-card-body {{ 

181 padding: 1rem; 

182}} 

183 

184.wa-card-footer {{ 

185 padding: 1rem; 

186 background: #f8f9fa; 

187 border-top: 1px solid #e9ecef; 

188}} 

189 

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}} 

205 

206.wa-btn:hover {{ 

207 transform: translateY(-1px); 

208 box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.15); 

209}} 

210 

211.wa-btn-primary {{ 

212 background: var(--wa-primary); 

213 border-color: var(--wa-primary); 

214 color: white; 

215}} 

216 

217.wa-btn-secondary {{ 

218 background: var(--wa-secondary); 

219 border-color: var(--wa-secondary); 

220 color: white; 

221}} 

222 

223.wa-btn-success {{ 

224 background: var(--wa-success); 

225 border-color: var(--wa-success); 

226 color: white; 

227}} 

228 

229.wa-btn-warning {{ 

230 background: var(--wa-warning); 

231 border-color: var(--wa-warning); 

232 color: #212529; 

233}} 

234 

235.wa-btn-danger {{ 

236 background: var(--wa-danger); 

237 border-color: var(--wa-danger); 

238 color: white; 

239}} 

240 

241.wa-btn-info {{ 

242 background: var(--wa-info); 

243 border-color: var(--wa-info); 

244 color: white; 

245}} 

246 

247/* Form Controls */ 

248.wa-form-group {{ 

249 margin-bottom: 1rem; 

250}} 

251 

252.wa-form-label {{ 

253 display: block; 

254 margin-bottom: 0.5rem; 

255 font-weight: 500; 

256 color: #495057; 

257}} 

258 

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}} 

271 

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}} 

277 

278/* Alert System */ 

279.wa-alert {{ 

280 padding: 1rem; 

281 margin-bottom: 1rem; 

282 border: 1px solid transparent; 

283 border-radius: 0.375rem; 

284}} 

285 

286.wa-alert-primary {{ 

287 color: #084298; 

288 background: #cfe2ff; 

289 border-color: #b6d4fe; 

290}} 

291 

292.wa-alert-success {{ 

293 color: #0f5132; 

294 background: #d1e7dd; 

295 border-color: #badbcc; 

296}} 

297 

298.wa-alert-warning {{ 

299 color: #664d03; 

300 background: #fff3cd; 

301 border-color: #ffecb5; 

302}} 

303 

304.wa-alert-danger {{ 

305 color: #842029; 

306 background: #f8d7da; 

307 border-color: #f5c2c7; 

308}} 

309 

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}} 

319 

320.wa-navbar-brand {{ 

321 font-size: 1.25rem; 

322 font-weight: 600; 

323 text-decoration: none; 

324 color: var(--wa-primary); 

325}} 

326 

327.wa-navbar-nav {{ 

328 display: flex; 

329 list-style: none; 

330 margin: 0; 

331 padding: 0; 

332 gap: 1rem; 

333}} 

334 

335.wa-navbar-link {{ 

336 text-decoration: none; 

337 color: #495057; 

338 transition: color 0.2s; 

339}} 

340 

341.wa-navbar-link:hover {{ 

342 color: var(--wa-primary); 

343}} 

344 

345/* Icon Integration */ 

346.wa-icon {{ 

347 display: inline-block; 

348 width: 1em; 

349 height: 1em; 

350 vertical-align: -0.125em; 

351}} 

352 

353.wa-icon-sm {{ 

354 font-size: 0.875rem; 

355}} 

356 

357.wa-icon-lg {{ 

358 font-size: 1.125rem; 

359}} 

360 

361.wa-icon-xl {{ 

362 font-size: 1.5rem; 

363}} 

364 

365.wa-icon-2x {{ 

366 font-size: 2rem; 

367}} 

368 

369/* Utility Classes */ 

370.wa-text-center {{ text-align: center; }} 

371.wa-text-left {{ text-align: left; }} 

372.wa-text-right {{ text-align: right; }} 

373 

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; }} 

379 

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; }} 

386 

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; }} 

393 

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; }} 

400 

401/* Responsive Design */ 

402@media (max-width: 768px) {{ 

403 .wa-container {{ 

404 padding: 0 0.5rem; 

405 }} 

406 

407 .wa-btn {{ 

408 width: 100%; 

409 margin-bottom: 0.5rem; 

410 }} 

411 

412 .wa-navbar {{ 

413 flex-direction: column; 

414 gap: 1rem; 

415 }} 

416}} 

417""" 

418 return css 

419 

420 def _generate_grid_css(self) -> str: 

421 """Generate responsive grid CSS.""" 

422 if not self.settings: 

423 self.settings = WebAwesomeSettings() 

424 

425 css = "" 

426 breakpoints = { 

427 "sm": "576px", 

428 "md": "768px", 

429 "lg": "992px", 

430 "xl": "1200px", 

431 } 

432 

433 for breakpoint, width in breakpoints.items(): 

434 css += f"\n@media (min-width: {width}) {{\n" 

435 

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" 

439 

440 css += "}\n" 

441 

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" 

446 

447 return css 

448 

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 } 

496 

497 return class_map.get(component, f"wa-{component}") 

498 

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 } 

506 

507 prefix = prefix_map.get(style, "fas") 

508 

509 # Ensure icon name has fa- prefix 

510 if not icon_name.startswith("fa-"): 

511 icon_name = f"fa-{icon_name}" 

512 

513 return f"wa-icon {prefix} {icon_name}" 

514 

515 

516# Template function registration for FastBlocks 

517def _register_wa_basic_filters(env: Any) -> None: 

518 """Register basic WebAwesome filters.""" 

519 

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 "" 

527 

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 

535 

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}" 

543 

544 

545def _register_wa_button_functions(env: Any) -> None: 

546 """Register WebAwesome button component functions.""" 

547 

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>' 

556 

557 btn_class = styles.get_component_class(f"btn-{variant}") 

558 if "class" in attributes: 

559 btn_class += f" {attributes.pop('class')}" 

560 

561 content = "" 

562 if icon: 

563 content += f'<i class="{styles.get_icon_class(icon)}"></i> ' 

564 content += text 

565 

566 attr_string = " ".join(f'{k}="{v}"' for k, v in attributes.items()) 

567 return f'<button class="{btn_class}" {attr_string}>{content}</button>' 

568 

569 

570def _register_wa_card_functions(env: Any) -> None: 

571 """Register WebAwesome card component functions.""" 

572 

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>' 

584 

585 card_class = styles.get_component_class("card") 

586 if "class" in attributes: 

587 card_class += f" {attributes.pop('class')}" 

588 

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>' 

597 

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>' 

600 

601 

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) 

607 

608 

609# ACB 0.19.0+ compatibility 

610__all__ = ["WebAwesomeAdapter", "WebAwesomeSettings", "register_webawesome_functions"]