Coverage for fastblocks / adapters / style / kelp.py: 38%

128 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-26 03:30 -0800

1"""Kelp styles adapter for FastBlocks with component 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 KelpStyleSettings(StyleBaseSettings): 

13 """Settings for Kelp styles adapter.""" 

14 

15 # Required ACB 0.19.0+ metadata 

16 MODULE_ID: UUID = UUID("01937d86-8b6d-a07e-c9fa-e8f9a0b1c2d3") # Static UUID7 

17 MODULE_STATUS: str = "stable" 

18 

19 # Kelp configuration 

20 version: str = "latest" 

21 cdn_url: str = "https://cdn.jsdelivr.net/npm/kelp" 

22 theme: str = "default" # default, dark, ocean, forest, sunset 

23 

24 # Color system 

25 primary_hue: int = 210 # Blue 

26 secondary_hue: int = 160 # Green 

27 accent_hue: int = 45 # Orange 

28 neutral_hue: int = 220 # Cool gray 

29 

30 # Spacing system (rem units) 

31 spacing_scale: list[str] = [ 

32 "0", 

33 "0.25", 

34 "0.5", 

35 "0.75", 

36 "1", 

37 "1.25", 

38 "1.5", 

39 "2", 

40 "2.5", 

41 "3", 

42 "4", 

43 "5", 

44 "6", 

45 "8", 

46 "10", 

47 "12", 

48 "16", 

49 "20", 

50 "24", 

51 ] 

52 

53 # Typography 

54 font_family_sans: str = "Inter, system-ui, -apple-system, sans-serif" 

55 font_family_mono: str = "JetBrains Mono, 'Fira Code', Consolas, monospace" 

56 font_scale: dict[str, str] = { 

57 "xs": "0.75rem", 

58 "sm": "0.875rem", 

59 "base": "1rem", 

60 "lg": "1.125rem", 

61 "xl": "1.25rem", 

62 "2xl": "1.5rem", 

63 "3xl": "1.875rem", 

64 "4xl": "2.25rem", 

65 "5xl": "3rem", 

66 "6xl": "3.75rem", 

67 } 

68 

69 # Border radius 

70 radius_scale: dict[str, str] = { 

71 "none": "0", 

72 "sm": "0.125rem", 

73 "base": "0.25rem", 

74 "md": "0.375rem", 

75 "lg": "0.5rem", 

76 "xl": "0.75rem", 

77 "2xl": "1rem", 

78 "3xl": "1.5rem", 

79 "full": "9999px", 

80 } 

81 

82 # Shadow system 

83 enable_shadows: bool = True 

84 enable_animations: bool = True 

85 

86 

87class KelpStyle(StyleBase): 

88 """Kelp styles adapter with modern component system.""" 

89 

90 # Required ACB 0.19.0+ metadata 

91 MODULE_ID: UUID = UUID("01937d86-8b6d-a07e-c9fa-e8f9a0b1c2d3") # Static UUID7 

92 MODULE_STATUS: str = "stable" 

93 

94 def __init__(self) -> None: 

95 """Initialize Kelp adapter.""" 

96 super().__init__() 

97 self.settings: KelpStyleSettings | None = None 

98 

99 # Register with ACB dependency system 

100 with suppress(Exception): 

101 depends.set(self) 

102 

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

104 """Get Kelp stylesheet links.""" 

105 if not self.settings: 

106 self.settings = KelpStyleSettings() 

107 

108 links = [] 

109 

110 # Kelp base CSS (if available from CDN) 

111 # Note: Kelp might be a custom framework, so we generate it inline 

112 kelp_css = self._generate_kelp_css() 

113 links.append(f"<style>{kelp_css}</style>") 

114 

115 # Inter font for better typography 

116 links.extend( 

117 ( 

118 '<link rel="preconnect" href="https://fonts.googleapis.com">', 

119 '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>', 

120 '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">', 

121 '<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800&display=swap" rel="stylesheet">', 

122 ) 

123 ) 

124 

125 return links 

126 

127 def _generate_kelp_css(self) -> str: 

128 """Generate Kelp CSS framework.""" 

129 if not self.settings: 

130 self.settings = KelpStyleSettings() 

131 

132 # Generate color variables based on HSL 

133 color_vars = self._generate_color_variables() 

134 spacing_vars = self._generate_spacing_variables() 

135 typography_vars = self._generate_typography_variables() 

136 radius_vars = self._generate_radius_variables() 

137 

138 css = f""" 

139/* Kelp CSS Framework for FastBlocks */ 

140{color_vars} 

141{spacing_vars} 

142{typography_vars} 

143{radius_vars} 

144 

145/* Base Reset */ 

146*, *::before, *::after {{ 

147 box-sizing: border-box; 

148}} 

149 

150* {{ 

151 margin: 0; 

152 padding: 0; 

153}} 

154 

155html {{ 

156 scroll-behavior: smooth; 

157 -webkit-font-smoothing: antialiased; 

158 -moz-osx-font-smoothing: grayscale; 

159}} 

160 

161body {{ 

162 font-family: var(--kelp-font-sans); 

163 font-size: var(--kelp-text-base); 

164 line-height: 1.6; 

165 color: var(--kelp-gray-900); 

166 background-color: var(--kelp-gray-50); 

167 min-height: 100vh; 

168}} 

169 

170/* Layout System */ 

171.kelp-container {{ 

172 width: 100%; 

173 max-width: 1200px; 

174 margin: 0 auto; 

175 padding: 0 var(--kelp-space-4); 

176}} 

177 

178.kelp-container-sm {{ max-width: 640px; }} 

179.kelp-container-md {{ max-width: 768px; }} 

180.kelp-container-lg {{ max-width: 1024px; }} 

181.kelp-container-xl {{ max-width: 1280px; }} 

182.kelp-container-2xl {{ max-width: 1536px; }} 

183 

184/* Flexbox Grid */ 

185.kelp-flex {{ 

186 display: flex; 

187}} 

188 

189.kelp-flex-col {{ 

190 flex-direction: column; 

191}} 

192 

193.kelp-flex-wrap {{ 

194 flex-wrap: wrap; 

195}} 

196 

197.kelp-items-center {{ 

198 align-items: center; 

199}} 

200 

201.kelp-items-start {{ 

202 align-items: flex-start; 

203}} 

204 

205.kelp-items-end {{ 

206 align-items: flex-end; 

207}} 

208 

209.kelp-justify-center {{ 

210 justify-content: center; 

211}} 

212 

213.kelp-justify-between {{ 

214 justify-content: space-between; 

215}} 

216 

217.kelp-justify-around {{ 

218 justify-content: space-around; 

219}} 

220 

221.kelp-gap-1 {{ gap: var(--kelp-space-1); }} 

222.kelp-gap-2 {{ gap: var(--kelp-space-2); }} 

223.kelp-gap-3 {{ gap: var(--kelp-space-3); }} 

224.kelp-gap-4 {{ gap: var(--kelp-space-4); }} 

225.kelp-gap-6 {{ gap: var(--kelp-space-6); }} 

226.kelp-gap-8 {{ gap: var(--kelp-space-8); }} 

227 

228/* Grid System */ 

229.kelp-grid {{ 

230 display: grid; 

231}} 

232 

233.kelp-grid-cols-1 {{ grid-template-columns: repeat(1, minmax(0, 1fr)); }} 

234.kelp-grid-cols-2 {{ grid-template-columns: repeat(2, minmax(0, 1fr)); }} 

235.kelp-grid-cols-3 {{ grid-template-columns: repeat(3, minmax(0, 1fr)); }} 

236.kelp-grid-cols-4 {{ grid-template-columns: repeat(4, minmax(0, 1fr)); }} 

237.kelp-grid-cols-6 {{ grid-template-columns: repeat(6, minmax(0, 1fr)); }} 

238.kelp-grid-cols-12 {{ grid-template-columns: repeat(12, minmax(0, 1fr)); }} 

239 

240/* Component: Card */ 

241.kelp-card {{ 

242 background: white; 

243 border: 1px solid var(--kelp-gray-200); 

244 border-radius: var(--kelp-radius-lg); 

245 overflow: hidden; 

246 transition: all 0.2s ease; 

247}} 

248 

249.kelp-card:hover {{ 

250 box-shadow: var(--kelp-shadow-lg); 

251 transform: translateY(-2px); 

252}} 

253 

254.kelp-card-header {{ 

255 padding: var(--kelp-space-4) var(--kelp-space-6); 

256 border-bottom: 1px solid var(--kelp-gray-200); 

257 background: var(--kelp-gray-50); 

258}} 

259 

260.kelp-card-body {{ 

261 padding: var(--kelp-space-6); 

262}} 

263 

264.kelp-card-footer {{ 

265 padding: var(--kelp-space-4) var(--kelp-space-6); 

266 border-top: 1px solid var(--kelp-gray-200); 

267 background: var(--kelp-gray-50); 

268}} 

269 

270/* Component: Button */ 

271.kelp-btn {{ 

272 display: inline-flex; 

273 align-items: center; 

274 justify-content: center; 

275 gap: var(--kelp-space-2); 

276 padding: var(--kelp-space-3) var(--kelp-space-6); 

277 font-family: inherit; 

278 font-size: var(--kelp-text-sm); 

279 font-weight: 500; 

280 line-height: 1; 

281 border: 1px solid transparent; 

282 border-radius: var(--kelp-radius-md); 

283 cursor: pointer; 

284 transition: all 0.2s ease; 

285 text-decoration: none; 

286 white-space: nowrap; 

287}} 

288 

289.kelp-btn:focus {{ 

290 outline: 2px solid var(--kelp-primary-500); 

291 outline-offset: 2px; 

292}} 

293 

294.kelp-btn:disabled {{ 

295 opacity: 0.6; 

296 cursor: not-allowed; 

297}} 

298 

299.kelp-btn-primary {{ 

300 background: var(--kelp-primary-600); 

301 border-color: var(--kelp-primary-600); 

302 color: white; 

303}} 

304 

305.kelp-btn-primary:hover:not(:disabled) {{ 

306 background: var(--kelp-primary-700); 

307 border-color: var(--kelp-primary-700); 

308 transform: translateY(-1px); 

309 box-shadow: var(--kelp-shadow-md); 

310}} 

311 

312.kelp-btn-secondary {{ 

313 background: var(--kelp-secondary-600); 

314 border-color: var(--kelp-secondary-600); 

315 color: white; 

316}} 

317 

318.kelp-btn-secondary:hover:not(:disabled) {{ 

319 background: var(--kelp-secondary-700); 

320 border-color: var(--kelp-secondary-700); 

321 transform: translateY(-1px); 

322 box-shadow: var(--kelp-shadow-md); 

323}} 

324 

325.kelp-btn-outline {{ 

326 background: transparent; 

327 border-color: var(--kelp-gray-300); 

328 color: var(--kelp-gray-700); 

329}} 

330 

331.kelp-btn-outline:hover:not(:disabled) {{ 

332 background: var(--kelp-gray-50); 

333 border-color: var(--kelp-gray-400); 

334}} 

335 

336.kelp-btn-ghost {{ 

337 background: transparent; 

338 border-color: transparent; 

339 color: var(--kelp-gray-700); 

340}} 

341 

342.kelp-btn-ghost:hover:not(:disabled) {{ 

343 background: var(--kelp-gray-100); 

344}} 

345 

346/* Button Sizes */ 

347.kelp-btn-sm {{ 

348 padding: var(--kelp-space-2) var(--kelp-space-4); 

349 font-size: var(--kelp-text-xs); 

350}} 

351 

352.kelp-btn-lg {{ 

353 padding: var(--kelp-space-4) var(--kelp-space-8); 

354 font-size: var(--kelp-text-base); 

355}} 

356 

357/* Component: Form Controls */ 

358.kelp-form-group {{ 

359 margin-bottom: var(--kelp-space-4); 

360}} 

361 

362.kelp-label {{ 

363 display: block; 

364 margin-bottom: var(--kelp-space-2); 

365 font-size: var(--kelp-text-sm); 

366 font-weight: 500; 

367 color: var(--kelp-gray-700); 

368}} 

369 

370.kelp-input {{ 

371 width: 100%; 

372 padding: var(--kelp-space-3); 

373 font-family: inherit; 

374 font-size: var(--kelp-text-sm); 

375 border: 1px solid var(--kelp-gray-300); 

376 border-radius: var(--kelp-radius-md); 

377 background: white; 

378 color: var(--kelp-gray-900); 

379 transition: all 0.2s ease; 

380}} 

381 

382.kelp-input:focus {{ 

383 outline: none; 

384 border-color: var(--kelp-primary-500); 

385 box-shadow: 0 0 0 3px var(--kelp-primary-100); 

386}} 

387 

388.kelp-input:disabled {{ 

389 background: var(--kelp-gray-100); 

390 color: var(--kelp-gray-500); 

391 cursor: not-allowed; 

392}} 

393 

394.kelp-textarea {{ 

395 resize: vertical; 

396 min-height: 80px; 

397}} 

398 

399.kelp-select {{ 

400 background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); 

401 background-position: right 0.5rem center; 

402 background-repeat: no-repeat; 

403 background-size: 1.5em 1.5em; 

404 padding-right: 2.5rem; 

405}} 

406 

407/* Component: Alert */ 

408.kelp-alert {{ 

409 padding: var(--kelp-space-4); 

410 border-radius: var(--kelp-radius-md); 

411 border: 1px solid; 

412 margin-bottom: var(--kelp-space-4); 

413}} 

414 

415.kelp-alert-info {{ 

416 background: var(--kelp-primary-50); 

417 border-color: var(--kelp-primary-200); 

418 color: var(--kelp-primary-800); 

419}} 

420 

421.kelp-alert-success {{ 

422 background: var(--kelp-secondary-50); 

423 border-color: var(--kelp-secondary-200); 

424 color: var(--kelp-secondary-800); 

425}} 

426 

427.kelp-alert-warning {{ 

428 background: var(--kelp-accent-50); 

429 border-color: var(--kelp-accent-200); 

430 color: var(--kelp-accent-800); 

431}} 

432 

433.kelp-alert-error {{ 

434 background: #fef2f2; 

435 border-color: #fecaca; 

436 color: #991b1b; 

437}} 

438 

439/* Component: Badge */ 

440.kelp-badge {{ 

441 display: inline-flex; 

442 align-items: center; 

443 padding: var(--kelp-space-1) var(--kelp-space-2); 

444 font-size: var(--kelp-text-xs); 

445 font-weight: 500; 

446 border-radius: var(--kelp-radius-full); 

447 text-transform: uppercase; 

448 letter-spacing: 0.05em; 

449}} 

450 

451.kelp-badge-primary {{ 

452 background: var(--kelp-primary-100); 

453 color: var(--kelp-primary-800); 

454}} 

455 

456.kelp-badge-secondary {{ 

457 background: var(--kelp-secondary-100); 

458 color: var(--kelp-secondary-800); 

459}} 

460 

461.kelp-badge-gray {{ 

462 background: var(--kelp-gray-100); 

463 color: var(--kelp-gray-800); 

464}} 

465 

466/* Utility Classes */ 

467{self._generate_utility_classes()} 

468 

469/* Responsive Design */ 

470{self._generate_responsive_classes()} 

471 

472/* Animation System */ 

473{self._generate_animations()} 

474""" 

475 return css 

476 

477 def _generate_color_variables(self) -> str: 

478 """Generate CSS color variables based on HSL.""" 

479 if not self.settings: 

480 self.settings = KelpStyleSettings() 

481 

482 def hsl_colors(hue: int, prefix: str) -> str: 

483 """Generate HSL color scale.""" 

484 return f""" 

485 --kelp-{prefix}-50: hsl({hue}, 100%, 97%); 

486 --kelp-{prefix}-100: hsl({hue}, 100%, 94%); 

487 --kelp-{prefix}-200: hsl({hue}, 100%, 87%); 

488 --kelp-{prefix}-300: hsl({hue}, 100%, 80%); 

489 --kelp-{prefix}-400: hsl({hue}, 100%, 66%); 

490 --kelp-{prefix}-500: hsl({hue}, 100%, 50%); 

491 --kelp-{prefix}-600: hsl({hue}, 100%, 45%); 

492 --kelp-{prefix}-700: hsl({hue}, 100%, 35%); 

493 --kelp-{prefix}-800: hsl({hue}, 100%, 25%); 

494 --kelp-{prefix}-900: hsl({hue}, 100%, 15%);""" 

495 

496 return f""" 

497:root {{ 

498 /* Color System */ 

499 {hsl_colors(self.settings.primary_hue, "primary")} 

500 {hsl_colors(self.settings.secondary_hue, "secondary")} 

501 {hsl_colors(self.settings.accent_hue, "accent")} 

502 

503 /* Neutral Colors */ 

504 --kelp-gray-50: hsl({self.settings.neutral_hue}, 20%, 98%); 

505 --kelp-gray-100: hsl({self.settings.neutral_hue}, 20%, 95%); 

506 --kelp-gray-200: hsl({self.settings.neutral_hue}, 15%, 89%); 

507 --kelp-gray-300: hsl({self.settings.neutral_hue}, 10%, 78%); 

508 --kelp-gray-400: hsl({self.settings.neutral_hue}, 8%, 56%); 

509 --kelp-gray-500: hsl({self.settings.neutral_hue}, 6%, 45%); 

510 --kelp-gray-600: hsl({self.settings.neutral_hue}, 5%, 35%); 

511 --kelp-gray-700: hsl({self.settings.neutral_hue}, 5%, 25%); 

512 --kelp-gray-800: hsl({self.settings.neutral_hue}, 5%, 15%); 

513 --kelp-gray-900: hsl({self.settings.neutral_hue}, 5%, 9%); 

514 

515 /* Shadow System */ 

516 --kelp-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); 

517 --kelp-shadow-base: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 

518 --kelp-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 

519 --kelp-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 

520 --kelp-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 

521}}""" 

522 

523 def _generate_spacing_variables(self) -> str: 

524 """Generate spacing variables.""" 

525 if not self.settings: 

526 self.settings = KelpStyleSettings() 

527 

528 vars_css = "" 

529 for i, value in enumerate(self.settings.spacing_scale): 

530 vars_css += f" --kelp-space-{i}: {value}rem;\n" 

531 

532 return f""" 

533 /* Spacing System */ 

534{vars_css}""" 

535 

536 def _generate_typography_variables(self) -> str: 

537 """Generate typography variables.""" 

538 if not self.settings: 

539 self.settings = KelpStyleSettings() 

540 

541 font_vars = f""" 

542 /* Typography System */ 

543 --kelp-font-sans: {self.settings.font_family_sans}; 

544 --kelp-font-mono: {self.settings.font_family_mono}; 

545""" 

546 

547 for name, size in self.settings.font_scale.items(): 

548 font_vars += f" --kelp-text-{name}: {size};\n" 

549 

550 return font_vars 

551 

552 def _generate_radius_variables(self) -> str: 

553 """Generate border radius variables.""" 

554 if not self.settings: 

555 self.settings = KelpStyleSettings() 

556 

557 radius_vars = " /* Border Radius System */\n" 

558 for name, value in self.settings.radius_scale.items(): 

559 radius_vars += f" --kelp-radius-{name}: {value};\n" 

560 

561 return radius_vars 

562 

563 @staticmethod 

564 def _generate_utility_classes() -> str: 

565 """Generate utility classes.""" 

566 return """ 

567/* Text Utilities */ 

568.kelp-text-left { text-align: left; } 

569.kelp-text-center { text-align: center; } 

570.kelp-text-right { text-align: right; } 

571.kelp-text-justify { text-align: justify; } 

572 

573.kelp-font-sans { font-family: var(--kelp-font-sans); } 

574.kelp-font-mono { font-family: var(--kelp-font-mono); } 

575 

576.kelp-text-xs { font-size: var(--kelp-text-xs); } 

577.kelp-text-sm { font-size: var(--kelp-text-sm); } 

578.kelp-text-base { font-size: var(--kelp-text-base); } 

579.kelp-text-lg { font-size: var(--kelp-text-lg); } 

580.kelp-text-xl { font-size: var(--kelp-text-xl); } 

581.kelp-text-2xl { font-size: var(--kelp-text-2xl); } 

582.kelp-text-3xl { font-size: var(--kelp-text-3xl); } 

583 

584.kelp-font-light { font-weight: 300; } 

585.kelp-font-normal { font-weight: 400; } 

586.kelp-font-medium { font-weight: 500; } 

587.kelp-font-semibold { font-weight: 600; } 

588.kelp-font-bold { font-weight: 700; } 

589 

590/* Display Utilities */ 

591.kelp-block { display: block; } 

592.kelp-inline { display: inline; } 

593.kelp-inline-block { display: inline-block; } 

594.kelp-hidden { display: none; } 

595 

596/* Spacing Utilities */ 

597.kelp-m-0 { margin: var(--kelp-space-0); } 

598.kelp-m-1 { margin: var(--kelp-space-1); } 

599.kelp-m-2 { margin: var(--kelp-space-2); } 

600.kelp-m-3 { margin: var(--kelp-space-3); } 

601.kelp-m-4 { margin: var(--kelp-space-4); } 

602.kelp-m-6 { margin: var(--kelp-space-6); } 

603.kelp-m-8 { margin: var(--kelp-space-8); } 

604 

605.kelp-p-0 { padding: var(--kelp-space-0); } 

606.kelp-p-1 { padding: var(--kelp-space-1); } 

607.kelp-p-2 { padding: var(--kelp-space-2); } 

608.kelp-p-3 { padding: var(--kelp-space-3); } 

609.kelp-p-4 { padding: var(--kelp-space-4); } 

610.kelp-p-6 { padding: var(--kelp-space-6); } 

611.kelp-p-8 { padding: var(--kelp-space-8); } 

612 

613/* Color Utilities */ 

614.kelp-text-primary { color: var(--kelp-primary-600); } 

615.kelp-text-secondary { color: var(--kelp-secondary-600); } 

616.kelp-text-gray { color: var(--kelp-gray-600); } 

617.kelp-text-white { color: white; } 

618 

619.kelp-bg-primary { background-color: var(--kelp-primary-600); } 

620.kelp-bg-secondary { background-color: var(--kelp-secondary-600); } 

621.kelp-bg-gray { background-color: var(--kelp-gray-100); } 

622.kelp-bg-white { background-color: white; } 

623 

624/* Border Utilities */ 

625.kelp-border { border: 1px solid var(--kelp-gray-200); } 

626.kelp-border-0 { border: none; } 

627.kelp-rounded { border-radius: var(--kelp-radius-base); } 

628.kelp-rounded-md { border-radius: var(--kelp-radius-md); } 

629.kelp-rounded-lg { border-radius: var(--kelp-radius-lg); } 

630.kelp-rounded-full { border-radius: var(--kelp-radius-full); } 

631 

632/* Shadow Utilities */ 

633.kelp-shadow { box-shadow: var(--kelp-shadow-base); } 

634.kelp-shadow-md { box-shadow: var(--kelp-shadow-md); } 

635.kelp-shadow-lg { box-shadow: var(--kelp-shadow-lg); } 

636.kelp-shadow-none { box-shadow: none; }""" 

637 

638 @staticmethod 

639 def _generate_responsive_classes() -> str: 

640 """Generate responsive design classes.""" 

641 return """ 

642/* Responsive Design */ 

643@media (max-width: 640px) { 

644 .kelp-container { 

645 padding: 0 var(--kelp-space-2); 

646 } 

647 

648 .kelp-grid-cols-2 { 

649 grid-template-columns: repeat(1, minmax(0, 1fr)); 

650 } 

651 

652 .kelp-grid-cols-3 { 

653 grid-template-columns: repeat(1, minmax(0, 1fr)); 

654 } 

655 

656 .kelp-grid-cols-4 { 

657 grid-template-columns: repeat(2, minmax(0, 1fr)); 

658 } 

659} 

660 

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

662 .kelp-md\\:hidden { 

663 display: none; 

664 } 

665 

666 .kelp-md\\:flex { 

667 display: flex; 

668 } 

669 

670 .kelp-md\\:grid-cols-1 { 

671 grid-template-columns: repeat(1, minmax(0, 1fr)); 

672 } 

673}""" 

674 

675 def _generate_animations(self) -> str: 

676 """Generate animation system.""" 

677 if not self.settings or not self.settings.enable_animations: 

678 return "" 

679 

680 return """ 

681/* Animation System */ 

682@keyframes kelp-fade-in { 

683 from { 

684 opacity: 0; 

685 transform: translateY(0.5rem); 

686 } 

687 to { 

688 opacity: 1; 

689 transform: translateY(0); 

690 } 

691} 

692 

693@keyframes kelp-slide-up { 

694 from { 

695 transform: translateY(1rem); 

696 opacity: 0; 

697 } 

698 to { 

699 transform: translateY(0); 

700 opacity: 1; 

701 } 

702} 

703 

704@keyframes kelp-scale-in { 

705 from { 

706 transform: scale(0.95); 

707 opacity: 0; 

708 } 

709 to { 

710 transform: scale(1); 

711 opacity: 1; 

712 } 

713} 

714 

715.kelp-animate-fade-in { 

716 animation: kelp-fade-in 0.3s ease-out; 

717} 

718 

719.kelp-animate-slide-up { 

720 animation: kelp-slide-up 0.4s ease-out; 

721} 

722 

723.kelp-animate-scale-in { 

724 animation: kelp-scale-in 0.2s ease-out; 

725} 

726 

727.kelp-transition { 

728 transition: all 0.2s ease; 

729} 

730 

731.kelp-transition-colors { 

732 transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease; 

733}""" 

734 

735 def get_component_class(self, component: str) -> str: 

736 """Get Kelp-specific classes.""" 

737 class_map = { 

738 # Layout 

739 "container": "kelp-container", 

740 "container-sm": "kelp-container-sm", 

741 "container-md": "kelp-container-md", 

742 "container-lg": "kelp-container-lg", 

743 "container-xl": "kelp-container-xl", 

744 "flex": "kelp-flex", 

745 "flex-col": "kelp-flex-col", 

746 "grid": "kelp-grid", 

747 # Components 

748 "card": "kelp-card", 

749 "card-header": "kelp-card-header", 

750 "card-body": "kelp-card-body", 

751 "card-footer": "kelp-card-footer", 

752 # Buttons 

753 "btn": "kelp-btn", 

754 "btn-primary": "kelp-btn kelp-btn-primary", 

755 "btn-secondary": "kelp-btn kelp-btn-secondary", 

756 "btn-outline": "kelp-btn kelp-btn-outline", 

757 "btn-ghost": "kelp-btn kelp-btn-ghost", 

758 "btn-sm": "kelp-btn kelp-btn-sm", 

759 "btn-lg": "kelp-btn kelp-btn-lg", 

760 # Forms 

761 "form-group": "kelp-form-group", 

762 "label": "kelp-label", 

763 "input": "kelp-input", 

764 "textarea": "kelp-input kelp-textarea", 

765 "select": "kelp-input kelp-select", 

766 # Alerts 

767 "alert": "kelp-alert", 

768 "alert-info": "kelp-alert kelp-alert-info", 

769 "alert-success": "kelp-alert kelp-alert-success", 

770 "alert-warning": "kelp-alert kelp-alert-warning", 

771 "alert-error": "kelp-alert kelp-alert-error", 

772 # Badges 

773 "badge": "kelp-badge", 

774 "badge-primary": "kelp-badge kelp-badge-primary", 

775 "badge-secondary": "kelp-badge kelp-badge-secondary", 

776 "badge-gray": "kelp-badge kelp-badge-gray", 

777 } 

778 

779 return class_map.get(component, f"kelp-{component}") 

780 

781 

782# Template function registration for FastBlocks 

783def _determine_component_tag(component_type: str, attributes: dict[str, Any]) -> str: 

784 """Determine HTML tag for Kelp component type.""" 

785 if component_type in ( 

786 "btn", 

787 "btn-primary", 

788 "btn-secondary", 

789 "btn-outline", 

790 "btn-ghost", 

791 ): 

792 return "button" 

793 

794 if component_type in ("input", "textarea", "select"): 

795 if component_type == "input": 

796 attributes.setdefault("type", "text") 

797 return "input" 

798 return "textarea" if component_type == "textarea" else component_type 

799 

800 return "div" 

801 

802 

803def _build_kelp_component_html( 

804 tag: str, 

805 component_class: str, 

806 content: str, 

807 attributes: dict[str, Any], 

808) -> str: 

809 """Build Kelp component HTML.""" 

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

811 

812 if tag == "input": 

813 return f'<{tag} class="{component_class}" {attr_string}>' 

814 

815 return f'<{tag} class="{component_class}" {attr_string}>{content}</{tag}>' 

816 

817 

818def register_kelp_functions(env: Any) -> None: 

819 """Register Kelp functions for Jinja2 templates.""" 

820 

821 @env.global_("kelp_stylesheet_links") # type: ignore[misc] 

822 def kelp_stylesheet_links() -> str: 

823 """Global function for Kelp stylesheet links.""" 

824 styles = depends.get_sync("styles") 

825 if isinstance(styles, KelpStyle): 

826 return "\n".join(styles.get_stylesheet_links()) 

827 return "" 

828 

829 @env.filter("kelp_class") # type: ignore[misc] 

830 def kelp_class_filter(component: str) -> str: 

831 """Filter for getting Kelp component classes.""" 

832 styles = depends.get_sync("styles") 

833 if isinstance(styles, KelpStyle): 

834 return styles.get_component_class(component) 

835 return component 

836 

837 @env.global_("kelp_component") # type: ignore[misc] 

838 def kelp_component( 

839 component_type: str, content: str = "", **attributes: Any 

840 ) -> str: 

841 """Generate Kelp component.""" 

842 styles = depends.get_sync("styles") 

843 if not isinstance(styles, KelpStyle): 

844 return f"<div>{content}</div>" 

845 

846 component_class = styles.get_component_class(component_type) 

847 

848 # Add custom classes 

849 if "class" in attributes: 

850 component_class += f" {attributes.pop('class')}" 

851 

852 # Determine tag and build HTML 

853 tag = _determine_component_tag(component_type, attributes) 

854 return _build_kelp_component_html(tag, component_class, content, attributes) 

855 

856 

857StyleSettings = KelpStyleSettings 

858Style = KelpStyle 

859 

860depends.set(Style, "kelp") 

861 

862 

863# ACB 0.19.0+ compatibility 

864__all__ = [ 

865 "KelpStyle", 

866 "KelpStyleSettings", 

867 "register_kelp_functions", 

868 "Style", 

869 "StyleSettings", 

870]