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

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.depends import depends 

8 

9from ._base import StyleBase, StyleBaseSettings 

10 

11 

12class WebAwesomeStyleSettings(StyleBaseSettings): 

13 """Settings for WebAwesome styles adapter.""" 

14 

15 # Required ACB 0.19.0+ metadata 

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

17 MODULE_STATUS: str = "stable" 

18 

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 

25 

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" 

34 

35 # Layout settings 

36 container_max_width: str = "1200px" 

37 grid_columns: int = 12 

38 gutter_width: str = "1rem" 

39 

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" 

46 

47 

48class WebAwesomeStyle(StyleBase): 

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

50 

51 # Required ACB 0.19.0+ metadata 

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

53 MODULE_STATUS: str = "stable" 

54 

55 def __init__(self) -> None: 

56 """Initialize WebAwesome adapter.""" 

57 super().__init__() 

58 self.settings: WebAwesomeStyleSettings | None = None 

59 

60 # Register with ACB dependency system 

61 with suppress(Exception): 

62 depends.set(self) 

63 

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

65 """Get WebAwesome stylesheet links.""" 

66 if not self.settings: 

67 self.settings = WebAwesomeStyleSettings() 

68 

69 links = [] 

70 

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 ) 

79 

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 ) 

84 

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 ) 

89 

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 ) 

95 

96 # Generate inline CSS for WebAwesome system 

97 inline_css = self._generate_webawesome_css() 

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

99 

100 return links 

101 

102 def _generate_webawesome_css(self) -> str: 

103 """Generate WebAwesome CSS framework.""" 

104 if not self.settings: 

105 self.settings = WebAwesomeStyleSettings() 

106 

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

122 

123/* Reset and Base */ 

124*, *::before, *::after {{ 

125 box-sizing: border-box; 

126}} 

127 

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

135 

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

142 

143.wa-container-fluid {{ 

144 width: 100%; 

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

146}} 

147 

148/* Grid System */ 

149.wa-row {{ 

150 display: flex; 

151 flex-wrap: wrap; 

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

153}} 

154 

155.wa-col {{ 

156 flex: 1; 

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

158}} 

159 

160/* Responsive columns */ 

161{self._generate_grid_css()} 

162 

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

171 

172.wa-card-header {{ 

173 padding: 1rem; 

174 background: #f8f9fa; 

175 border-bottom: 1px solid #e9ecef; 

176 font-weight: 600; 

177}} 

178 

179.wa-card-body {{ 

180 padding: 1rem; 

181}} 

182 

183.wa-card-footer {{ 

184 padding: 1rem; 

185 background: #f8f9fa; 

186 border-top: 1px solid #e9ecef; 

187}} 

188 

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

204 

205.wa-btn:hover {{ 

206 transform: translateY(-1px); 

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

208}} 

209 

210.wa-btn-primary {{ 

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

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

213 color: white; 

214}} 

215 

216.wa-btn-secondary {{ 

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

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

219 color: white; 

220}} 

221 

222.wa-btn-success {{ 

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

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

225 color: white; 

226}} 

227 

228.wa-btn-warning {{ 

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

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

231 color: #212529; 

232}} 

233 

234.wa-btn-danger {{ 

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

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

237 color: white; 

238}} 

239 

240.wa-btn-info {{ 

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

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

243 color: white; 

244}} 

245 

246/* Form Controls */ 

247.wa-form-group {{ 

248 margin-bottom: 1rem; 

249}} 

250 

251.wa-form-label {{ 

252 display: block; 

253 margin-bottom: 0.5rem; 

254 font-weight: 500; 

255 color: #495057; 

256}} 

257 

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

270 

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

276 

277/* Alert System */ 

278.wa-alert {{ 

279 padding: 1rem; 

280 margin-bottom: 1rem; 

281 border: 1px solid transparent; 

282 border-radius: 0.375rem; 

283}} 

284 

285.wa-alert-primary {{ 

286 color: #084298; 

287 background: #cfe2ff; 

288 border-color: #b6d4fe; 

289}} 

290 

291.wa-alert-success {{ 

292 color: #0f5132; 

293 background: #d1e7dd; 

294 border-color: #badbcc; 

295}} 

296 

297.wa-alert-warning {{ 

298 color: #664d03; 

299 background: #fff3cd; 

300 border-color: #ffecb5; 

301}} 

302 

303.wa-alert-danger {{ 

304 color: #842029; 

305 background: #f8d7da; 

306 border-color: #f5c2c7; 

307}} 

308 

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

318 

319.wa-navbar-brand {{ 

320 font-size: 1.25rem; 

321 font-weight: 600; 

322 text-decoration: none; 

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

324}} 

325 

326.wa-navbar-nav {{ 

327 display: flex; 

328 list-style: none; 

329 margin: 0; 

330 padding: 0; 

331 gap: 1rem; 

332}} 

333 

334.wa-navbar-link {{ 

335 text-decoration: none; 

336 color: #495057; 

337 transition: color 0.2s; 

338}} 

339 

340.wa-navbar-link:hover {{ 

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

342}} 

343 

344/* Icon Integration */ 

345.wa-icon {{ 

346 display: inline-block; 

347 width: 1em; 

348 height: 1em; 

349 vertical-align: -0.125em; 

350}} 

351 

352.wa-icon-sm {{ 

353 font-size: 0.875rem; 

354}} 

355 

356.wa-icon-lg {{ 

357 font-size: 1.125rem; 

358}} 

359 

360.wa-icon-xl {{ 

361 font-size: 1.5rem; 

362}} 

363 

364.wa-icon-2x {{ 

365 font-size: 2rem; 

366}} 

367 

368/* Utility Classes */ 

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

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

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

372 

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

378 

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

385 

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

392 

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

399 

400/* Responsive Design */ 

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

402 .wa-container {{ 

403 padding: 0 0.5rem; 

404 }} 

405 

406 .wa-btn {{ 

407 width: 100%; 

408 margin-bottom: 0.5rem; 

409 }} 

410 

411 .wa-navbar {{ 

412 flex-direction: column; 

413 gap: 1rem; 

414 }} 

415}} 

416""" 

417 return css 

418 

419 def _generate_grid_css(self) -> str: 

420 """Generate responsive grid CSS.""" 

421 if not self.settings: 

422 self.settings = WebAwesomeStyleSettings() 

423 

424 css = "" 

425 breakpoints = { 

426 "sm": "576px", 

427 "md": "768px", 

428 "lg": "992px", 

429 "xl": "1200px", 

430 } 

431 

432 for _breakpoint, width in breakpoints.items(): 

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

434 

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 ) 

441 

442 css += "}\n" 

443 

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" 

448 

449 return css 

450 

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 } 

498 

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

500 

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 } 

509 

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

511 

512 # Ensure icon name has fa- prefix 

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

514 icon_name = f"fa-{icon_name}" 

515 

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

517 

518 

519# Template function registration for FastBlocks 

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

521 """Register basic WebAwesome filters.""" 

522 

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

530 

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 

538 

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

546 

547 

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

549 """Register WebAwesome button component functions.""" 

550 

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

559 

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

561 if "class" in attributes: 

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

563 

564 content = "" 

565 if icon: 

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

567 content += text 

568 

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

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

571 

572 

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

574 """Register WebAwesome card component functions.""" 

575 

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

587 

588 card_class = styles.get_component_class("card") 

589 if "class" in attributes: 

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

591 

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

600 

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

603 

604 

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) 

610 

611 

612StyleSettings = WebAwesomeStyleSettings 

613Style = WebAwesomeStyle 

614 

615depends.set(Style, "webawesome") 

616 

617# ACB 0.19.0+ compatibility 

618__all__ = [ 

619 "WebAwesomeStyle", 

620 "WebAwesomeStyleSettings", 

621 "register_webawesome_functions", 

622 "Style", 

623 "StyleSettings", 

624]