Coverage for fastblocks/adapters/styles/kelpui.py: 0%
124 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""KelpUI styles adapter for FastBlocks with component system."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.config import Settings
8from acb.depends import depends
10from ._base import StylesBase
13class KelpUISettings(Settings): # type: ignore[misc]
14 """Settings for KelpUI styles adapter."""
16 # Required ACB 0.19.0+ metadata
17 MODULE_ID: UUID = UUID("01937d86-8b6d-a07e-c9fa-e8f9a0b1c2d3") # Static UUID7
18 MODULE_STATUS: str = "stable"
20 # KelpUI configuration
21 version: str = "latest"
22 cdn_url: str = "https://cdn.jsdelivr.net/npm/kelpui"
23 theme: str = "default" # default, dark, ocean, forest, sunset
25 # Color system
26 primary_hue: int = 210 # Blue
27 secondary_hue: int = 160 # Green
28 accent_hue: int = 45 # Orange
29 neutral_hue: int = 220 # Cool gray
31 # Spacing system (rem units)
32 spacing_scale: list[str] = [
33 "0",
34 "0.25",
35 "0.5",
36 "0.75",
37 "1",
38 "1.25",
39 "1.5",
40 "2",
41 "2.5",
42 "3",
43 "4",
44 "5",
45 "6",
46 "8",
47 "10",
48 "12",
49 "16",
50 "20",
51 "24",
52 ]
54 # Typography
55 font_family_sans: str = "Inter, system-ui, -apple-system, sans-serif"
56 font_family_mono: str = "JetBrains Mono, 'Fira Code', Consolas, monospace"
57 font_scale: dict[str, str] = {
58 "xs": "0.75rem",
59 "sm": "0.875rem",
60 "base": "1rem",
61 "lg": "1.125rem",
62 "xl": "1.25rem",
63 "2xl": "1.5rem",
64 "3xl": "1.875rem",
65 "4xl": "2.25rem",
66 "5xl": "3rem",
67 "6xl": "3.75rem",
68 }
70 # Border radius
71 radius_scale: dict[str, str] = {
72 "none": "0",
73 "sm": "0.125rem",
74 "base": "0.25rem",
75 "md": "0.375rem",
76 "lg": "0.5rem",
77 "xl": "0.75rem",
78 "2xl": "1rem",
79 "3xl": "1.5rem",
80 "full": "9999px",
81 }
83 # Shadow system
84 enable_shadows: bool = True
85 enable_animations: bool = True
88class KelpUIAdapter(StylesBase):
89 """KelpUI styles adapter with modern component system."""
91 # Required ACB 0.19.0+ metadata
92 MODULE_ID: UUID = UUID("01937d86-8b6d-a07e-c9fa-e8f9a0b1c2d3") # Static UUID7
93 MODULE_STATUS: str = "stable"
95 def __init__(self) -> None:
96 """Initialize KelpUI adapter."""
97 super().__init__()
98 self.settings: KelpUISettings | None = None
100 # Register with ACB dependency system
101 with suppress(Exception):
102 depends.set(self)
104 def get_stylesheet_links(self) -> list[str]:
105 """Get KelpUI stylesheet links."""
106 if not self.settings:
107 self.settings = KelpUISettings()
109 links = []
111 # KelpUI base CSS (if available from CDN)
112 # Note: KelpUI might be a custom framework, so we generate it inline
113 kelp_css = self._generate_kelpui_css()
114 links.append(f"<style>{kelp_css}</style>")
116 # Inter font for better typography
117 links.extend(
118 (
119 '<link rel="preconnect" href="https://fonts.googleapis.com">',
120 '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
121 '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">',
122 '<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">',
123 )
124 )
126 return links
128 def _generate_kelpui_css(self) -> str:
129 """Generate KelpUI CSS framework."""
130 if not self.settings:
131 self.settings = KelpUISettings()
133 # Generate color variables based on HSL
134 color_vars = self._generate_color_variables()
135 spacing_vars = self._generate_spacing_variables()
136 typography_vars = self._generate_typography_variables()
137 radius_vars = self._generate_radius_variables()
139 css = f"""
140/* KelpUI CSS Framework for FastBlocks */
141{color_vars}
142{spacing_vars}
143{typography_vars}
144{radius_vars}
146/* Base Reset */
147*, *::before, *::after {{
148 box-sizing: border-box;
149}}
151* {{
152 margin: 0;
153 padding: 0;
154}}
156html {{
157 scroll-behavior: smooth;
158 -webkit-font-smoothing: antialiased;
159 -moz-osx-font-smoothing: grayscale;
160}}
162body {{
163 font-family: var(--kelp-font-sans);
164 font-size: var(--kelp-text-base);
165 line-height: 1.6;
166 color: var(--kelp-gray-900);
167 background-color: var(--kelp-gray-50);
168 min-height: 100vh;
169}}
171/* Layout System */
172.kelp-container {{
173 width: 100%;
174 max-width: 1200px;
175 margin: 0 auto;
176 padding: 0 var(--kelp-space-4);
177}}
179.kelp-container-sm {{ max-width: 640px; }}
180.kelp-container-md {{ max-width: 768px; }}
181.kelp-container-lg {{ max-width: 1024px; }}
182.kelp-container-xl {{ max-width: 1280px; }}
183.kelp-container-2xl {{ max-width: 1536px; }}
185/* Flexbox Grid */
186.kelp-flex {{
187 display: flex;
188}}
190.kelp-flex-col {{
191 flex-direction: column;
192}}
194.kelp-flex-wrap {{
195 flex-wrap: wrap;
196}}
198.kelp-items-center {{
199 align-items: center;
200}}
202.kelp-items-start {{
203 align-items: flex-start;
204}}
206.kelp-items-end {{
207 align-items: flex-end;
208}}
210.kelp-justify-center {{
211 justify-content: center;
212}}
214.kelp-justify-between {{
215 justify-content: space-between;
216}}
218.kelp-justify-around {{
219 justify-content: space-around;
220}}
222.kelp-gap-1 {{ gap: var(--kelp-space-1); }}
223.kelp-gap-2 {{ gap: var(--kelp-space-2); }}
224.kelp-gap-3 {{ gap: var(--kelp-space-3); }}
225.kelp-gap-4 {{ gap: var(--kelp-space-4); }}
226.kelp-gap-6 {{ gap: var(--kelp-space-6); }}
227.kelp-gap-8 {{ gap: var(--kelp-space-8); }}
229/* Grid System */
230.kelp-grid {{
231 display: grid;
232}}
234.kelp-grid-cols-1 {{ grid-template-columns: repeat(1, minmax(0, 1fr)); }}
235.kelp-grid-cols-2 {{ grid-template-columns: repeat(2, minmax(0, 1fr)); }}
236.kelp-grid-cols-3 {{ grid-template-columns: repeat(3, minmax(0, 1fr)); }}
237.kelp-grid-cols-4 {{ grid-template-columns: repeat(4, minmax(0, 1fr)); }}
238.kelp-grid-cols-6 {{ grid-template-columns: repeat(6, minmax(0, 1fr)); }}
239.kelp-grid-cols-12 {{ grid-template-columns: repeat(12, minmax(0, 1fr)); }}
241/* Component: Card */
242.kelp-card {{
243 background: white;
244 border: 1px solid var(--kelp-gray-200);
245 border-radius: var(--kelp-radius-lg);
246 overflow: hidden;
247 transition: all 0.2s ease;
248}}
250.kelp-card:hover {{
251 box-shadow: var(--kelp-shadow-lg);
252 transform: translateY(-2px);
253}}
255.kelp-card-header {{
256 padding: var(--kelp-space-4) var(--kelp-space-6);
257 border-bottom: 1px solid var(--kelp-gray-200);
258 background: var(--kelp-gray-50);
259}}
261.kelp-card-body {{
262 padding: var(--kelp-space-6);
263}}
265.kelp-card-footer {{
266 padding: var(--kelp-space-4) var(--kelp-space-6);
267 border-top: 1px solid var(--kelp-gray-200);
268 background: var(--kelp-gray-50);
269}}
271/* Component: Button */
272.kelp-btn {{
273 display: inline-flex;
274 align-items: center;
275 justify-content: center;
276 gap: var(--kelp-space-2);
277 padding: var(--kelp-space-3) var(--kelp-space-6);
278 font-family: inherit;
279 font-size: var(--kelp-text-sm);
280 font-weight: 500;
281 line-height: 1;
282 border: 1px solid transparent;
283 border-radius: var(--kelp-radius-md);
284 cursor: pointer;
285 transition: all 0.2s ease;
286 text-decoration: none;
287 white-space: nowrap;
288}}
290.kelp-btn:focus {{
291 outline: 2px solid var(--kelp-primary-500);
292 outline-offset: 2px;
293}}
295.kelp-btn:disabled {{
296 opacity: 0.6;
297 cursor: not-allowed;
298}}
300.kelp-btn-primary {{
301 background: var(--kelp-primary-600);
302 border-color: var(--kelp-primary-600);
303 color: white;
304}}
306.kelp-btn-primary:hover:not(:disabled) {{
307 background: var(--kelp-primary-700);
308 border-color: var(--kelp-primary-700);
309 transform: translateY(-1px);
310 box-shadow: var(--kelp-shadow-md);
311}}
313.kelp-btn-secondary {{
314 background: var(--kelp-secondary-600);
315 border-color: var(--kelp-secondary-600);
316 color: white;
317}}
319.kelp-btn-secondary:hover:not(:disabled) {{
320 background: var(--kelp-secondary-700);
321 border-color: var(--kelp-secondary-700);
322 transform: translateY(-1px);
323 box-shadow: var(--kelp-shadow-md);
324}}
326.kelp-btn-outline {{
327 background: transparent;
328 border-color: var(--kelp-gray-300);
329 color: var(--kelp-gray-700);
330}}
332.kelp-btn-outline:hover:not(:disabled) {{
333 background: var(--kelp-gray-50);
334 border-color: var(--kelp-gray-400);
335}}
337.kelp-btn-ghost {{
338 background: transparent;
339 border-color: transparent;
340 color: var(--kelp-gray-700);
341}}
343.kelp-btn-ghost:hover:not(:disabled) {{
344 background: var(--kelp-gray-100);
345}}
347/* Button Sizes */
348.kelp-btn-sm {{
349 padding: var(--kelp-space-2) var(--kelp-space-4);
350 font-size: var(--kelp-text-xs);
351}}
353.kelp-btn-lg {{
354 padding: var(--kelp-space-4) var(--kelp-space-8);
355 font-size: var(--kelp-text-base);
356}}
358/* Component: Form Controls */
359.kelp-form-group {{
360 margin-bottom: var(--kelp-space-4);
361}}
363.kelp-label {{
364 display: block;
365 margin-bottom: var(--kelp-space-2);
366 font-size: var(--kelp-text-sm);
367 font-weight: 500;
368 color: var(--kelp-gray-700);
369}}
371.kelp-input {{
372 width: 100%;
373 padding: var(--kelp-space-3);
374 font-family: inherit;
375 font-size: var(--kelp-text-sm);
376 border: 1px solid var(--kelp-gray-300);
377 border-radius: var(--kelp-radius-md);
378 background: white;
379 color: var(--kelp-gray-900);
380 transition: all 0.2s ease;
381}}
383.kelp-input:focus {{
384 outline: none;
385 border-color: var(--kelp-primary-500);
386 box-shadow: 0 0 0 3px var(--kelp-primary-100);
387}}
389.kelp-input:disabled {{
390 background: var(--kelp-gray-100);
391 color: var(--kelp-gray-500);
392 cursor: not-allowed;
393}}
395.kelp-textarea {{
396 resize: vertical;
397 min-height: 80px;
398}}
400.kelp-select {{
401 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");
402 background-position: right 0.5rem center;
403 background-repeat: no-repeat;
404 background-size: 1.5em 1.5em;
405 padding-right: 2.5rem;
406}}
408/* Component: Alert */
409.kelp-alert {{
410 padding: var(--kelp-space-4);
411 border-radius: var(--kelp-radius-md);
412 border: 1px solid;
413 margin-bottom: var(--kelp-space-4);
414}}
416.kelp-alert-info {{
417 background: var(--kelp-primary-50);
418 border-color: var(--kelp-primary-200);
419 color: var(--kelp-primary-800);
420}}
422.kelp-alert-success {{
423 background: var(--kelp-secondary-50);
424 border-color: var(--kelp-secondary-200);
425 color: var(--kelp-secondary-800);
426}}
428.kelp-alert-warning {{
429 background: var(--kelp-accent-50);
430 border-color: var(--kelp-accent-200);
431 color: var(--kelp-accent-800);
432}}
434.kelp-alert-error {{
435 background: #fef2f2;
436 border-color: #fecaca;
437 color: #991b1b;
438}}
440/* Component: Badge */
441.kelp-badge {{
442 display: inline-flex;
443 align-items: center;
444 padding: var(--kelp-space-1) var(--kelp-space-2);
445 font-size: var(--kelp-text-xs);
446 font-weight: 500;
447 border-radius: var(--kelp-radius-full);
448 text-transform: uppercase;
449 letter-spacing: 0.05em;
450}}
452.kelp-badge-primary {{
453 background: var(--kelp-primary-100);
454 color: var(--kelp-primary-800);
455}}
457.kelp-badge-secondary {{
458 background: var(--kelp-secondary-100);
459 color: var(--kelp-secondary-800);
460}}
462.kelp-badge-gray {{
463 background: var(--kelp-gray-100);
464 color: var(--kelp-gray-800);
465}}
467/* Utility Classes */
468{self._generate_utility_classes()}
470/* Responsive Design */
471{self._generate_responsive_classes()}
473/* Animation System */
474{self._generate_animations()}
475"""
476 return css
478 def _generate_color_variables(self) -> str:
479 """Generate CSS color variables based on HSL."""
480 if not self.settings:
481 self.settings = KelpUISettings()
483 def hsl_colors(hue: int, prefix: str) -> str:
484 """Generate HSL color scale."""
485 return f"""
486 --kelp-{prefix}-50: hsl({hue}, 100%, 97%);
487 --kelp-{prefix}-100: hsl({hue}, 100%, 94%);
488 --kelp-{prefix}-200: hsl({hue}, 100%, 87%);
489 --kelp-{prefix}-300: hsl({hue}, 100%, 80%);
490 --kelp-{prefix}-400: hsl({hue}, 100%, 66%);
491 --kelp-{prefix}-500: hsl({hue}, 100%, 50%);
492 --kelp-{prefix}-600: hsl({hue}, 100%, 45%);
493 --kelp-{prefix}-700: hsl({hue}, 100%, 35%);
494 --kelp-{prefix}-800: hsl({hue}, 100%, 25%);
495 --kelp-{prefix}-900: hsl({hue}, 100%, 15%);"""
497 return f"""
498:root {{
499 /* Color System */
500 {hsl_colors(self.settings.primary_hue, "primary")}
501 {hsl_colors(self.settings.secondary_hue, "secondary")}
502 {hsl_colors(self.settings.accent_hue, "accent")}
504 /* Neutral Colors */
505 --kelp-gray-50: hsl({self.settings.neutral_hue}, 20%, 98%);
506 --kelp-gray-100: hsl({self.settings.neutral_hue}, 20%, 95%);
507 --kelp-gray-200: hsl({self.settings.neutral_hue}, 15%, 89%);
508 --kelp-gray-300: hsl({self.settings.neutral_hue}, 10%, 78%);
509 --kelp-gray-400: hsl({self.settings.neutral_hue}, 8%, 56%);
510 --kelp-gray-500: hsl({self.settings.neutral_hue}, 6%, 45%);
511 --kelp-gray-600: hsl({self.settings.neutral_hue}, 5%, 35%);
512 --kelp-gray-700: hsl({self.settings.neutral_hue}, 5%, 25%);
513 --kelp-gray-800: hsl({self.settings.neutral_hue}, 5%, 15%);
514 --kelp-gray-900: hsl({self.settings.neutral_hue}, 5%, 9%);
516 /* Shadow System */
517 --kelp-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
518 --kelp-shadow-base: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
519 --kelp-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
520 --kelp-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
521 --kelp-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
522}}"""
524 def _generate_spacing_variables(self) -> str:
525 """Generate spacing variables."""
526 if not self.settings:
527 self.settings = KelpUISettings()
529 vars_css = ""
530 for i, value in enumerate(self.settings.spacing_scale):
531 vars_css += f" --kelp-space-{i}: {value}rem;\n"
533 return f"""
534 /* Spacing System */
535{vars_css}"""
537 def _generate_typography_variables(self) -> str:
538 """Generate typography variables."""
539 if not self.settings:
540 self.settings = KelpUISettings()
542 font_vars = f"""
543 /* Typography System */
544 --kelp-font-sans: {self.settings.font_family_sans};
545 --kelp-font-mono: {self.settings.font_family_mono};
546"""
548 for name, size in self.settings.font_scale.items():
549 font_vars += f" --kelp-text-{name}: {size};\n"
551 return font_vars
553 def _generate_radius_variables(self) -> str:
554 """Generate border radius variables."""
555 if not self.settings:
556 self.settings = KelpUISettings()
558 radius_vars = " /* Border Radius System */\n"
559 for name, value in self.settings.radius_scale.items():
560 radius_vars += f" --kelp-radius-{name}: {value};\n"
562 return radius_vars
564 def _generate_utility_classes(self) -> 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 def _generate_responsive_classes(self) -> str:
639 """Generate responsive design classes."""
640 return """
641/* Responsive Design */
642@media (max-width: 640px) {
643 .kelp-container {
644 padding: 0 var(--kelp-space-2);
645 }
647 .kelp-grid-cols-2 {
648 grid-template-columns: repeat(1, minmax(0, 1fr));
649 }
651 .kelp-grid-cols-3 {
652 grid-template-columns: repeat(1, minmax(0, 1fr));
653 }
655 .kelp-grid-cols-4 {
656 grid-template-columns: repeat(2, minmax(0, 1fr));
657 }
658}
660@media (max-width: 768px) {
661 .kelp-md\\:hidden {
662 display: none;
663 }
665 .kelp-md\\:flex {
666 display: flex;
667 }
669 .kelp-md\\:grid-cols-1 {
670 grid-template-columns: repeat(1, minmax(0, 1fr));
671 }
672}"""
674 def _generate_animations(self) -> str:
675 """Generate animation system."""
676 if not self.settings or not self.settings.enable_animations:
677 return ""
679 return """
680/* Animation System */
681@keyframes kelp-fade-in {
682 from {
683 opacity: 0;
684 transform: translateY(0.5rem);
685 }
686 to {
687 opacity: 1;
688 transform: translateY(0);
689 }
690}
692@keyframes kelp-slide-up {
693 from {
694 transform: translateY(1rem);
695 opacity: 0;
696 }
697 to {
698 transform: translateY(0);
699 opacity: 1;
700 }
701}
703@keyframes kelp-scale-in {
704 from {
705 transform: scale(0.95);
706 opacity: 0;
707 }
708 to {
709 transform: scale(1);
710 opacity: 1;
711 }
712}
714.kelp-animate-fade-in {
715 animation: kelp-fade-in 0.3s ease-out;
716}
718.kelp-animate-slide-up {
719 animation: kelp-slide-up 0.4s ease-out;
720}
722.kelp-animate-scale-in {
723 animation: kelp-scale-in 0.2s ease-out;
724}
726.kelp-transition {
727 transition: all 0.2s ease;
728}
730.kelp-transition-colors {
731 transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
732}"""
734 def get_component_class(self, component: str) -> str:
735 """Get KelpUI-specific classes."""
736 class_map = {
737 # Layout
738 "container": "kelp-container",
739 "container-sm": "kelp-container-sm",
740 "container-md": "kelp-container-md",
741 "container-lg": "kelp-container-lg",
742 "container-xl": "kelp-container-xl",
743 "flex": "kelp-flex",
744 "flex-col": "kelp-flex-col",
745 "grid": "kelp-grid",
746 # Components
747 "card": "kelp-card",
748 "card-header": "kelp-card-header",
749 "card-body": "kelp-card-body",
750 "card-footer": "kelp-card-footer",
751 # Buttons
752 "btn": "kelp-btn",
753 "btn-primary": "kelp-btn kelp-btn-primary",
754 "btn-secondary": "kelp-btn kelp-btn-secondary",
755 "btn-outline": "kelp-btn kelp-btn-outline",
756 "btn-ghost": "kelp-btn kelp-btn-ghost",
757 "btn-sm": "kelp-btn kelp-btn-sm",
758 "btn-lg": "kelp-btn kelp-btn-lg",
759 # Forms
760 "form-group": "kelp-form-group",
761 "label": "kelp-label",
762 "input": "kelp-input",
763 "textarea": "kelp-input kelp-textarea",
764 "select": "kelp-input kelp-select",
765 # Alerts
766 "alert": "kelp-alert",
767 "alert-info": "kelp-alert kelp-alert-info",
768 "alert-success": "kelp-alert kelp-alert-success",
769 "alert-warning": "kelp-alert kelp-alert-warning",
770 "alert-error": "kelp-alert kelp-alert-error",
771 # Badges
772 "badge": "kelp-badge",
773 "badge-primary": "kelp-badge kelp-badge-primary",
774 "badge-secondary": "kelp-badge kelp-badge-secondary",
775 "badge-gray": "kelp-badge kelp-badge-gray",
776 }
778 return class_map.get(component, f"kelp-{component}")
781# Template function registration for FastBlocks
782def _determine_component_tag(component_type: str, attributes: dict[str, Any]) -> str:
783 """Determine HTML tag for KelpUI component type."""
784 if component_type in (
785 "btn",
786 "btn-primary",
787 "btn-secondary",
788 "btn-outline",
789 "btn-ghost",
790 ):
791 return "button"
793 if component_type in ("input", "textarea", "select"):
794 if component_type == "input":
795 attributes.setdefault("type", "text")
796 return "input"
797 return "textarea" if component_type == "textarea" else component_type
799 return "div"
802def _build_kelp_component_html(
803 tag: str,
804 component_class: str,
805 content: str,
806 attributes: dict[str, Any],
807) -> str:
808 """Build KelpUI component HTML."""
809 attr_string = " ".join(f'{k}="{v}"' for k, v in attributes.items())
811 if tag == "input":
812 return f'<{tag} class="{component_class}" {attr_string}>'
814 return f'<{tag} class="{component_class}" {attr_string}>{content}</{tag}>'
817def register_kelpui_functions(env: Any) -> None:
818 """Register KelpUI functions for Jinja2 templates."""
820 @env.global_("kelp_stylesheet_links") # type: ignore[misc]
821 def kelp_stylesheet_links() -> str:
822 """Global function for KelpUI stylesheet links."""
823 styles = depends.get("styles")
824 if isinstance(styles, KelpUIAdapter):
825 return "\n".join(styles.get_stylesheet_links())
826 return ""
828 @env.filter("kelp_class") # type: ignore[misc]
829 def kelp_class_filter(component: str) -> str:
830 """Filter for getting KelpUI component classes."""
831 styles = depends.get("styles")
832 if isinstance(styles, KelpUIAdapter):
833 return styles.get_component_class(component)
834 return component
836 @env.global_("kelp_component") # type: ignore[misc]
837 def kelp_component(
838 component_type: str, content: str = "", **attributes: Any
839 ) -> str:
840 """Generate KelpUI component."""
841 styles = depends.get("styles")
842 if not isinstance(styles, KelpUIAdapter):
843 return f"<div>{content}</div>"
845 component_class = styles.get_component_class(component_type)
847 # Add custom classes
848 if "class" in attributes:
849 component_class += f" {attributes.pop('class')}"
851 # Determine tag and build HTML
852 tag = _determine_component_tag(component_type, attributes)
853 return _build_kelp_component_html(tag, component_class, content, attributes)
856# ACB 0.19.0+ compatibility
857__all__ = ["KelpUIAdapter", "KelpUISettings", "register_kelpui_functions"]