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
« 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."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.depends import depends
9from ._base import StyleBase, StyleBaseSettings
12class KelpStyleSettings(StyleBaseSettings):
13 """Settings for Kelp styles adapter."""
15 # Required ACB 0.19.0+ metadata
16 MODULE_ID: UUID = UUID("01937d86-8b6d-a07e-c9fa-e8f9a0b1c2d3") # Static UUID7
17 MODULE_STATUS: str = "stable"
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
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
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 ]
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 }
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 }
82 # Shadow system
83 enable_shadows: bool = True
84 enable_animations: bool = True
87class KelpStyle(StyleBase):
88 """Kelp styles adapter with modern component system."""
90 # Required ACB 0.19.0+ metadata
91 MODULE_ID: UUID = UUID("01937d86-8b6d-a07e-c9fa-e8f9a0b1c2d3") # Static UUID7
92 MODULE_STATUS: str = "stable"
94 def __init__(self) -> None:
95 """Initialize Kelp adapter."""
96 super().__init__()
97 self.settings: KelpStyleSettings | None = None
99 # Register with ACB dependency system
100 with suppress(Exception):
101 depends.set(self)
103 def get_stylesheet_links(self) -> list[str]:
104 """Get Kelp stylesheet links."""
105 if not self.settings:
106 self.settings = KelpStyleSettings()
108 links = []
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>")
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 )
125 return links
127 def _generate_kelp_css(self) -> str:
128 """Generate Kelp CSS framework."""
129 if not self.settings:
130 self.settings = KelpStyleSettings()
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()
138 css = f"""
139/* Kelp CSS Framework for FastBlocks */
140{color_vars}
141{spacing_vars}
142{typography_vars}
143{radius_vars}
145/* Base Reset */
146*, *::before, *::after {{
147 box-sizing: border-box;
148}}
150* {{
151 margin: 0;
152 padding: 0;
153}}
155html {{
156 scroll-behavior: smooth;
157 -webkit-font-smoothing: antialiased;
158 -moz-osx-font-smoothing: grayscale;
159}}
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}}
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}}
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; }}
184/* Flexbox Grid */
185.kelp-flex {{
186 display: flex;
187}}
189.kelp-flex-col {{
190 flex-direction: column;
191}}
193.kelp-flex-wrap {{
194 flex-wrap: wrap;
195}}
197.kelp-items-center {{
198 align-items: center;
199}}
201.kelp-items-start {{
202 align-items: flex-start;
203}}
205.kelp-items-end {{
206 align-items: flex-end;
207}}
209.kelp-justify-center {{
210 justify-content: center;
211}}
213.kelp-justify-between {{
214 justify-content: space-between;
215}}
217.kelp-justify-around {{
218 justify-content: space-around;
219}}
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); }}
228/* Grid System */
229.kelp-grid {{
230 display: grid;
231}}
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)); }}
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}}
249.kelp-card:hover {{
250 box-shadow: var(--kelp-shadow-lg);
251 transform: translateY(-2px);
252}}
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}}
260.kelp-card-body {{
261 padding: var(--kelp-space-6);
262}}
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}}
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}}
289.kelp-btn:focus {{
290 outline: 2px solid var(--kelp-primary-500);
291 outline-offset: 2px;
292}}
294.kelp-btn:disabled {{
295 opacity: 0.6;
296 cursor: not-allowed;
297}}
299.kelp-btn-primary {{
300 background: var(--kelp-primary-600);
301 border-color: var(--kelp-primary-600);
302 color: white;
303}}
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}}
312.kelp-btn-secondary {{
313 background: var(--kelp-secondary-600);
314 border-color: var(--kelp-secondary-600);
315 color: white;
316}}
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}}
325.kelp-btn-outline {{
326 background: transparent;
327 border-color: var(--kelp-gray-300);
328 color: var(--kelp-gray-700);
329}}
331.kelp-btn-outline:hover:not(:disabled) {{
332 background: var(--kelp-gray-50);
333 border-color: var(--kelp-gray-400);
334}}
336.kelp-btn-ghost {{
337 background: transparent;
338 border-color: transparent;
339 color: var(--kelp-gray-700);
340}}
342.kelp-btn-ghost:hover:not(:disabled) {{
343 background: var(--kelp-gray-100);
344}}
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}}
352.kelp-btn-lg {{
353 padding: var(--kelp-space-4) var(--kelp-space-8);
354 font-size: var(--kelp-text-base);
355}}
357/* Component: Form Controls */
358.kelp-form-group {{
359 margin-bottom: var(--kelp-space-4);
360}}
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}}
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}}
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}}
388.kelp-input:disabled {{
389 background: var(--kelp-gray-100);
390 color: var(--kelp-gray-500);
391 cursor: not-allowed;
392}}
394.kelp-textarea {{
395 resize: vertical;
396 min-height: 80px;
397}}
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}}
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}}
415.kelp-alert-info {{
416 background: var(--kelp-primary-50);
417 border-color: var(--kelp-primary-200);
418 color: var(--kelp-primary-800);
419}}
421.kelp-alert-success {{
422 background: var(--kelp-secondary-50);
423 border-color: var(--kelp-secondary-200);
424 color: var(--kelp-secondary-800);
425}}
427.kelp-alert-warning {{
428 background: var(--kelp-accent-50);
429 border-color: var(--kelp-accent-200);
430 color: var(--kelp-accent-800);
431}}
433.kelp-alert-error {{
434 background: #fef2f2;
435 border-color: #fecaca;
436 color: #991b1b;
437}}
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}}
451.kelp-badge-primary {{
452 background: var(--kelp-primary-100);
453 color: var(--kelp-primary-800);
454}}
456.kelp-badge-secondary {{
457 background: var(--kelp-secondary-100);
458 color: var(--kelp-secondary-800);
459}}
461.kelp-badge-gray {{
462 background: var(--kelp-gray-100);
463 color: var(--kelp-gray-800);
464}}
466/* Utility Classes */
467{self._generate_utility_classes()}
469/* Responsive Design */
470{self._generate_responsive_classes()}
472/* Animation System */
473{self._generate_animations()}
474"""
475 return css
477 def _generate_color_variables(self) -> str:
478 """Generate CSS color variables based on HSL."""
479 if not self.settings:
480 self.settings = KelpStyleSettings()
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%);"""
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")}
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%);
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}}"""
523 def _generate_spacing_variables(self) -> str:
524 """Generate spacing variables."""
525 if not self.settings:
526 self.settings = KelpStyleSettings()
528 vars_css = ""
529 for i, value in enumerate(self.settings.spacing_scale):
530 vars_css += f" --kelp-space-{i}: {value}rem;\n"
532 return f"""
533 /* Spacing System */
534{vars_css}"""
536 def _generate_typography_variables(self) -> str:
537 """Generate typography variables."""
538 if not self.settings:
539 self.settings = KelpStyleSettings()
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"""
547 for name, size in self.settings.font_scale.items():
548 font_vars += f" --kelp-text-{name}: {size};\n"
550 return font_vars
552 def _generate_radius_variables(self) -> str:
553 """Generate border radius variables."""
554 if not self.settings:
555 self.settings = KelpStyleSettings()
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"
561 return radius_vars
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; }
573.kelp-font-sans { font-family: var(--kelp-font-sans); }
574.kelp-font-mono { font-family: var(--kelp-font-mono); }
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); }
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; }
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; }
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); }
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); }
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; }
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; }
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); }
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; }"""
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 }
648 .kelp-grid-cols-2 {
649 grid-template-columns: repeat(1, minmax(0, 1fr));
650 }
652 .kelp-grid-cols-3 {
653 grid-template-columns: repeat(1, minmax(0, 1fr));
654 }
656 .kelp-grid-cols-4 {
657 grid-template-columns: repeat(2, minmax(0, 1fr));
658 }
659}
661@media (max-width: 768px) {
662 .kelp-md\\:hidden {
663 display: none;
664 }
666 .kelp-md\\:flex {
667 display: flex;
668 }
670 .kelp-md\\:grid-cols-1 {
671 grid-template-columns: repeat(1, minmax(0, 1fr));
672 }
673}"""
675 def _generate_animations(self) -> str:
676 """Generate animation system."""
677 if not self.settings or not self.settings.enable_animations:
678 return ""
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}
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}
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}
715.kelp-animate-fade-in {
716 animation: kelp-fade-in 0.3s ease-out;
717}
719.kelp-animate-slide-up {
720 animation: kelp-slide-up 0.4s ease-out;
721}
723.kelp-animate-scale-in {
724 animation: kelp-scale-in 0.2s ease-out;
725}
727.kelp-transition {
728 transition: all 0.2s ease;
729}
731.kelp-transition-colors {
732 transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
733}"""
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 }
779 return class_map.get(component, f"kelp-{component}")
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"
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
800 return "div"
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())
812 if tag == "input":
813 return f'<{tag} class="{component_class}" {attr_string}>'
815 return f'<{tag} class="{component_class}" {attr_string}>{content}</{tag}>'
818def register_kelp_functions(env: Any) -> None:
819 """Register Kelp functions for Jinja2 templates."""
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 ""
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
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>"
846 component_class = styles.get_component_class(component_type)
848 # Add custom classes
849 if "class" in attributes:
850 component_class += f" {attributes.pop('class')}"
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)
857StyleSettings = KelpStyleSettings
858Style = KelpStyle
860depends.set(Style, "kelp")
863# ACB 0.19.0+ compatibility
864__all__ = [
865 "KelpStyle",
866 "KelpStyleSettings",
867 "register_kelp_functions",
868 "Style",
869 "StyleSettings",
870]